diff --git a/services/web/.dockerignore b/services/web/.dockerignore new file mode 100644 index 0000000000..1a00134a80 --- /dev/null +++ b/services/web/.dockerignore @@ -0,0 +1,17 @@ +.git + +.npmrc + +modules/*/Makefile +**/node_modules +copybara +data +public/js +public/minjs +public/stylesheets +public/manifest.json + +build.tar + +.sentryclirc +.sentryclirc.enc diff --git a/services/web/.eastrc b/services/web/.eastrc new file mode 100644 index 0000000000..6c22d9e35a --- /dev/null +++ b/services/web/.eastrc @@ -0,0 +1,4 @@ +{ + "adapter": "./migrations/lib/adapter", + "migrationNumberFormat": "dateTime" +} diff --git a/services/web/.eslintignore b/services/web/.eslintignore new file mode 100644 index 0000000000..4c6c6dc3da --- /dev/null +++ b/services/web/.eslintignore @@ -0,0 +1,7 @@ +# NOTE: changing paths may require updating them in the Makefile too. +node_modules +modules/**/scripts +frontend/js/vendor +modules/**/frontend/js/vendor +public/js +public/minjs diff --git a/services/web/.eslintrc b/services/web/.eslintrc new file mode 100644 index 0000000000..e577f1067b --- /dev/null +++ b/services/web/.eslintrc @@ -0,0 +1,166 @@ +{ + "root": true, + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:jsx-a11y/recommended", + "standard", + "standard-jsx", + "standard-react", + "prettier" + ], + "plugins": [ + "jsx-a11y", + "mocha", + "chai-expect", + "chai-friendly" + ], + "env": { + "browser": true, + "mocha": true, + "node": true, + "es2020": true + }, + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "settings": { + // Tell eslint-plugin-react to detect which version of React we are using + "react": { + "version": "detect" + } + }, + "rules": { + // Swap the no-unused-expressions rule with a more chai-friendly one + "no-unused-expressions": "off", + "chai-friendly/no-unused-expressions": "error", + + // Disable some rules after upgrading ESLint + // TODO: re-enable and fix + "no-var": "off", + + // do not allow importing of implicit dependencies. + "import/no-extraneous-dependencies": "error", + + "node/no-callback-literal": "off", + "node/no-deprecated-api": "off", + "node/handle-callback-err": "off", + "node/no-path-concat": "off" + }, + "overrides": [ + // NOTE: changing paths may require updating them in the Makefile too. + { + // Test specific rules + "files": ["**/test/*/src/**/*.js", "**/test/**/*.test.js"], + "globals": { + "expect": true + }, + "rules": { + // mocha-specific rules + "mocha/handle-done-callback": "error", + "mocha/no-exclusive-tests": "error", + "mocha/no-global-tests": "error", + "mocha/no-identical-title": "error", + "mocha/no-nested-tests": "error", + "mocha/no-pending-tests": "error", + "mocha/no-skipped-tests": "error", + "mocha/no-mocha-arrows": "error", + + // chai-specific rules + "chai-expect/missing-assertion": "error", + "chai-expect/terminating-properties": "error", + + // prefer-arrow-callback applies to all callbacks, not just ones in mocha tests. + // we don't enforce this at the top-level - just in tests to manage `this` scope + // based on mocha's context mechanism + "mocha/prefer-arrow-callback": "error" + } + }, + { + // Frontend test specific rules + "files": ["**/test/karma/**/*.js"], + "globals": { + "expect": true, + "$": true + } + }, + { + // Backend specific rules + "files": ["**/app/src/**/*.js"], + "rules": { + // don't allow console.log in backend code + "no-console": "error", + + // do not allow importing of implicit dependencies. + "import/no-extraneous-dependencies": ["error", { + // do not allow importing of devDependencies. + "devDependencies": false + }] + } + }, + { + // Frontend specific rules + "files": ["**/frontend/js/**/*.js", "**/frontend/stories/**/*.js", "**/*.stories.js", "**/test/frontend/**/*.js"], + "globals": { + "__webpack_public_path__": true, + "$": true, + "angular": true, + "ace": true, + "ga": true, + "sl_console": true, + "sl_debugging": true, + // Injected in layout.pug + "user_id": true, + "ExposedSettings": true + }, + "rules": { + // Prevent usage of legacy string refs + "react/no-string-refs": "error", + + // Prevent curly braces around strings (as they're unnecessary) + "react/jsx-curly-brace-presence": ["error", { + "props": "never", + "children": "never" + }], + + // Allow target="_blank" in JSX + "react/jsx-no-target-blank": "off", + + // Don't import React for JSX; the JSX runtime is added by a Babel plugin + "react/react-in-jsx-scope": "off", + "react/jsx-uses-react": "off", + + // Fix conflict between prettier & standard by overriding to prefer + // double quotes + "jsx-quotes": ["error", "prefer-double"], + + // Override weird behaviour of jsx-a11y label-has-for (says labels must be + // nested *and* have for/id attributes) + "jsx-a11y/label-has-for": [ + "error", + { + "required": { + "some": [ + "nesting", + "id" + ] + } + } + ] + } + }, + { + "files": ["scripts/ukamf/*.js"], + "rules": { + // Do not allow importing of any dependencies unless specified in either + // - web/package.json + // - web/scripts/ukamf/package.json + "import/no-extraneous-dependencies": ["error", {"packageDir": [".", "scripts/ukamf"]}] + } + } + ] +} diff --git a/services/web/.github/ISSUE_TEMPLATE.md b/services/web/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..e0093aa90c --- /dev/null +++ b/services/web/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,38 @@ + + +## Steps to Reproduce + + + +1. +2. +3. + +## Expected Behaviour + + +## Observed Behaviour + + + +## Context + + +## Technical Info + + +* URL: +* Browser Name and version: +* Operating System and version (desktop or mobile): +* Signed in as: +* Project and/or file: + +## Analysis + + +## Who Needs to Know? + + + +- +- diff --git a/services/web/.github/PULL_REQUEST_TEMPLATE.md b/services/web/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..8d03e742ad --- /dev/null +++ b/services/web/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,43 @@ +### Description + + + +#### Screenshots + + + +#### Related Issues / PRs + + + +### Review + + + +#### Potential Impact + + + +#### Manual Testing Performed + +- [ ] +- [ ] + +#### Accessibility + + + +### Deployment + + + +#### Deployment Checklist + +- [ ] Update documentation not included in the PR (if any) +- [ ] + +#### Metrics and Monitoring + + + +#### Who Needs to Know? diff --git a/services/web/.gitignore b/services/web/.gitignore new file mode 100644 index 0000000000..f7bd24a97c --- /dev/null +++ b/services/web/.gitignore @@ -0,0 +1,79 @@ +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Logs and databases # +###################### +*.log +*.sql +*.sqlite + +# OS generated files # +###################### +.DS_Store? +ehthumbs.db +Icon? +Thumbs.db + +node_modules/* +data/* +coverage + +cookies.txt +requestQueueWorker.js +TpdsWorker.js +BackgroundJobsWorker.js +UserAndProjectPopulator.coffee + +public/manifest.json + +public/js +public/minjs +public/stylesheets +public/fonts + +Gemfile.lock + +*.swp +.DS_Store + +docker-shared.yml + +config/*.coffee +!config/settings.defaults.coffee +!config/settings.webpack.coffee +config/*.js +!config/settings.defaults.js +!config/settings.webpack.js +!config/settings.overrides.saas.js +!config/settings.overrides.server-pro.js + +modules/**/Makefile + +# Sentry secrets file (injected by CI) +.sentryclirc + +# via dev-environment +.npmrc + +# Intellij +.idea +.run diff --git a/services/web/.nvmrc b/services/web/.nvmrc new file mode 100644 index 0000000000..5a80a7e912 --- /dev/null +++ b/services/web/.nvmrc @@ -0,0 +1 @@ +12.22.3 diff --git a/services/web/.prettierignore b/services/web/.prettierignore new file mode 100644 index 0000000000..04ecd4764c --- /dev/null +++ b/services/web/.prettierignore @@ -0,0 +1,8 @@ +# NOTE: changing paths may require updating them in the Makefile too. +node_modules +modules/**/scripts +frontend/js/vendor +modules/**/frontend/js/vendor +public/js +public/minjs +frontend/stylesheets/components/nvd3.less diff --git a/services/web/.prettierrc b/services/web/.prettierrc new file mode 100644 index 0000000000..13e31862ff --- /dev/null +++ b/services/web/.prettierrc @@ -0,0 +1,9 @@ +{ + "arrowParens": "avoid", + "jsxSingleQuote": false, + "semi": false, + "singleQuote": true, + "trailingComma": "es5", + "tabWidth": 2, + "useTabs": false +} diff --git a/services/web/.storybook/global.css b/services/web/.storybook/global.css new file mode 100644 index 0000000000..7a99a82e64 --- /dev/null +++ b/services/web/.storybook/global.css @@ -0,0 +1,3 @@ +.sidebar-container a[title='Overleaf'] { + max-width: 100px; +} diff --git a/services/web/.storybook/main.js b/services/web/.storybook/main.js new file mode 100644 index 0000000000..e8344f26e7 --- /dev/null +++ b/services/web/.storybook/main.js @@ -0,0 +1,50 @@ +const path = require('path') + +// NOTE: must be set before webpack config is imported +process.env.SHARELATEX_CONFIG = path.resolve( + __dirname, + '../config/settings.webpack.js' +) + +const customConfig = require('../webpack.config.dev') + +module.exports = { + stories: [ + '../frontend/stories/**/*.stories.js', + '../modules/**/stories/**/*.stories.js', + ], + addons: ['@storybook/addon-essentials', '@storybook/addon-a11y'], + webpackFinal: storybookConfig => { + // Combine Storybook's webpack loaders with our webpack loaders + const rules = [ + // Filter out the Storybook font file loader, which overrides our font + // file loader causing the font to fail to load + ...storybookConfig.module.rules.filter( + rule => !rule.test.toString().includes('woff') + ), + // Replace the less rule, adding to-string-loader + // Filter out the MiniCSS extraction, which conflicts with the built-in CSS loader + ...customConfig.module.rules.filter( + rule => + !rule.test.toString().includes('less') && + !rule.test.toString().includes('css') + ), + { + test: /\.less$/, + use: ['to-string-loader', 'css-loader', 'less-loader'], + }, + ] + + // Combine Storybook's webpack plugins with our webpack plugins + const plugins = [...storybookConfig.plugins, ...customConfig.plugins] + + return { + ...storybookConfig, + module: { + ...storybookConfig.module, + rules, + }, + plugins, + } + }, +} diff --git a/services/web/.storybook/manager.js b/services/web/.storybook/manager.js new file mode 100644 index 0000000000..33557edd7e --- /dev/null +++ b/services/web/.storybook/manager.js @@ -0,0 +1,15 @@ +import { addons } from '@storybook/addons' +import { create } from '@storybook/theming' + +import './global.css' + +import brandImage from '../public/img/ol-brand/overleaf.svg' + +const theme = create({ + base: 'light', + brandTitle: 'Overleaf', + brandUrl: 'https://www.overleaf.com', + brandImage, +}) + +addons.setConfig({ theme }) diff --git a/services/web/.storybook/preview.css b/services/web/.storybook/preview.css new file mode 100644 index 0000000000..0fb8660505 --- /dev/null +++ b/services/web/.storybook/preview.css @@ -0,0 +1,11 @@ +.sb-show-main.modal-open { + overflow-y: auto !important; +} + +.sb-show-main .modal-backdrop { + display: none; +} + +.sb-show-main .modal { + position: relative; +} diff --git a/services/web/.storybook/preview.js b/services/web/.storybook/preview.js new file mode 100644 index 0000000000..7a658984bc --- /dev/null +++ b/services/web/.storybook/preview.js @@ -0,0 +1,126 @@ +import './preview.css' + +// Storybook does not (currently) support async loading of "stories". Therefore +// the strategy in frontend/js/i18n.js does not work (because we cannot wait on +// the promise to resolve). +// Therefore we have to use the synchronous method for configuring +// react-i18next. Because this, we can only hard-code a single language. +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' +import en from '../locales/en.json' +i18n.use(initReactI18next).init({ + lng: 'en', + + resources: { + en: { translation: en }, + }, + + react: { + useSuspense: false, + }, + + interpolation: { + prefix: '__', + suffix: '__', + unescapeSuffix: 'HTML', + skipOnVariables: true, + defaultVariables: { + appName: 'Overleaf', + }, + }, +}) + +export const parameters = { + // Automatically mark prop-types like onClick, onToggle, etc as Storybook + // "actions", so that they are logged in the Actions pane at the bottom of the + // viewer + actions: { argTypesRegex: '^on.*' }, + docs: { + // render stories in iframes, to isolate modals + inlineStories: false, + }, +} + +export const globalTypes = { + theme: { + name: 'Theme', + description: 'Editor theme', + defaultValue: 'default-', + toolbar: { + icon: 'circlehollow', + items: [ + { value: 'default-', title: 'Default' }, + { value: 'light-', title: 'Light' }, + { value: 'ieee-', title: 'IEEE' }, + ], + }, + }, +} + +export const loaders = [ + async ({ globals }) => { + const { theme } = globals + + return { + // NOTE: this uses `${theme}style.less` rather than `${theme}.less` + // so that webpack only bundles files ending with "style.less" + activeStyle: await import( + `../frontend/stylesheets/${theme === 'default-' ? '' : theme}style.less` + ), + } + }, +] + +const withTheme = (Story, context) => { + const { activeStyle } = context.loaded + + return ( + <> + {activeStyle && } + + + ) +} + +export const decorators = [withTheme] + +window.ExposedSettings = { + maxEntitiesPerProject: 10, + maxUploadSize: 5 * 1024 * 1024, + enableSubscriptions: true, + textExtensions: [ + 'tex', + 'latex', + 'sty', + 'cls', + 'bst', + 'bib', + 'bibtex', + 'txt', + 'tikz', + 'mtx', + 'rtex', + 'md', + 'asy', + 'latexmkrc', + 'lbx', + 'bbx', + 'cbx', + 'm', + 'lco', + 'dtx', + 'ins', + 'ist', + 'def', + 'clo', + 'ldf', + 'rmd', + 'lua', + 'gv', + 'mf', + ], +} + +window.user = { + id: 'storybook', +} diff --git a/services/web/.vscode/settings.json b/services/web/.vscode/settings.json new file mode 100644 index 0000000000..96a1261905 --- /dev/null +++ b/services/web/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "files.exclude": { + "node_modules": true, + "data": true + } +} diff --git a/services/web/Dockerfile b/services/web/Dockerfile new file mode 100644 index 0000000000..082ef79957 --- /dev/null +++ b/services/web/Dockerfile @@ -0,0 +1,56 @@ +# the base image is suitable for running web with /app bind mounted +FROM node:12.22.3 as base + +WORKDIR /app + +# install_deps changes app files and installs npm packages +# as such it has to run at a later stage + +RUN apt-get update \ +&& apt-get install -y parallel \ +&& rm -rf /var/lib/apt/lists/* + +RUN mkdir /app/node_modules && chown node:node /app/node_modules + +# the deps image is used for caching npm ci +FROM base as deps + +COPY package.json package-lock.json /app/ + +RUN npm ci --quiet + + +# the dev is suitable for running tests +FROM deps as dev + +COPY . /app + +RUN mkdir -p /app/data/dumpFolder && \ + mkdir -p /app/data/logs && \ + mkdir -p /app/data/pdf && \ + mkdir -p /app/data/uploads && \ + mkdir -p /app/data/zippedProjects && \ + chmod -R 0755 /app/data/ && \ + chown -R node:node /app/data/ + +ARG SENTRY_RELEASE +ENV SENTRY_RELEASE=$SENTRY_RELEASE + +USER node + + +# the webpack image has deps+src+webpack artifacts +FROM dev as webpack + +USER root +RUN chmod 0755 ./install_deps.sh && ./install_deps.sh + + +# the final production image without webpack source maps +FROM webpack as app + +RUN find /app/public -name '*.js.map' -delete +RUN rm /app/modules/server-ce-scripts -rf +USER node + +CMD ["node", "--expose-gc", "app.js"] diff --git a/services/web/Dockerfile.frontend b/services/web/Dockerfile.frontend new file mode 100644 index 0000000000..80a2069b71 --- /dev/null +++ b/services/web/Dockerfile.frontend @@ -0,0 +1,6 @@ +FROM node:12.22.3 + +# Install Google Chrome +RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - +RUN sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' +RUN apt-get update && apt-get install -y google-chrome-stable diff --git a/services/web/Dockerfile.frontend.ci b/services/web/Dockerfile.frontend.ci new file mode 100644 index 0000000000..140d2bc788 --- /dev/null +++ b/services/web/Dockerfile.frontend.ci @@ -0,0 +1,11 @@ +ARG PROJECT_NAME +ARG BRANCH_NAME +ARG BUILD_NUMBER + +FROM ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + +USER root + +RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \ + echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list && \ + apt-get update && apt-get install -y google-chrome-stable diff --git a/services/web/LICENSE b/services/web/LICENSE new file mode 100644 index 0000000000..dba13ed2dd --- /dev/null +++ b/services/web/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/services/web/Makefile b/services/web/Makefile new file mode 100644 index 0000000000..d513742bed --- /dev/null +++ b/services/web/Makefile @@ -0,0 +1,510 @@ +DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml --log-level ERROR + +BUILD_NUMBER ?= local +BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) +PROJECT_NAME = web +BUILD_DIR_NAME = $(shell pwd | xargs basename | tr -cd '[a-zA-Z0-9_.\-]') + +export SHARELATEX_CONFIG ?= /app/test/acceptance/config/settings.test.saas.js +export BASE_CONFIG ?= ${SHARELATEX_CONFIG} + +CFG_SAAS=/app/test/acceptance/config/settings.test.saas.js +CFG_SERVER_CE=/app/test/acceptance/config/settings.test.server-ce.js +CFG_SERVER_PRO=/app/test/acceptance/config/settings.test.server-pro.js + +DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ + BRANCH_NAME=$(BRANCH_NAME) \ + PROJECT_NAME=$(PROJECT_NAME) \ + MOCHA_GREP=${MOCHA_GREP} \ + docker-compose ${DOCKER_COMPOSE_FLAGS} + +MODULE_DIRS := $(shell find modules -mindepth 1 -maxdepth 1 -type d -not -name '.git' ) +MODULE_MAKEFILES := $(MODULE_DIRS:=/Makefile) +MODULE_NAME=$(shell basename $(MODULE)) + +$(MODULE_MAKEFILES): Makefile.module + cp Makefile.module $@ || diff Makefile.module $@ + +# +# Clean +# + +clean_ci: + $(DOCKER_COMPOSE) down -v -t 0 + docker container list | grep 'days ago' | cut -d ' ' -f 1 - | xargs -r docker container stop + docker image prune -af --filter "until=48h" + docker network prune -f + +# +# Tests +# + +test: test_unit test_karma test_acceptance test_frontend + +test_module: test_unit_module test_acceptance_module + +# +# Unit tests +# + +test_unit: test_unit_all +test_unit_all: + COMPOSE_PROJECT_NAME=unit_test_all_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --rm test_unit npm run test:unit:all + COMPOSE_PROJECT_NAME=unit_test_all_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0 + +test_unit_all_silent: + COMPOSE_PROJECT_NAME=unit_test_all_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --rm test_unit npm run test:unit:all:silent + COMPOSE_PROJECT_NAME=unit_test_all_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0 + +test_unit_app: + COMPOSE_PROJECT_NAME=unit_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0 + COMPOSE_PROJECT_NAME=unit_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --name unit_test_$(BUILD_DIR_NAME) --rm test_unit + COMPOSE_PROJECT_NAME=unit_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0 + +TEST_SUITES = $(sort $(filter-out \ + $(wildcard test/unit/src/helpers/*), \ + $(wildcard test/unit/src/*/*))) + +MOCHA_CMD_LINE = \ + mocha \ + --exit \ + --file test/unit/bootstrap.js \ + --grep=${MOCHA_GREP} \ + --reporter spec \ + --timeout 25000 \ + +.PHONY: $(TEST_SUITES) +$(TEST_SUITES): + $(MOCHA_CMD_LINE) $@ + +J ?= 1 +test_unit_app_parallel_gnu_make: $(TEST_SUITES) +test_unit_app_parallel_gnu_make_docker: export COMPOSE_PROJECT_NAME = \ + unit_test_parallel_make_$(BUILD_DIR_NAME) +test_unit_app_parallel_gnu_make_docker: + $(DOCKER_COMPOSE) down -v -t 0 + $(DOCKER_COMPOSE) run --rm test_unit \ + make test_unit_app_parallel_gnu_make --output-sync -j $(J) + $(DOCKER_COMPOSE) down -v -t 0 + +test_unit_app_parallel: test_unit_app_parallel_gnu_parallel +test_unit_app_parallel_gnu_parallel: export COMPOSE_PROJECT_NAME = \ + unit_test_parallel_$(BUILD_DIR_NAME) +test_unit_app_parallel_gnu_parallel: + $(DOCKER_COMPOSE) down -v -t 0 + $(DOCKER_COMPOSE) run --rm test_unit npm run test:unit:app:parallel + $(DOCKER_COMPOSE) down -v -t 0 + +TEST_UNIT_MODULES = $(MODULE_DIRS:=/test_unit) +$(TEST_UNIT_MODULES): %/test_unit: %/Makefile +test_unit_modules: $(TEST_UNIT_MODULES) + +test_unit_module: + $(MAKE) modules/$(MODULE_NAME)/test_unit + +# +# Karma frontend tests +# + +test_karma: build_test_karma test_karma_run + +test_karma_run: + COMPOSE_PROJECT_NAME=karma_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0 + COMPOSE_PROJECT_NAME=karma_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --rm test_karma + COMPOSE_PROJECT_NAME=karma_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0 + +test_karma_build_run: build_test_karma test_karma_run + +# +# Frontend tests +# + +test_frontend: + COMPOSE_PROJECT_NAME=frontend_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0 + COMPOSE_PROJECT_NAME=frontend_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --rm test_frontend + COMPOSE_PROJECT_NAME=frontend_test_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0 + +# +# Acceptance tests +# + +test_acceptance: test_acceptance_app test_acceptance_modules +test_acceptance_saas: test_acceptance_app_saas test_acceptance_modules_merged_saas +test_acceptance_server_ce: test_acceptance_app_server_ce test_acceptance_modules_merged_server_ce +test_acceptance_server_pro: test_acceptance_app_server_pro test_acceptance_modules_merged_server_pro + +TEST_ACCEPTANCE_APP := \ + test_acceptance_app_saas \ + test_acceptance_app_server_ce \ + test_acceptance_app_server_pro \ + +test_acceptance_app: $(TEST_ACCEPTANCE_APP) +test_acceptance_app_saas: export COMPOSE_PROJECT_NAME=acceptance_test_saas_$(BUILD_DIR_NAME) +test_acceptance_app_saas: export SHARELATEX_CONFIG=$(CFG_SAAS) +test_acceptance_app_server_ce: export COMPOSE_PROJECT_NAME=acceptance_test_server_ce_$(BUILD_DIR_NAME) +test_acceptance_app_server_ce: export SHARELATEX_CONFIG=$(CFG_SERVER_CE) +test_acceptance_app_server_pro: export COMPOSE_PROJECT_NAME=acceptance_test_server_pro_$(BUILD_DIR_NAME) +test_acceptance_app_server_pro: export SHARELATEX_CONFIG=$(CFG_SERVER_PRO) + +$(TEST_ACCEPTANCE_APP): + $(DOCKER_COMPOSE) down -v -t 0 + $(DOCKER_COMPOSE) run --rm test_acceptance + $(DOCKER_COMPOSE) down -v -t 0 + +# We are using _make magic_ for turning these file-targets into calls to +# sub-Makefiles in the individual modules. +# These sub-Makefiles need to be kept in sync with the template, hence we +# add a dependency on each modules Makefile and cross-link that to the +# template at the very top of this file. +# Example: `web$ make modules/server-ce-scripts/test_acceptance_server_ce` +# Description: Run the acceptance tests of the server-ce-scripts module in a +# Server CE Environment. +# Break down: +# Target: modules/server-ce-scripts/test_acceptance_server_ce +# -> depends on modules/server-ce-scripts/Makefile +# -> add environment variable BASE_CONFIG=$(CFG_SERVER_CE) +# -> BASE_CONFIG=/app/test/acceptance/config/settings.test.server-ce.js +# -> automatic target: `make -C server-ce-scripts test_acceptance_server_ce` +# -> automatic target: run `make test_acceptance_server_ce` in module +# Target: modules/server-ce-scripts/Makefile +# -> depends on Makefile.module +# -> automatic target: copies the file when changed +TEST_ACCEPTANCE_MODULES = $(MODULE_DIRS:=/test_acceptance) +$(TEST_ACCEPTANCE_MODULES): %/test_acceptance: %/Makefile +$(TEST_ACCEPTANCE_MODULES): modules/%/test_acceptance: + $(MAKE) test_acceptance_module MODULE_NAME=$* + +TEST_ACCEPTANCE_MODULES_SAAS = $(MODULE_DIRS:=/test_acceptance_saas) +$(TEST_ACCEPTANCE_MODULES_SAAS): %/test_acceptance_saas: %/Makefile +$(TEST_ACCEPTANCE_MODULES_SAAS): export BASE_CONFIG = $(CFG_SAAS) + +# This line adds `/test_acceptance_saas` suffix to all items in $(MODULE_DIRS). +TEST_ACCEPTANCE_MODULES_SERVER_CE = $(MODULE_DIRS:=/test_acceptance_server_ce) +# This line adds a dependency on the modules Makefile. +$(TEST_ACCEPTANCE_MODULES_SERVER_CE): %/test_acceptance_server_ce: %/Makefile +# This line adds the environment variable BASE_CONFIG=$(CFG_SERVER_CE) to all +# invocations of `web$ make modules/foo/test_acceptance_server_ce`. +$(TEST_ACCEPTANCE_MODULES_SERVER_CE): export BASE_CONFIG = $(CFG_SERVER_CE) + +TEST_ACCEPTANCE_MODULES_SERVER_PRO = $(MODULE_DIRS:=/test_acceptance_server_pro) +$(TEST_ACCEPTANCE_MODULES_SERVER_PRO): %/test_acceptance_server_pro: %/Makefile +$(TEST_ACCEPTANCE_MODULES_SERVER_PRO): export BASE_CONFIG = $(CFG_SERVER_PRO) + +CLEAN_TEST_ACCEPTANCE_MODULES = $(MODULE_DIRS:=/clean_test_acceptance) +$(CLEAN_TEST_ACCEPTANCE_MODULES): %/clean_test_acceptance: %/Makefile +clean_test_acceptance_modules: $(CLEAN_TEST_ACCEPTANCE_MODULES) +clean_ci: clean_test_acceptance_modules + +test_acceptance_module_noop: + @echo + @echo Module '$(MODULE_NAME)' does not run in ${LABEL}. + @echo + +TEST_ACCEPTANCE_MODULE_MAYBE_IN := \ + test_acceptance_module_maybe_in_saas \ + test_acceptance_module_maybe_in_server_ce \ + test_acceptance_module_maybe_in_server_pro \ + +test_acceptance_module: $(TEST_ACCEPTANCE_MODULE_MAYBE_IN) +test_acceptance_module_maybe_in_saas: export BASE_CONFIG=$(CFG_SAAS) +test_acceptance_module_maybe_in_server_ce: export BASE_CONFIG=$(CFG_SERVER_CE) +test_acceptance_module_maybe_in_server_pro: export BASE_CONFIG=$(CFG_SERVER_PRO) + +# We need to figure out whether the module is loaded in a given environment. +# This information is stored in the (base-)settings. +# We get the full list of modules and check for a matching module entry. +# Either the grep will find and emit the module, or exits with code 1, which +# we handle with a fallback to a noop make target. +# Run the node command in a docker-compose container which provides the needed +# npm dependencies (from disk in dev-env or from the CI image in CI). +# Pick the test_unit service which is very light-weight -- the test_acceptance +# service would start mongo/redis. +$(TEST_ACCEPTANCE_MODULE_MAYBE_IN): test_acceptance_module_maybe_in_%: + $(MAKE) $(shell \ + SHARELATEX_CONFIG=$(BASE_CONFIG) \ + $(DOCKER_COMPOSE) run --rm test_unit \ + node test/acceptance/getModuleTargets test_acceptance_$* \ + | grep -e /$(MODULE_NAME)/ || echo test_acceptance_module_noop LABEL=$* \ + ) + +# See docs for test_acceptance_server_ce how this works. +test_acceptance_module_saas: export BASE_CONFIG = $(CFG_SAAS) +test_acceptance_module_saas: + $(MAKE) modules/$(MODULE_NAME)/test_acceptance_saas + +test_acceptance_module_server_ce: export BASE_CONFIG = $(CFG_SERVER_CE) +test_acceptance_module_server_ce: + $(MAKE) modules/$(MODULE_NAME)/test_acceptance_server_ce + +test_acceptance_module_server_pro: export BASE_CONFIG = $(CFG_SERVER_PRO) +test_acceptance_module_server_pro: + $(MAKE) modules/$(MODULE_NAME)/test_acceptance_server_pro + +# See docs for test_acceptance_server_ce how this works. +TEST_ACCEPTANCE_MODULES_MERGED_INNER = $(MODULE_DIRS:=/test_acceptance_merged_inner) +$(TEST_ACCEPTANCE_MODULES_MERGED_INNER): %/test_acceptance_merged_inner: %/Makefile +test_acceptance_modules_merged_inner: + $(MAKE) $(shell \ + SHARELATEX_CONFIG=$(BASE_CONFIG) \ + node test/acceptance/getModuleTargets test_acceptance_merged_inner \ + ) + +# inner loop for running saas tests in parallel +no_more_targets: + +# If we ever have more than 40 modules, we need to add _5 targets to all the places and have it START at 41. +test_acceptance_modules_merged_inner_1: export START=1 +test_acceptance_modules_merged_inner_2: export START=11 +test_acceptance_modules_merged_inner_3: export START=21 +test_acceptance_modules_merged_inner_4: export START=31 +TEST_ACCEPTANCE_MODULES_MERGED_INNER_SPLIT = \ + test_acceptance_modules_merged_inner_1 \ + test_acceptance_modules_merged_inner_2 \ + test_acceptance_modules_merged_inner_3 \ + test_acceptance_modules_merged_inner_4 \ + +# The node script prints one module per line. +# Using tail and head we skip over the first n=START entries and print the last 10. +# Finally we check with grep for any targets in a batch and print a fallback if none were found. +$(TEST_ACCEPTANCE_MODULES_MERGED_INNER_SPLIT): + $(MAKE) $(shell \ + SHARELATEX_CONFIG=$(BASE_CONFIG) \ + node test/acceptance/getModuleTargets test_acceptance_merged_inner \ + | tail -n+$(START) | head -n 10 \ + | grep -e . || echo no_more_targets \ + ) + +# See docs for test_acceptance_server_ce how this works. +test_acceptance_modules_merged_saas: export COMPOSE_PROJECT_NAME = \ + acceptance_test_modules_merged_saas_$(BUILD_DIR_NAME) +test_acceptance_modules_merged_saas: export BASE_CONFIG = $(CFG_SAAS) + +test_acceptance_modules_merged_server_ce: export COMPOSE_PROJECT_NAME = \ + acceptance_test_modules_merged_server_ce_$(BUILD_DIR_NAME) +test_acceptance_modules_merged_server_ce: export BASE_CONFIG = $(CFG_SERVER_CE) + +test_acceptance_modules_merged_server_pro: export COMPOSE_PROJECT_NAME = \ + acceptance_test_modules_merged_server_pro_$(BUILD_DIR_NAME) +test_acceptance_modules_merged_server_pro: export BASE_CONFIG = $(CFG_SERVER_PRO) + +# All these variants run the same command. +# Each target has a different set of environment defined above. +TEST_ACCEPTANCE_MODULES_MERGED_VARIANTS = \ + test_acceptance_modules_merged_saas \ + test_acceptance_modules_merged_server_ce \ + test_acceptance_modules_merged_server_pro \ + +$(TEST_ACCEPTANCE_MODULES_MERGED_VARIANTS): + $(DOCKER_COMPOSE) down -v -t 0 + $(DOCKER_COMPOSE) run --rm test_acceptance make test_acceptance_modules_merged_inner + $(DOCKER_COMPOSE) down -v -t 0 + +# outer loop for running saas tests in parallel +TEST_ACCEPTANCE_MODULES_MERGED_SPLIT_SAAS = \ + test_acceptance_modules_merged_saas_1 \ + test_acceptance_modules_merged_saas_2 \ + test_acceptance_modules_merged_saas_3 \ + test_acceptance_modules_merged_saas_4 \ + +test_acceptance_modules_merged_saas_1: export COMPOSE_PROJECT_NAME = \ + acceptance_test_modules_merged_saas_1_$(BUILD_DIR_NAME) +test_acceptance_modules_merged_saas_2: export COMPOSE_PROJECT_NAME = \ + acceptance_test_modules_merged_saas_2_$(BUILD_DIR_NAME) +test_acceptance_modules_merged_saas_3: export COMPOSE_PROJECT_NAME = \ + acceptance_test_modules_merged_saas_3_$(BUILD_DIR_NAME) +test_acceptance_modules_merged_saas_4: export COMPOSE_PROJECT_NAME = \ + acceptance_test_modules_merged_saas_4_$(BUILD_DIR_NAME) +$(TEST_ACCEPTANCE_MODULES_MERGED_SPLIT_SAAS): export BASE_CONFIG = $(CFG_SAAS) + +$(TEST_ACCEPTANCE_MODULES_MERGED_SPLIT_SAAS): test_acceptance_modules_merged_saas_%: + $(DOCKER_COMPOSE) down -v -t 0 + $(DOCKER_COMPOSE) run --rm test_acceptance make test_acceptance_modules_merged_inner_$* + $(DOCKER_COMPOSE) down -v -t 0 + +test_acceptance_modules: $(TEST_ACCEPTANCE_MODULES_MERGED_VARIANTS) + +# +# CI tests +# + +ci: + MOCHA_ARGS="--reporter tap" \ + $(MAKE) test + +# +# Lint & format +# +ORG_PATH = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +RUN_LINT_FORMAT ?= \ + docker run --rm ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + +NODE_MODULES_PATH := ${PATH}:${PWD}/node_modules/.bin:/app/node_modules/.bin +WITH_NODE_MODULES_PATH = \ + format_backend \ + format_frontend \ + format_misc \ + format_styles \ + format_test_app_unit \ + format_test_app_rest \ + format_test_modules \ + $(TEST_SUITES) \ + +$(WITH_NODE_MODULES_PATH): export PATH=$(NODE_MODULES_PATH) + +lint: lint_backend +lint_backend: + npx eslint \ + app.js \ + 'app/**/*.js' \ + 'modules/*/index.js' \ + 'modules/*/app/**/*.js' \ + --max-warnings=0 + +lint: lint_frontend +lint_frontend: + npx eslint \ + 'frontend/**/*.js' \ + 'modules/*/frontend/**/*.js' \ + --max-warnings=0 + +lint: lint_test +lint_test: lint_test_app +lint_test_app: lint_test_app_unit +lint_test_app_unit: + npx eslint \ + 'test/unit/**/*.js' \ + --max-warnings=0 + +lint_test_app: lint_test_app_rest +lint_test_app_rest: + npx eslint \ + 'test/**/*.js' \ + --ignore-pattern 'test/unit/**/*.js' \ + --max-warnings=0 + +lint_test: lint_test_modules +lint_test_modules: + npx eslint \ + 'modules/*/test/**/*.js' \ + --max-warnings=0 + +lint: lint_misc +# migrations, scripts, webpack config, karma config +lint_misc: + npx eslint . \ + --ignore-pattern app.js \ + --ignore-pattern 'app/**/*.js' \ + --ignore-pattern 'modules/*/app/**/*.js' \ + --ignore-pattern 'modules/*/index.js' \ + --ignore-pattern 'frontend/**/*.js' \ + --ignore-pattern 'modules/*/frontend/**/*.js' \ + --ignore-pattern 'test/**/*.js' \ + --ignore-pattern 'modules/*/test/**/*.js' \ + --max-warnings=0 + +lint: lint_pug +lint_pug: + bin/lint_pug_templates + +lint_in_docker: + $(RUN_LINT_FORMAT) make lint -j --output-sync + +format: format_js +format_js: + npm run --silent format + +format: format_styles +format_styles: + npm run --silent format:styles + +format_fix: + npm run --silent format:fix + +format_styles_fix: + npm run --silent format:styles:fix + +format_in_docker: + $(RUN_LINT_FORMAT) make format -j --output-sync + +# +# Build & publish +# + +IMAGE_CI ?= ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) +IMAGE_REPO ?= gcr.io/overleaf-ops/$(PROJECT_NAME) +IMAGE_REPO_BRANCH ?= $(IMAGE_REPO):$(BRANCH_NAME) +IMAGE_REPO_MAIN ?= $(IMAGE_REPO):main +IMAGE_REPO_MASTER ?= $(IMAGE_REPO):master +IMAGE_REPO_FINAL ?= $(IMAGE_REPO_BRANCH)-$(BUILD_NUMBER) + +export SENTRY_RELEASE ?= ${COMMIT_SHA} + +build_deps: + docker build --pull \ + --cache-from $(IMAGE_REPO_BRANCH)-deps \ + --cache-from $(IMAGE_REPO_MAIN)-deps \ + --cache-from $(IMAGE_REPO_MASTER)-deps \ + --tag $(IMAGE_REPO_BRANCH)-deps \ + --target deps \ + . + +build_dev: + docker build \ + --build-arg SENTRY_RELEASE \ + --cache-from $(IMAGE_REPO_BRANCH)-deps \ + --cache-from $(IMAGE_CI)-dev \ + --tag $(IMAGE_CI) \ + --tag $(IMAGE_CI)-dev \ + --target dev \ + . + +build_webpack: + $(MAKE) build_webpack_once \ + || $(MAKE) build_webpack_once + +build_webpack_once: + docker build \ + --build-arg SENTRY_RELEASE \ + --cache-from $(IMAGE_CI)-dev \ + --cache-from $(IMAGE_CI)-webpack \ + --tag $(IMAGE_CI)-webpack \ + --target webpack \ + . + +build: + docker build \ + --build-arg SENTRY_RELEASE \ + --cache-from $(IMAGE_CI)-webpack \ + --cache-from $(IMAGE_REPO_FINAL) \ + --tag $(IMAGE_REPO_FINAL) \ + . + +build_test_karma: + COMPOSE_PROJECT_NAME=karma_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) build test_karma + +publish: + docker push $(DOCKER_REPO)/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + +tar: + COMPOSE_PROJECT_NAME=tar_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) run --rm tar + COMPOSE_PROJECT_NAME=tar_$(BUILD_DIR_NAME) $(DOCKER_COMPOSE) down -v -t 0 + +MODULE_TARGETS = \ + $(TEST_ACCEPTANCE_MODULES_SAAS) \ + $(TEST_ACCEPTANCE_MODULES_SERVER_CE) \ + $(TEST_ACCEPTANCE_MODULES_SERVER_PRO) \ + $(TEST_ACCEPTANCE_MODULES_MERGED_INNER) \ + $(CLEAN_TEST_ACCEPTANCE_MODULES) \ + $(TEST_UNIT_MODULES) \ + +$(MODULE_TARGETS): + $(MAKE) -C $(dir $@) $(notdir $@) BUILD_DIR_NAME=$(BUILD_DIR_NAME) + +.PHONY: + $(MODULE_TARGETS) \ + compile_modules compile_modules_full clean_ci \ + test test_module test_unit test_unit_app \ + test_unit_modules test_unit_module test_karma test_karma_run \ + test_karma_build_run test_frontend test_acceptance test_acceptance_app \ + test_acceptance_modules test_acceptance_module ci format format_fix lint \ + build build_test_karma publish tar diff --git a/services/web/Makefile.module b/services/web/Makefile.module new file mode 100644 index 0000000000..d4951cf82e --- /dev/null +++ b/services/web/Makefile.module @@ -0,0 +1,71 @@ +BUILD_DIR_NAME ?= web +MODULE_NAME := $(notdir $(shell pwd)) +MODULE_DIR := modules/$(MODULE_NAME) +PROJECT_NAME = web + +export SHARELATEX_CONFIG = /app/$(MODULE_DIR)/test/acceptance/config/settings.test.js +export BASE_CONFIG ?= /app/test/acceptance/config/settings.test.saas.js + +CFG_SAAS=/app/test/acceptance/config/settings.test.saas.js +CFG_SERVER_CE=/app/test/acceptance/config/settings.test.server-ce.js +CFG_SERVER_PRO=/app/test/acceptance/config/settings.test.server-pro.js + +DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml --log-level ERROR +DOCKER_COMPOSE := cd ../../ && \ + MODULE_DIR=$(MODULE_DIR) \ + BUILD_NUMBER=$(BUILD_NUMBER) \ + BRANCH_NAME=$(BRANCH_NAME) \ + PROJECT_NAME=$(PROJECT_NAME) \ + MOCHA_GREP=${MOCHA_GREP} \ + docker-compose ${DOCKER_COMPOSE_FLAGS} + +DOCKER_COMPOSE_TEST_ACCEPTANCE := \ + export COMPOSE_PROJECT_NAME=acceptance_test_$(BUILD_DIR_NAME)_$(MODULE_NAME) \ + && $(DOCKER_COMPOSE) + +DOCKER_COMPOSE_TEST_UNIT := \ + export COMPOSE_PROJECT_NAME=unit_test_$(BUILD_DIR_NAME)_$(MODULE_NAME) \ + && $(DOCKER_COMPOSE) + +ifeq (,$(wildcard test/unit)) +test_unit: + +else +test_unit: + ${DOCKER_COMPOSE_TEST_UNIT} run --rm test_unit npm -q run test:unit:run_dir -- ${MOCHA_ARGS} $(MODULE_DIR)/test/unit/src + ${DOCKER_COMPOSE_TEST_UNIT} down + +endif + +ALL_TEST_ACCEPTANCE_VARIANTS := \ + test_acceptance \ + test_acceptance_saas \ + test_acceptance_server_ce \ + test_acceptance_server_pro \ + +ifeq (,$(wildcard test/acceptance)) +$(ALL_TEST_ACCEPTANCE_VARIANTS) test_acceptance_merged_inner: + @echo + @echo Module $(MODULE_NAME) does not have acceptance tests. + @echo + +clean_test_acceptance: + +else +test_acceptance_saas: export BASE_CONFIG = $(CFG_SAAS) +test_acceptance_server_ce: export BASE_CONFIG = $(CFG_SERVER_CE) +test_acceptance_server_pro: export BASE_CONFIG = $(CFG_SERVER_PRO) + +$(ALL_TEST_ACCEPTANCE_VARIANTS): + $(MAKE) --no-print-directory clean_test_acceptance + ${DOCKER_COMPOSE_TEST_ACCEPTANCE} run --rm test_acceptance npm -q run test:acceptance:run_dir -- ${MOCHA_ARGS} $(MODULE_DIR)/test/acceptance/src + $(MAKE) --no-print-directory clean_test_acceptance + +test_acceptance_merged_inner: + cd ../../ && \ + npm -q run test:acceptance:run_dir -- ${MOCHA_ARGS} $(MODULE_DIR)/test/acceptance/src + +clean_test_acceptance: + ${DOCKER_COMPOSE_TEST_ACCEPTANCE} down -v -t 0 + +endif diff --git a/services/web/README.md b/services/web/README.md new file mode 100644 index 0000000000..94790bb8f0 --- /dev/null +++ b/services/web/README.md @@ -0,0 +1,145 @@ +overleaf/web +============== + +overleaf/web is the front-end web service of the open-source web-based collaborative LaTeX editor, +[Overleaf](https://www.overleaf.com). +It serves all the HTML pages, CSS and javascript to the client. overleaf/web also contains +a lot of logic around creating and editing projects, and account management. + + +The rest of the Overleaf stack, along with information about contributing can be found in the +[overleaf/overleaf](https://github.com/overleaf/overleaf) repository. + +Build process +---------------- + +overleaf/web uses [Grunt](http://gruntjs.com/) to build its front-end related assets. + +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.* + +### Running Tests + +To run all tests run: +``` +make test +``` + +To run both unit and acceptance tests for a module run: +``` +make test_module MODULE=overleaf-integration +``` + +### 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 test_unit +``` + +During development it is often useful to only run a subset of tests, which can be configured with arguments to the mocha CLI: + +``` +make test_unit MOCHA_GREP='AuthorizationManager' +``` + +To run only the unit tests for a single module do: +``` +make test_unit_module MODULE=overleaf-integration +``` + +Module tests can also use a MOCHA_GREP argument: +``` +make test_unit_module MODULE=overleaf-integration MOCHA_GREP=SSO +``` + +### 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 test_acceptance +``` + +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 test_acceptance_app_start_service +make test_acceptance_app_run # Run as many times as needed during development +make test_acceptance_app_stop_service +``` + +`make test_acceptance` just runs these three commands in sequence and then runs `make test_acceptance_modules` which performs the tests for each module in the `modules` directory. (Note that there is not currently an equivalent to the `-start` / `-run` x _n_ / `-stop` series for modules.) + +During development it is often useful to only run a subset of tests, which can be configured with arguments to the mocha CLI: + +``` +make test_acceptance_run MOCHA_GREP='AuthorizationManager' +``` + +To run only the acceptance tests for a single module do: +``` +make test_acceptance_module MODULE=overleaf-integration +``` + +Module tests can also use a MOCHA_GREP argument: +``` +make test_acceptance_module MODULE=overleaf-integration MOCHA_GREP=SSO +``` + +Routes +------ + +Run `bin/routes` to print out all routes in the project. + + +License and Credits +------------------- + +This project is licensed under the [AGPLv3 license](http://www.gnu.org/licenses/agpl-3.0.html) + +### Stylesheets + +Overleaf is based on [Bootstrap](http://getbootstrap.com/), which is licensed under the +[MIT license](http://opensource.org/licenses/MIT). +All modifications (`*.less` files in `public/stylesheets`) are also licensed +under the MIT license. + +### Artwork + +#### Silk icon set 1.3 + +We gratefully acknowledge [Mark James](http://www.famfamfam.com/lab/icons/silk/) for +releasing his Silk icon set under the Creative Commons Attribution 2.5 license. Some +of these icons are used within Overleaf inside the `public/img/silk` and +`public/brand/icons` directories. + +#### IconShock icons + +We gratefully acknowledge [IconShock](http://www.iconshock.com) for use of the icons +in the `public/img/iconshock` directory found via +[findicons.com](http://findicons.com/icon/498089/height?id=526085#) + diff --git a/services/web/app.js b/services/web/app.js new file mode 100644 index 0000000000..b2d4e59908 --- /dev/null +++ b/services/web/app.js @@ -0,0 +1,76 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const metrics = require('@overleaf/metrics') +metrics.initialize(process.env.METRICS_APP_NAME || 'web') +const Settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const PlansLocator = require('./app/src/Features/Subscription/PlansLocator') +logger.initialize(process.env.METRICS_APP_NAME || 'web') +logger.logger.serializers.user = require('./app/src/infrastructure/LoggerSerializers').user +logger.logger.serializers.docs = require('./app/src/infrastructure/LoggerSerializers').docs +logger.logger.serializers.files = require('./app/src/infrastructure/LoggerSerializers').files +logger.logger.serializers.project = require('./app/src/infrastructure/LoggerSerializers').project +if ((Settings.sentry != null ? Settings.sentry.dsn : undefined) != null) { + logger.initializeErrorReporting(Settings.sentry.dsn) +} + +const http = require('http') +const https = require('https') +http.globalAgent.maxSockets = Settings.limits.httpGlobalAgentMaxSockets +https.globalAgent.maxSockets = Settings.limits.httpsGlobalAgentMaxSockets + +metrics.memory.monitor(logger) + +const Server = require('./app/src/infrastructure/Server') +const mongodb = require('./app/src/infrastructure/mongodb') +const mongoose = require('./app/src/infrastructure/Mongoose') + +if (Settings.catchErrors) { + process.removeAllListeners('uncaughtException') + process.on('uncaughtException', error => + logger.error({ err: error }, 'uncaughtException') + ) +} +const port = Settings.port || Settings.internal.web.port || 3000 +const host = Settings.internal.web.host || 'localhost' +if (!module.parent) { + // Called directly + + // We want to make sure that we provided a password through the environment. + if (!process.env.WEB_API_USER || !process.env.WEB_API_PASSWORD) { + throw new Error('No API user and password provided') + } + + PlansLocator.ensurePlansAreSetupCorrectly() + + Promise.all([mongodb.waitForDb(), mongoose.connectionPromise]) + .then(() => { + Server.server.listen(port, host, function () { + logger.info(`web starting up, listening on ${host}:${port}`) + logger.info(`${require('http').globalAgent.maxSockets} sockets enabled`) + // wait until the process is ready before monitoring the event loop + metrics.event_loop.monitor(logger) + }) + }) + .catch(err => { + logger.fatal({ err }, 'Cannot connect to mongo. Exiting.') + process.exit(1) + }) +} + +// handle SIGTERM for graceful shutdown in kubernetes +process.on('SIGTERM', function (signal) { + logger.warn({ signal: signal }, 'received signal, shutting down') + Settings.shuttingDown = true +}) + +module.exports = Server.server diff --git a/services/web/app/src/Features/Analytics/AnalyticsController.js b/services/web/app/src/Features/Analytics/AnalyticsController.js new file mode 100644 index 0000000000..30329616f0 --- /dev/null +++ b/services/web/app/src/Features/Analytics/AnalyticsController.js @@ -0,0 +1,38 @@ +const metrics = require('@overleaf/metrics') +const AnalyticsManager = require('./AnalyticsManager') +const SessionManager = require('../Authentication/SessionManager') +const GeoIpLookup = require('../../infrastructure/GeoIpLookup') +const Features = require('../../infrastructure/Features') + +module.exports = { + updateEditingSession(req, res, next) { + if (!Features.hasFeature('analytics')) { + return res.sendStatus(202) + } + const userId = SessionManager.getLoggedInUserId(req.session) + const { projectId } = req.params + let countryCode = null + + if (userId) { + GeoIpLookup.getDetails(req.ip, function (err, geoDetails) { + if (err) { + metrics.inc('analytics_geo_ip_lookup_errors') + } else if (geoDetails && geoDetails.country_code) { + countryCode = geoDetails.country_code + } + AnalyticsManager.updateEditingSession(userId, projectId, countryCode) + }) + } + res.sendStatus(202) + }, + + recordEvent(req, res, next) { + if (!Features.hasFeature('analytics')) { + return res.sendStatus(202) + } + const userId = + SessionManager.getLoggedInUserId(req.session) || req.sessionID + AnalyticsManager.recordEvent(userId, req.params.event, req.body) + res.sendStatus(202) + }, +} diff --git a/services/web/app/src/Features/Analytics/AnalyticsManager.js b/services/web/app/src/Features/Analytics/AnalyticsManager.js new file mode 100644 index 0000000000..278ea4691c --- /dev/null +++ b/services/web/app/src/Features/Analytics/AnalyticsManager.js @@ -0,0 +1,120 @@ +const Settings = require('@overleaf/settings') +const Metrics = require('../../infrastructure/Metrics') +const Queues = require('../../infrastructure/Queues') + +const analyticsEventsQueue = Queues.getAnalyticsEventsQueue() +const analyticsEditingSessionsQueue = Queues.getAnalyticsEditingSessionsQueue() +const analyticsUserPropertiesQueue = Queues.getAnalyticsUserPropertiesQueue() + +function identifyUser(userId, oldUserId) { + if (!userId || !oldUserId) { + return + } + if (isAnalyticsDisabled() || isSmokeTestUser(userId)) { + return + } + Metrics.analyticsQueue.inc({ status: 'adding', event_type: 'identify' }) + analyticsEventsQueue + .add('identify', { userId, oldUserId }) + .then(() => { + Metrics.analyticsQueue.inc({ status: 'added', event_type: 'identify' }) + }) + .catch(() => { + Metrics.analyticsQueue.inc({ status: 'error', event_type: 'identify' }) + }) +} + +function recordEvent(userId, event, segmentation) { + if (!userId) { + return + } + if (isAnalyticsDisabled() || isSmokeTestUser(userId)) { + return + } + Metrics.analyticsQueue.inc({ status: 'adding', event_type: 'event' }) + analyticsEventsQueue + .add('event', { userId, event, segmentation }) + .then(() => { + Metrics.analyticsQueue.inc({ status: 'added', event_type: 'event' }) + }) + .catch(() => { + Metrics.analyticsQueue.inc({ status: 'error', event_type: 'event' }) + }) +} + +function updateEditingSession(userId, projectId, countryCode) { + if (!userId) { + return + } + if (isAnalyticsDisabled() || isSmokeTestUser(userId)) { + return + } + Metrics.analyticsQueue.inc({ + status: 'adding', + event_type: 'editing-session', + }) + analyticsEditingSessionsQueue + .add({ userId, projectId, countryCode }) + .then(() => { + Metrics.analyticsQueue.inc({ + status: 'added', + event_type: 'editing-session', + }) + }) + .catch(() => { + Metrics.analyticsQueue.inc({ + status: 'error', + event_type: 'editing-session', + }) + }) +} + +function setUserProperty(userId, propertyName, propertyValue) { + if (!userId) { + return + } + if (isAnalyticsDisabled() || isSmokeTestUser(userId)) { + return + } + + if (propertyValue === undefined) { + throw new Error( + 'propertyValue cannot be undefined, use null to unset a property' + ) + } + + Metrics.analyticsQueue.inc({ + status: 'adding', + event_type: 'user-property', + }) + analyticsUserPropertiesQueue + .add({ userId, propertyName, propertyValue }) + .then(() => { + Metrics.analyticsQueue.inc({ + status: 'added', + event_type: 'user-property', + }) + }) + .catch(() => { + Metrics.analyticsQueue.inc({ + status: 'error', + event_type: 'user-property', + }) + }) +} + +function isSmokeTestUser(userId) { + const smokeTestUserId = Settings.smokeTest && Settings.smokeTest.userId + return smokeTestUserId != null && userId.toString() === smokeTestUserId +} + +function isAnalyticsDisabled() { + return !(Settings.analytics && Settings.analytics.enabled) +} + +module.exports = { + identifyUser, + recordEvent, + updateEditingSession, + setUserProperty, +} diff --git a/services/web/app/src/Features/Analytics/AnalyticsProxy.js b/services/web/app/src/Features/Analytics/AnalyticsProxy.js new file mode 100644 index 0000000000..b1c19008a4 --- /dev/null +++ b/services/web/app/src/Features/Analytics/AnalyticsProxy.js @@ -0,0 +1,28 @@ +const settings = require('@overleaf/settings') +const Errors = require('../Errors/Errors') +const httpProxy = require('express-http-proxy') +const URL = require('url') + +module.exports = { + call(basePath) { + if (!settings.apis.analytics) { + return (req, res, next) => + next( + new Errors.ServiceNotConfiguredError( + 'Analytics service not configured' + ) + ) + } + + return httpProxy(settings.apis.analytics.url, { + proxyReqPathResolver(req) { + const requestPath = URL.parse(req.url).path + return `${basePath}${requestPath}` + }, + proxyReqOptDecorator(proxyReqOpts, srcReq) { + proxyReqOpts.headers = {} // unset all headers + return proxyReqOpts + }, + }) + }, +} diff --git a/services/web/app/src/Features/Analytics/AnalyticsRegistrationSourceHelper.js b/services/web/app/src/Features/Analytics/AnalyticsRegistrationSourceHelper.js new file mode 100644 index 0000000000..c29f91caad --- /dev/null +++ b/services/web/app/src/Features/Analytics/AnalyticsRegistrationSourceHelper.js @@ -0,0 +1,125 @@ +var RefererParser = require('referer-parser') +const { URL } = require('url') +const AnalyticsManager = require('./AnalyticsManager') + +function clearSource(session) { + if (session) { + delete session.required_login_for + } +} + +const UTM_KEYS = [ + 'utm_campaign', + 'utm_source', + 'utm_term', + 'utm_medium', + 'utm_count', +] + +function parseUtm(query) { + var utmValues = {} + for (const utmKey of UTM_KEYS) { + if (query[utmKey]) { + utmValues[utmKey] = query[utmKey] + } + } + return Object.keys(utmValues).length > 0 ? utmValues : null +} + +function parseReferrer(referrer, url) { + if (!referrer) { + return { + medium: 'direct', + detail: 'none', + } + } + + const parsedReferrer = new RefererParser(referrer, url) + + const referrerValues = { + medium: parsedReferrer.medium, + detail: parsedReferrer.referer, + } + + if (referrerValues.medium === 'unknown') { + try { + const referrerHostname = new URL(referrer).hostname + if (referrerHostname) { + referrerValues.medium = 'link' + referrerValues.detail = referrerHostname + } + } catch (error) { + // ignore referrer parsing errors + } + } + + return referrerValues +} + +function setInbound(session, url, query, referrer) { + const inboundSession = { + referrer: parseReferrer(referrer, url), + utm: parseUtm(query), + } + + if (inboundSession.referrer || inboundSession.utm) { + session.inbound = inboundSession + } +} + +function clearInbound(session) { + if (session) { + delete session.inbound + } +} + +function addUserProperties(userId, session) { + if (!session) { + return + } + + if (session.referal_id) { + AnalyticsManager.setUserProperty( + userId, + `registered-from-bonus-scheme`, + true + ) + } + + if (session.required_login_for) { + AnalyticsManager.setUserProperty( + userId, + `registered-from-${session.required_login_for}`, + true + ) + } + + if (session.inbound) { + if (session.inbound.referrer) { + AnalyticsManager.setUserProperty( + userId, + `registered-from-referrer-${session.inbound.referrer.medium}`, + session.inbound.referrer.detail || 'other' + ) + } + + if (session.inbound.utm) { + for (const utmKey of UTM_KEYS) { + if (session.inbound.utm[utmKey]) { + AnalyticsManager.setUserProperty( + userId, + `registered-from-${utmKey.replace('_', '-')}`, + session.inbound.utm[utmKey] + ) + } + } + } + } +} + +module.exports = { + clearSource, + setInbound, + clearInbound, + addUserProperties, +} diff --git a/services/web/app/src/Features/Analytics/AnalyticsRegistrationSourceMiddleware.js b/services/web/app/src/Features/Analytics/AnalyticsRegistrationSourceMiddleware.js new file mode 100644 index 0000000000..19b7c96619 --- /dev/null +++ b/services/web/app/src/Features/Analytics/AnalyticsRegistrationSourceMiddleware.js @@ -0,0 +1,55 @@ +const logger = require('logger-sharelatex') +const OError = require('@overleaf/o-error') +const AnalyticsRegistrationSourceHelper = require('./AnalyticsRegistrationSourceHelper') +const SessionManager = require('../../Features/Authentication/SessionManager') + +function setSource(source) { + return function (req, res, next) { + if (req.session) { + req.session.required_login_for = source + } + next() + } +} + +function clearSource() { + return function (req, res, next) { + AnalyticsRegistrationSourceHelper.clearSource(req.session) + next() + } +} + +function setInbound() { + return function setInbound(req, res, next) { + if (req.session.inbound) { + return next() // don't overwrite referrer + } + + if (SessionManager.isUserLoggedIn(req.session)) { + return next() // don't store referrer if user is alread logged in + } + + const referrer = req.header('referrer') + try { + AnalyticsRegistrationSourceHelper.setInbound( + req.session, + req.url, + req.query, + referrer + ) + } catch (error) { + // log errors and fail silently + OError.tag(error, 'failed to parse inbound referrer', { + referrer, + }) + logger.warn({ error }, error.message) + } + next() + } +} + +module.exports = { + setSource, + clearSource, + setInbound, +} diff --git a/services/web/app/src/Features/Analytics/AnalyticsRouter.js b/services/web/app/src/Features/Analytics/AnalyticsRouter.js new file mode 100644 index 0000000000..90707a3eb7 --- /dev/null +++ b/services/web/app/src/Features/Analytics/AnalyticsRouter.js @@ -0,0 +1,40 @@ +const AuthenticationController = require('./../Authentication/AuthenticationController') +const AnalyticsController = require('./AnalyticsController') +const AnalyticsProxy = require('./AnalyticsProxy') +const RateLimiterMiddleware = require('./../Security/RateLimiterMiddleware') + +module.exports = { + apply(webRouter, privateApiRouter, publicApiRouter) { + webRouter.post( + '/event/:event([a-z0-9-_]+)', + RateLimiterMiddleware.rateLimit({ + endpointName: 'analytics-record-event', + maxRequests: 200, + timeInterval: 60, + }), + AnalyticsController.recordEvent + ) + + webRouter.put( + '/editingSession/:projectId', + RateLimiterMiddleware.rateLimit({ + endpointName: 'analytics-update-editing-session', + params: ['projectId'], + maxRequests: 20, + timeInterval: 60, + }), + AnalyticsController.updateEditingSession + ) + + publicApiRouter.use( + '/analytics/uniExternalCollaboration', + AuthenticationController.requirePrivateApiAuth(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'analytics-uni-external-collab-proxy', + maxRequests: 20, + timeInterval: 60, + }), + AnalyticsProxy.call('/uniExternalCollaboration') + ) + }, +} diff --git a/services/web/app/src/Features/Authentication/AuthenticationController.js b/services/web/app/src/Features/Authentication/AuthenticationController.js new file mode 100644 index 0000000000..bbd0d4c543 --- /dev/null +++ b/services/web/app/src/Features/Authentication/AuthenticationController.js @@ -0,0 +1,509 @@ +const AuthenticationManager = require('./AuthenticationManager') +const SessionManager = require('./SessionManager') +const OError = require('@overleaf/o-error') +const LoginRateLimiter = require('../Security/LoginRateLimiter') +const UserUpdater = require('../User/UserUpdater') +const Metrics = require('@overleaf/metrics') +const logger = require('logger-sharelatex') +const querystring = require('querystring') +const Settings = require('@overleaf/settings') +const basicAuth = require('basic-auth-connect') +const crypto = require('crypto') +const UserHandler = require('../User/UserHandler') +const UserSessionsManager = require('../User/UserSessionsManager') +const SessionStoreManager = require('../../infrastructure/SessionStoreManager') +const Analytics = require('../Analytics/AnalyticsManager') +const passport = require('passport') +const NotificationsBuilder = require('../Notifications/NotificationsBuilder') +const UrlHelper = require('../Helpers/UrlHelper') +const AsyncFormHelper = require('../Helpers/AsyncFormHelper') +const _ = require('lodash') +const UserAuditLogHandler = require('../User/UserAuditLogHandler') +const AnalyticsRegistrationSourceHelper = require('../Analytics/AnalyticsRegistrationSourceHelper') +const { + acceptsJson, +} = require('../../infrastructure/RequestContentTypeDetection') + +function send401WithChallenge(res) { + res.setHeader('WWW-Authenticate', 'OverleafLogin') + res.sendStatus(401) +} + +const AuthenticationController = { + serializeUser(user, callback) { + if (!user._id || !user.email) { + const err = new Error('serializeUser called with non-user object') + logger.warn({ user }, err.message) + return callback(err) + } + const lightUser = { + _id: user._id, + first_name: user.first_name, + last_name: user.last_name, + isAdmin: user.isAdmin, + staffAccess: user.staffAccess, + email: user.email, + referal_id: user.referal_id, + session_created: new Date().toISOString(), + ip_address: user._login_req_ip, + must_reconfirm: user.must_reconfirm, + v1_id: user.overleaf != null ? user.overleaf.id : undefined, + } + callback(null, lightUser) + }, + + deserializeUser(user, cb) { + cb(null, user) + }, + + passportLogin(req, res, next) { + // This function is middleware which wraps the passport.authenticate middleware, + // so we can send back our custom `{message: {text: "", type: ""}}` responses on failure, + // and send a `{redir: ""}` response on success + passport.authenticate('local', function (err, user, info) { + if (err) { + return next(err) + } + if (user) { + // `user` is either a user object or false + return AuthenticationController.finishLogin(user, req, res, next) + } else { + if (info.redir != null) { + return res.json({ redir: info.redir }) + } else { + return res.json({ message: info }) + } + } + })(req, res, next) + }, + + finishLogin(user, req, res, next) { + if (user === false) { + return res.redirect('/login') + } // OAuth2 'state' mismatch + + const Modules = require('../../infrastructure/Modules') + Modules.hooks.fire( + 'preFinishLogin', + req, + res, + user, + function (error, results) { + if (error) { + return next(error) + } + if (results.some(result => result && result.doNotFinish)) { + return + } + + if (user.must_reconfirm) { + return AuthenticationController._redirectToReconfirmPage( + req, + res, + user + ) + } + + const redir = + AuthenticationController._getRedirectFromSession(req) || '/project' + _loginAsyncHandlers(req, user) + const userId = user._id + UserAuditLogHandler.addEntry(userId, 'login', userId, req.ip, err => { + if (err) { + return next(err) + } + _afterLoginSessionSetup(req, user, function (err) { + if (err) { + return next(err) + } + AuthenticationController._clearRedirectFromSession(req) + AnalyticsRegistrationSourceHelper.clearSource(req.session) + AnalyticsRegistrationSourceHelper.clearInbound(req.session) + AsyncFormHelper.redirect(req, res, redir) + }) + }) + } + ) + }, + + doPassportLogin(req, username, password, done) { + const email = username.toLowerCase() + const Modules = require('../../infrastructure/Modules') + Modules.hooks.fire( + 'preDoPassportLogin', + req, + email, + function (err, infoList) { + if (err) { + return done(err) + } + const info = infoList.find(i => i != null) + if (info != null) { + return done(null, false, info) + } + LoginRateLimiter.processLoginRequest(email, function (err, isAllowed) { + if (err) { + return done(err) + } + if (!isAllowed) { + logger.log({ email }, 'too many login requests') + return done(null, null, { + text: req.i18n.translate('to_many_login_requests_2_mins'), + type: 'error', + }) + } + AuthenticationManager.authenticate( + { email }, + password, + function (error, user) { + if (error != null) { + return done(error) + } + if (user != null) { + // async actions + done(null, user) + } else { + AuthenticationController._recordFailedLogin() + logger.log({ email }, 'failed log in') + done(null, false, { + text: req.i18n.translate('email_or_password_wrong_try_again'), + type: 'error', + }) + } + } + ) + }) + } + ) + }, + + ipMatchCheck(req, user) { + if (req.ip !== user.lastLoginIp) { + NotificationsBuilder.ipMatcherAffiliation(user._id).create(req.ip) + } + return UserUpdater.updateUser(user._id.toString(), { + $set: { lastLoginIp: req.ip }, + }) + }, + + requireLogin() { + const doRequest = function (req, res, next) { + if (next == null) { + next = function () {} + } + if (!SessionManager.isUserLoggedIn(req.session)) { + if (acceptsJson(req)) return send401WithChallenge(res) + return AuthenticationController._redirectToLoginOrRegisterPage(req, res) + } else { + req.user = SessionManager.getSessionUser(req.session) + return next() + } + } + + return doRequest + }, + + requireOauth() { + // require this here because module may not be included in some versions + const Oauth2Server = require('../../../../modules/oauth2-server/app/src/Oauth2Server') + return function (req, res, next) { + if (next == null) { + next = function () {} + } + const request = new Oauth2Server.Request(req) + const response = new Oauth2Server.Response(res) + return Oauth2Server.server.authenticate( + request, + response, + {}, + function (err, token) { + if (err) { + // use a 401 status code for malformed header for git-bridge + if ( + err.code === 400 && + err.message === 'Invalid request: malformed authorization header' + ) { + err.code = 401 + } + // send all other errors + return res + .status(err.code) + .json({ error: err.name, error_description: err.message }) + } + req.oauth = { access_token: token.accessToken } + req.oauth_token = token + req.oauth_user = token.user + return next() + } + ) + } + }, + + validateUserSession: function () { + // Middleware to check that the user's session is still good on key actions, + // such as opening a a project. Could be used to check that session has not + // exceeded a maximum lifetime (req.session.session_created), or for session + // hijacking checks (e.g. change of ip address, req.session.ip_address). For + // now, just check that the session has been loaded from the session store + // correctly. + return function (req, res, next) { + // check that the session store is returning valid results + if (req.session && !SessionStoreManager.hasValidationToken(req)) { + // force user to update session + req.session.regenerate(() => { + // need to destroy the existing session and generate a new one + // otherwise they will already be logged in when they are redirected + // to the login page + if (acceptsJson(req)) return send401WithChallenge(res) + AuthenticationController._redirectToLoginOrRegisterPage(req, res) + }) + } else { + next() + } + } + }, + + _globalLoginWhitelist: [], + addEndpointToLoginWhitelist(endpoint) { + return AuthenticationController._globalLoginWhitelist.push(endpoint) + }, + + requireGlobalLogin(req, res, next) { + if ( + AuthenticationController._globalLoginWhitelist.includes( + req._parsedUrl.pathname + ) + ) { + return next() + } + + if (req.headers.authorization != null) { + AuthenticationController.requirePrivateApiAuth()(req, res, next) + } else if (SessionManager.isUserLoggedIn(req.session)) { + next() + } else { + logger.log( + { url: req.url }, + 'user trying to access endpoint not in global whitelist' + ) + if (acceptsJson(req)) return send401WithChallenge(res) + AuthenticationController.setRedirectInSession(req) + res.redirect('/login') + } + }, + + validateAdmin(req, res, next) { + const adminDomains = Settings.adminDomains + if ( + !adminDomains || + !(Array.isArray(adminDomains) && adminDomains.length) + ) { + return next() + } + const user = SessionManager.getSessionUser(req.session) + if (!(user && user.isAdmin)) { + return next() + } + const email = user.email + if (email == null) { + return next( + new OError('[ValidateAdmin] Admin user without email address', { + userId: user._id, + }) + ) + } + if (!adminDomains.find(domain => email.endsWith(`@${domain}`))) { + return next( + new OError('[ValidateAdmin] Admin user with invalid email domain', { + email: email, + userId: user._id, + }) + ) + } + return next() + }, + + requireBasicAuth: function (userDetails) { + return basicAuth(function (user, pass) { + const expectedPassword = userDetails[user] + const isValid = + expectedPassword && + expectedPassword.length === pass.length && + crypto.timingSafeEqual(Buffer.from(expectedPassword), Buffer.from(pass)) + if (!isValid) { + logger.err({ user }, 'invalid login details') + } + return isValid + }) + }, + + requirePrivateApiAuth() { + return AuthenticationController.requireBasicAuth(Settings.httpAuthUsers) + }, + + setRedirectInSession(req, value) { + if (value == null) { + value = + Object.keys(req.query).length > 0 + ? `${req.path}?${querystring.stringify(req.query)}` + : `${req.path}` + } + if ( + req.session != null && + !/^\/(socket.io|js|stylesheets|img)\/.*$/.test(value) && + !/^.*\.(png|jpeg|svg)$/.test(value) + ) { + const safePath = UrlHelper.getSafeRedirectPath(value) + return (req.session.postLoginRedirect = safePath) + } + }, + + _redirectToLoginOrRegisterPage(req, res) { + if ( + req.query.zipUrl != null || + req.query.project_name != null || + req.path === '/user/subscription/new' + ) { + AuthenticationController._redirectToRegisterPage(req, res) + } else { + AuthenticationController._redirectToLoginPage(req, res) + } + }, + + _redirectToLoginPage(req, res) { + logger.log( + { url: req.url }, + 'user not logged in so redirecting to login page' + ) + AuthenticationController.setRedirectInSession(req) + const url = `/login?${querystring.stringify(req.query)}` + res.redirect(url) + Metrics.inc('security.login-redirect') + }, + + _redirectToReconfirmPage(req, res, user) { + logger.log( + { url: req.url }, + 'user needs to reconfirm so redirecting to reconfirm page' + ) + req.session.reconfirm_email = user != null ? user.email : undefined + const redir = '/user/reconfirm' + AsyncFormHelper.redirect(req, res, redir) + }, + + _redirectToRegisterPage(req, res) { + logger.log( + { url: req.url }, + 'user not logged in so redirecting to register page' + ) + AuthenticationController.setRedirectInSession(req) + const url = `/register?${querystring.stringify(req.query)}` + res.redirect(url) + Metrics.inc('security.login-redirect') + }, + + _recordSuccessfulLogin(userId, callback) { + if (callback == null) { + callback = function () {} + } + UserUpdater.updateUser( + userId.toString(), + { + $set: { lastLoggedIn: new Date() }, + $inc: { loginCount: 1 }, + }, + function (error) { + if (error != null) { + callback(error) + } + Metrics.inc('user.login.success') + callback() + } + ) + }, + + _recordFailedLogin(callback) { + Metrics.inc('user.login.failed') + if (callback) callback() + }, + + _getRedirectFromSession(req) { + let safePath + const value = _.get(req, ['session', 'postLoginRedirect']) + if (value) { + safePath = UrlHelper.getSafeRedirectPath(value) + } + return safePath || null + }, + + _clearRedirectFromSession(req) { + if (req.session != null) { + delete req.session.postLoginRedirect + } + }, +} + +function _afterLoginSessionSetup(req, user, callback) { + if (callback == null) { + callback = function () {} + } + req.login(user, function (err) { + if (err) { + OError.tag(err, 'error from req.login', { + user_id: user._id, + }) + return callback(err) + } + // Regenerate the session to get a new sessionID (cookie value) to + // protect against session fixation attacks + const oldSession = req.session + req.session.destroy(function (err) { + if (err) { + OError.tag(err, 'error when trying to destroy old session', { + user_id: user._id, + }) + return callback(err) + } + req.sessionStore.generate(req) + // Note: the validation token is not writable, so it does not get + // transferred to the new session below. + for (const key in oldSession) { + const value = oldSession[key] + if (key !== '__tmp' && key !== 'csrfSecret') { + req.session[key] = value + } + } + req.session.save(function (err) { + if (err) { + OError.tag(err, 'error saving regenerated session after login', { + user_id: user._id, + }) + return callback(err) + } + UserSessionsManager.trackSession(user, req.sessionID, function () {}) + callback(null) + }) + }) + }) +} +function _loginAsyncHandlers(req, user) { + UserHandler.setupLoginData(user, err => { + if (err != null) { + logger.warn({ err }, 'error setting up login data') + } + }) + LoginRateLimiter.recordSuccessfulLogin(user.email) + AuthenticationController._recordSuccessfulLogin(user._id) + AuthenticationController.ipMatchCheck(req, user) + Analytics.recordEvent(user._id, 'user-logged-in') + Analytics.identifyUser(user._id, req.sessionID) + logger.log( + { email: user.email, user_id: user._id.toString() }, + 'successful log in' + ) + req.session.justLoggedIn = true + // capture the request ip for use when creating the session + return (user._login_req_ip = req.ip) +} + +module.exports = AuthenticationController diff --git a/services/web/app/src/Features/Authentication/AuthenticationErrors.js b/services/web/app/src/Features/Authentication/AuthenticationErrors.js new file mode 100644 index 0000000000..c2df0fcc89 --- /dev/null +++ b/services/web/app/src/Features/Authentication/AuthenticationErrors.js @@ -0,0 +1,9 @@ +const Errors = require('../Errors/Errors') + +class InvalidEmailError extends Errors.BackwardCompatibleError {} +class InvalidPasswordError extends Errors.BackwardCompatibleError {} + +module.exports = { + InvalidEmailError, + InvalidPasswordError, +} diff --git a/services/web/app/src/Features/Authentication/AuthenticationManager.js b/services/web/app/src/Features/Authentication/AuthenticationManager.js new file mode 100644 index 0000000000..0aa903803e --- /dev/null +++ b/services/web/app/src/Features/Authentication/AuthenticationManager.js @@ -0,0 +1,227 @@ +const Settings = require('@overleaf/settings') +const { User } = require('../../models/User') +const { db, ObjectId } = require('../../infrastructure/mongodb') +const bcrypt = require('bcrypt') +const EmailHelper = require('../Helpers/EmailHelper') +const { + InvalidEmailError, + InvalidPasswordError, +} = require('./AuthenticationErrors') +const util = require('util') + +const BCRYPT_ROUNDS = Settings.security.bcryptRounds || 12 +const BCRYPT_MINOR_VERSION = Settings.security.bcryptMinorVersion || 'a' + +const _checkWriteResult = function (result, callback) { + // for MongoDB + if (result && result.modifiedCount === 1) { + callback(null, true) + } else { + callback(null, false) + } +} + +const AuthenticationManager = { + authenticate(query, password, callback) { + // Using Mongoose for legacy reasons here. The returned User instance + // gets serialized into the session and there may be subtle differences + // between the user returned by Mongoose vs mongodb (such as default values) + User.findOne(query, (error, user) => { + if (error) { + return callback(error) + } + if (!user || !user.hashedPassword) { + return callback(null, null) + } + bcrypt.compare(password, user.hashedPassword, function (error, match) { + if (error) { + return callback(error) + } + if (!match) { + return callback(null, null) + } + AuthenticationManager.checkRounds( + user, + user.hashedPassword, + password, + function (err) { + if (err) { + return callback(err) + } + callback(null, user) + } + ) + }) + }) + }, + + validateEmail(email) { + const parsed = EmailHelper.parseEmail(email) + if (!parsed) { + return new InvalidEmailError({ message: 'email not valid' }) + } + return null + }, + + // validates a password based on a similar set of rules to `complexPassword.js` on the frontend + // note that `passfield.js` enforces more rules than this, but these are the most commonly set. + // returns null on success, or an error object. + validatePassword(password, email) { + if (password == null) { + return new InvalidPasswordError({ + message: 'password not set', + info: { code: 'not_set' }, + }) + } + + let allowAnyChars, min, max + if (Settings.passwordStrengthOptions) { + allowAnyChars = Settings.passwordStrengthOptions.allowAnyChars === true + if (Settings.passwordStrengthOptions.length) { + min = Settings.passwordStrengthOptions.length.min + max = Settings.passwordStrengthOptions.length.max + } + } + allowAnyChars = !!allowAnyChars + min = min || 6 + max = max || 72 + + // we don't support passwords > 72 characters in length, because bcrypt truncates them + if (max > 72) { + max = 72 + } + + if (password.length < min) { + return new InvalidPasswordError({ + message: 'password is too short', + info: { code: 'too_short' }, + }) + } + if (password.length > max) { + return new InvalidPasswordError({ + message: 'password is too long', + info: { code: 'too_long' }, + }) + } + if ( + !allowAnyChars && + !AuthenticationManager._passwordCharactersAreValid(password) + ) { + return new InvalidPasswordError({ + message: 'password contains an invalid character', + info: { code: 'invalid_character' }, + }) + } + if (typeof email === 'string' && email !== '') { + const startOfEmail = email.split('@')[0] + if ( + password.indexOf(email) !== -1 || + password.indexOf(startOfEmail) !== -1 + ) { + return new InvalidPasswordError({ + message: 'password contains part of email address', + info: { code: 'contains_email' }, + }) + } + } + return null + }, + + setUserPassword(user, password, callback) { + AuthenticationManager.setUserPasswordInV2(user, password, callback) + }, + + checkRounds(user, hashedPassword, password, callback) { + // Temporarily disable this function, TODO: re-enable this + if (Settings.security.disableBcryptRoundsUpgrades) { + return callback() + } + // check current number of rounds and rehash if necessary + const currentRounds = bcrypt.getRounds(hashedPassword) + if (currentRounds < BCRYPT_ROUNDS) { + AuthenticationManager.setUserPassword(user, password, callback) + } else { + callback() + } + }, + + hashPassword(password, callback) { + bcrypt.genSalt(BCRYPT_ROUNDS, BCRYPT_MINOR_VERSION, function (error, salt) { + if (error) { + return callback(error) + } + bcrypt.hash(password, salt, callback) + }) + }, + + setUserPasswordInV2(user, password, callback) { + if (!user || !user.email || !user._id) { + return callback(new Error('invalid user object')) + } + const validationError = this.validatePassword(password, user.email) + if (validationError) { + return callback(validationError) + } + this.hashPassword(password, function (error, hash) { + if (error) { + return callback(error) + } + db.users.updateOne( + { + _id: ObjectId(user._id.toString()), + }, + { + $set: { + hashedPassword: hash, + }, + $unset: { + password: true, + }, + }, + function (updateError, result) { + if (updateError) { + return callback(updateError) + } + _checkWriteResult(result, callback) + } + ) + }) + }, + + _passwordCharactersAreValid(password) { + let digits, letters, lettersUp, symbols + if ( + Settings.passwordStrengthOptions && + Settings.passwordStrengthOptions.chars + ) { + digits = Settings.passwordStrengthOptions.chars.digits + letters = Settings.passwordStrengthOptions.chars.letters + lettersUp = Settings.passwordStrengthOptions.chars.letters_up + symbols = Settings.passwordStrengthOptions.chars.symbols + } + digits = digits || '1234567890' + letters = letters || 'abcdefghijklmnopqrstuvwxyz' + lettersUp = lettersUp || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + symbols = symbols || '@#$%^&*()-_=+[]{};:<>/?!£€.,' + + for (let charIndex = 0; charIndex <= password.length - 1; charIndex++) { + if ( + digits.indexOf(password[charIndex]) === -1 && + letters.indexOf(password[charIndex]) === -1 && + lettersUp.indexOf(password[charIndex]) === -1 && + symbols.indexOf(password[charIndex]) === -1 + ) { + return false + } + } + return true + }, +} + +AuthenticationManager.promises = { + authenticate: util.promisify(AuthenticationManager.authenticate), + hashPassword: util.promisify(AuthenticationManager.hashPassword), + setUserPassword: util.promisify(AuthenticationManager.setUserPassword), +} + +module.exports = AuthenticationManager diff --git a/services/web/app/src/Features/Authentication/SessionManager.js b/services/web/app/src/Features/Authentication/SessionManager.js new file mode 100644 index 0000000000..a64ee98fe1 --- /dev/null +++ b/services/web/app/src/Features/Authentication/SessionManager.js @@ -0,0 +1,46 @@ +const _ = require('lodash') + +const SessionManager = { + getSessionUser(session) { + const sessionUser = _.get(session, ['user']) + const sessionPassportUser = _.get(session, ['passport', 'user']) + return sessionUser || sessionPassportUser || null + }, + + setInSessionUser(session, props) { + const sessionUser = SessionManager.getSessionUser(session) + if (!sessionUser) { + return + } + for (const key in props) { + const value = props[key] + sessionUser[key] = value + } + return null + }, + + isUserLoggedIn(session) { + const userId = SessionManager.getLoggedInUserId(session) + return ![null, undefined, false].includes(userId) + }, + + getLoggedInUserId(session) { + const user = SessionManager.getSessionUser(session) + if (user) { + return user._id + } else { + return null + } + }, + + getLoggedInUserV1Id(session) { + const user = SessionManager.getSessionUser(session) + if (user != null && user.v1_id != null) { + return user.v1_id + } else { + return null + } + }, +} + +module.exports = SessionManager diff --git a/services/web/app/src/Features/Authorization/AuthorizationManager.js b/services/web/app/src/Features/Authorization/AuthorizationManager.js new file mode 100644 index 0000000000..1a71d96598 --- /dev/null +++ b/services/web/app/src/Features/Authorization/AuthorizationManager.js @@ -0,0 +1,295 @@ +const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') +const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') +const ProjectGetter = require('../Project/ProjectGetter') +const { User } = require('../../models/User') +const PrivilegeLevels = require('./PrivilegeLevels') +const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler') +const PublicAccessLevels = require('./PublicAccessLevels') +const Errors = require('../Errors/Errors') +const { ObjectId } = require('mongodb') +const { promisifyAll } = require('../../util/promises') + +const AuthorizationManager = { + isRestrictedUser(userId, privilegeLevel, isTokenMember) { + if (privilegeLevel === PrivilegeLevels.NONE) { + return true + } + return ( + privilegeLevel === PrivilegeLevels.READ_ONLY && (isTokenMember || !userId) + ) + }, + + isRestrictedUserForProject(userId, projectId, token, callback) { + AuthorizationManager.getPrivilegeLevelForProject( + userId, + projectId, + token, + (err, privilegeLevel) => { + if (err) { + return callback(err) + } + CollaboratorsHandler.userIsTokenMember( + userId, + projectId, + (err, isTokenMember) => { + if (err) { + return callback(err) + } + callback( + null, + AuthorizationManager.isRestrictedUser( + userId, + privilegeLevel, + isTokenMember + ) + ) + } + ) + } + ) + }, + + getPublicAccessLevel(projectId, callback) { + if (!ObjectId.isValid(projectId)) { + return callback(new Error('invalid project id')) + } + // Note, the Project property in the DB is `publicAccesLevel`, without the second `s` + ProjectGetter.getProject( + projectId, + { publicAccesLevel: 1 }, + function (error, project) { + if (error) { + return callback(error) + } + if (!project) { + return callback( + new Errors.NotFoundError(`no project found with id ${projectId}`) + ) + } + callback(null, project.publicAccesLevel) + } + ) + }, + + // Get the privilege level that the user has for the project + // Returns: + // * privilegeLevel: "owner", "readAndWrite", of "readOnly" if the user has + // access. false if the user does not have access + // * becausePublic: true if the access level is only because the project is public. + // * becauseSiteAdmin: true if access level is only because user is admin + getPrivilegeLevelForProject(userId, projectId, token, callback) { + if (userId) { + AuthorizationManager.getPrivilegeLevelForProjectWithUser( + userId, + projectId, + token, + callback + ) + } else { + AuthorizationManager.getPrivilegeLevelForProjectWithoutUser( + projectId, + token, + callback + ) + } + }, + + // User is present, get their privilege level from database + getPrivilegeLevelForProjectWithUser(userId, projectId, token, callback) { + CollaboratorsGetter.getMemberIdPrivilegeLevel( + userId, + projectId, + function (error, privilegeLevel) { + if (error) { + return callback(error) + } + if (privilegeLevel && privilegeLevel !== PrivilegeLevels.NONE) { + // The user has direct access + return callback(null, privilegeLevel, false, false) + } + AuthorizationManager.isUserSiteAdmin(userId, function (error, isAdmin) { + if (error) { + return callback(error) + } + if (isAdmin) { + return callback(null, PrivilegeLevels.OWNER, false, true) + } + // Legacy public-access system + // User is present (not anonymous), but does not have direct access + AuthorizationManager.getPublicAccessLevel( + projectId, + function (err, publicAccessLevel) { + if (err) { + return callback(err) + } + if (publicAccessLevel === PublicAccessLevels.READ_ONLY) { + return callback(null, PrivilegeLevels.READ_ONLY, true, false) + } + if (publicAccessLevel === PublicAccessLevels.READ_AND_WRITE) { + return callback( + null, + PrivilegeLevels.READ_AND_WRITE, + true, + false + ) + } + callback(null, PrivilegeLevels.NONE, false, false) + } + ) + }) + } + ) + }, + + // User is Anonymous, Try Token-based access + getPrivilegeLevelForProjectWithoutUser(projectId, token, callback) { + AuthorizationManager.getPublicAccessLevel( + projectId, + function (err, publicAccessLevel) { + if (err) { + return callback(err) + } + if (publicAccessLevel === PublicAccessLevels.READ_ONLY) { + // Legacy public read-only access for anonymous user + return callback(null, PrivilegeLevels.READ_ONLY, true, false) + } + if (publicAccessLevel === PublicAccessLevels.READ_AND_WRITE) { + // Legacy public read-write access for anonymous user + return callback(null, PrivilegeLevels.READ_AND_WRITE, true, false) + } + if (publicAccessLevel === PublicAccessLevels.TOKEN_BASED) { + return AuthorizationManager.getPrivilegeLevelForProjectWithToken( + projectId, + token, + callback + ) + } + // Deny anonymous user access + callback(null, PrivilegeLevels.NONE, false, false) + } + ) + }, + + getPrivilegeLevelForProjectWithToken(projectId, token, callback) { + // Anonymous users can have read-only access to token-based projects, + // while read-write access must be logged in, + // unless the `enableAnonymousReadAndWriteSharing` setting is enabled + TokenAccessHandler.validateTokenForAnonymousAccess( + projectId, + token, + function (err, isValidReadAndWrite, isValidReadOnly) { + if (err) { + return callback(err) + } + if (isValidReadOnly) { + // Grant anonymous user read-only access + return callback(null, PrivilegeLevels.READ_ONLY, false, false) + } + if (isValidReadAndWrite) { + // Grant anonymous user read-and-write access + return callback(null, PrivilegeLevels.READ_AND_WRITE, false, false) + } + // Deny anonymous access + callback(null, PrivilegeLevels.NONE, false, false) + } + ) + }, + + canUserReadProject(userId, projectId, token, callback) { + AuthorizationManager.getPrivilegeLevelForProject( + userId, + projectId, + token, + function (error, privilegeLevel) { + if (error) { + return callback(error) + } + callback( + null, + [ + PrivilegeLevels.OWNER, + PrivilegeLevels.READ_AND_WRITE, + PrivilegeLevels.READ_ONLY, + ].includes(privilegeLevel) + ) + } + ) + }, + + canUserWriteProjectContent(userId, projectId, token, callback) { + AuthorizationManager.getPrivilegeLevelForProject( + userId, + projectId, + token, + function (error, privilegeLevel) { + if (error) { + return callback(error) + } + callback( + null, + [PrivilegeLevels.OWNER, PrivilegeLevels.READ_AND_WRITE].includes( + privilegeLevel + ) + ) + } + ) + }, + + canUserWriteProjectSettings(userId, projectId, token, callback) { + AuthorizationManager.getPrivilegeLevelForProject( + userId, + projectId, + token, + function (error, privilegeLevel, becausePublic) { + if (error) { + return callback(error) + } + if (privilegeLevel === PrivilegeLevels.OWNER) { + return callback(null, true) + } + if ( + privilegeLevel === PrivilegeLevels.READ_AND_WRITE && + !becausePublic + ) { + return callback(null, true) + } + callback(null, false) + } + ) + }, + + canUserAdminProject(userId, projectId, token, callback) { + AuthorizationManager.getPrivilegeLevelForProject( + userId, + projectId, + token, + function (error, privilegeLevel, becausePublic, becauseSiteAdmin) { + if (error) { + return callback(error) + } + callback( + null, + privilegeLevel === PrivilegeLevels.OWNER, + becauseSiteAdmin + ) + } + ) + }, + + isUserSiteAdmin(userId, callback) { + if (!userId) { + return callback(null, false) + } + User.findOne({ _id: userId }, { isAdmin: 1 }, function (error, user) { + if (error) { + return callback(error) + } + callback(null, (user && user.isAdmin) === true) + }) + }, +} + +module.exports = AuthorizationManager +module.exports.promises = promisifyAll(AuthorizationManager, { + without: 'isRestrictedUser', +}) diff --git a/services/web/app/src/Features/Authorization/AuthorizationMiddleware.js b/services/web/app/src/Features/Authorization/AuthorizationMiddleware.js new file mode 100644 index 0000000000..ef9a234d40 --- /dev/null +++ b/services/web/app/src/Features/Authorization/AuthorizationMiddleware.js @@ -0,0 +1,272 @@ +let AuthorizationMiddleware +const AuthorizationManager = require('./AuthorizationManager') +const async = require('async') +const logger = require('logger-sharelatex') +const { ObjectId } = require('mongodb') +const Errors = require('../Errors/Errors') +const HttpErrorHandler = require('../Errors/HttpErrorHandler') +const AuthenticationController = require('../Authentication/AuthenticationController') +const SessionManager = require('../Authentication/SessionManager') +const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler') + +module.exports = AuthorizationMiddleware = { + ensureUserCanReadMultipleProjects(req, res, next) { + const projectIds = (req.query.project_ids || '').split(',') + AuthorizationMiddleware._getUserId(req, function (error, userId) { + if (error) { + return next(error) + } + // Remove the projects we have access to. Note rejectSeries doesn't use + // errors in callbacks + async.rejectSeries( + projectIds, + function (projectId, cb) { + const token = TokenAccessHandler.getRequestToken(req, projectId) + AuthorizationManager.canUserReadProject( + userId, + projectId, + token, + function (error, canRead) { + if (error) { + return next(error) + } + cb(canRead) + } + ) + }, + function (unauthorizedProjectIds) { + if (unauthorizedProjectIds.length > 0) { + return AuthorizationMiddleware.redirectToRestricted(req, res, next) + } + next() + } + ) + }) + }, + + blockRestrictedUserFromProject(req, res, next) { + AuthorizationMiddleware._getUserAndProjectId( + req, + function (error, userId, projectId) { + if (error) { + return next(error) + } + const token = TokenAccessHandler.getRequestToken(req, projectId) + AuthorizationManager.isRestrictedUserForProject( + userId, + projectId, + token, + (err, isRestrictedUser) => { + if (err) { + return next(err) + } + if (isRestrictedUser) { + return res.sendStatus(403) + } + next() + } + ) + } + ) + }, + + ensureUserCanReadProject(req, res, next) { + AuthorizationMiddleware._getUserAndProjectId( + req, + function (error, userId, projectId) { + if (error) { + return next(error) + } + const token = TokenAccessHandler.getRequestToken(req, projectId) + AuthorizationManager.canUserReadProject( + userId, + projectId, + token, + function (error, canRead) { + if (error) { + return next(error) + } + if (canRead) { + logger.log( + { userId, projectId }, + 'allowing user read access to project' + ) + return next() + } + logger.log( + { userId, projectId }, + 'denying user read access to project' + ) + HttpErrorHandler.forbidden(req, res) + } + ) + } + ) + }, + + ensureUserCanWriteProjectSettings(req, res, next) { + AuthorizationMiddleware._getUserAndProjectId( + req, + function (error, userId, projectId) { + if (error) { + return next(error) + } + const token = TokenAccessHandler.getRequestToken(req, projectId) + AuthorizationManager.canUserWriteProjectSettings( + userId, + projectId, + token, + function (error, canWrite) { + if (error) { + return next(error) + } + if (canWrite) { + logger.log( + { userId, projectId }, + 'allowing user write access to project settings' + ) + return next() + } + logger.log( + { userId, projectId }, + 'denying user write access to project settings' + ) + HttpErrorHandler.forbidden(req, res) + } + ) + } + ) + }, + + ensureUserCanWriteProjectContent(req, res, next) { + AuthorizationMiddleware._getUserAndProjectId( + req, + function (error, userId, projectId) { + if (error) { + return next(error) + } + const token = TokenAccessHandler.getRequestToken(req, projectId) + AuthorizationManager.canUserWriteProjectContent( + userId, + projectId, + token, + function (error, canWrite) { + if (error) { + return next(error) + } + if (canWrite) { + logger.log( + { userId, projectId }, + 'allowing user write access to project content' + ) + return next() + } + logger.log( + { userId, projectId }, + 'denying user write access to project settings' + ) + HttpErrorHandler.forbidden(req, res) + } + ) + } + ) + }, + + ensureUserCanAdminProject(req, res, next) { + AuthorizationMiddleware._getUserAndProjectId( + req, + function (error, userId, projectId) { + if (error) { + return next(error) + } + const token = TokenAccessHandler.getRequestToken(req, projectId) + AuthorizationManager.canUserAdminProject( + userId, + projectId, + token, + function (error, canAdmin) { + if (error) { + return next(error) + } + if (canAdmin) { + logger.log( + { userId, projectId }, + 'allowing user admin access to project' + ) + return next() + } + logger.log( + { userId, projectId }, + 'denying user admin access to project' + ) + HttpErrorHandler.forbidden(req, res) + } + ) + } + ) + }, + + ensureUserIsSiteAdmin(req, res, next) { + AuthorizationMiddleware._getUserId(req, function (error, userId) { + if (error) { + return next(error) + } + AuthorizationManager.isUserSiteAdmin(userId, function (error, isAdmin) { + if (error) { + return next(error) + } + if (isAdmin) { + logger.log({ userId }, 'allowing user admin access to site') + return next() + } + logger.log({ userId }, 'denying user admin access to site') + AuthorizationMiddleware.redirectToRestricted(req, res, next) + }) + }) + }, + + _getUserAndProjectId(req, callback) { + const projectId = req.params.project_id || req.params.Project_id + if (!projectId) { + return callback(new Error('Expected project_id in request parameters')) + } + if (!ObjectId.isValid(projectId)) { + return callback( + new Errors.NotFoundError(`invalid projectId: ${projectId}`) + ) + } + AuthorizationMiddleware._getUserId(req, function (error, userId) { + if (error) { + return callback(error) + } + callback(null, userId, projectId) + }) + }, + + _getUserId(req, callback) { + const userId = + SessionManager.getLoggedInUserId(req.session) || + (req.oauth_user && req.oauth_user._id) || + null + callback(null, userId) + }, + + redirectToRestricted(req, res, next) { + // TODO: move this to throwing ForbiddenError + res.redirect( + `/restricted?from=${encodeURIComponent(res.locals.currentUrl)}` + ) + }, + + restricted(req, res, next) { + if (SessionManager.isUserLoggedIn(req.session)) { + return res.render('user/restricted', { title: 'restricted' }) + } + const { from } = req.query + logger.log({ from }, 'redirecting to login') + if (from) { + AuthenticationController.setRedirectInSession(req, from) + } + res.redirect('/login') + }, +} diff --git a/services/web/app/src/Features/Authorization/PrivilegeLevels.js b/services/web/app/src/Features/Authorization/PrivilegeLevels.js new file mode 100644 index 0000000000..a598f3d04d --- /dev/null +++ b/services/web/app/src/Features/Authorization/PrivilegeLevels.js @@ -0,0 +1,8 @@ +const PrivilegeLevels = { + NONE: false, + READ_ONLY: 'readOnly', + READ_AND_WRITE: 'readAndWrite', + OWNER: 'owner', +} + +module.exports = PrivilegeLevels diff --git a/services/web/app/src/Features/Authorization/PublicAccessLevels.js b/services/web/app/src/Features/Authorization/PublicAccessLevels.js new file mode 100644 index 0000000000..c7619ff638 --- /dev/null +++ b/services/web/app/src/Features/Authorization/PublicAccessLevels.js @@ -0,0 +1,6 @@ +module.exports = { + READ_ONLY: 'readOnly', // LEGACY + READ_AND_WRITE: 'readAndWrite', // LEGACY + PRIVATE: 'private', + TOKEN_BASED: 'tokenBased', +} diff --git a/services/web/app/src/Features/Authorization/Sources.js b/services/web/app/src/Features/Authorization/Sources.js new file mode 100644 index 0000000000..e84126af14 --- /dev/null +++ b/services/web/app/src/Features/Authorization/Sources.js @@ -0,0 +1,5 @@ +module.exports = { + INVITE: 'invite', + TOKEN: 'token', + OWNER: 'owner', +} diff --git a/services/web/app/src/Features/BetaProgram/BetaProgramController.js b/services/web/app/src/Features/BetaProgram/BetaProgramController.js new file mode 100644 index 0000000000..b8d8d3c49f --- /dev/null +++ b/services/web/app/src/Features/BetaProgram/BetaProgramController.js @@ -0,0 +1,56 @@ +const BetaProgramHandler = require('./BetaProgramHandler') +const OError = require('@overleaf/o-error') +const UserGetter = require('../User/UserGetter') +const Settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const SessionManager = require('../Authentication/SessionManager') + +const BetaProgramController = { + optIn(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + logger.log({ userId }, 'user opting in to beta program') + if (userId == null) { + return next(new Error('no user id in session')) + } + BetaProgramHandler.optIn(userId, function (err) { + if (err) { + return next(err) + } + res.redirect('/beta/participate') + }) + }, + + optOut(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + logger.log({ userId }, 'user opting out of beta program') + if (userId == null) { + return next(new Error('no user id in session')) + } + BetaProgramHandler.optOut(userId, function (err) { + if (err) { + return next(err) + } + res.redirect('/beta/participate') + }) + }, + + optInPage(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + logger.log({ user_id: userId }, 'showing beta participation page for user') + UserGetter.getUser(userId, function (err, user) { + if (err) { + OError.tag(err, 'error fetching user', { + userId, + }) + return next(err) + } + res.render('beta_program/opt_in', { + title: 'sharelatex_beta_program', + user, + languages: Settings.languages, + }) + }) + }, +} + +module.exports = BetaProgramController diff --git a/services/web/app/src/Features/BetaProgram/BetaProgramHandler.js b/services/web/app/src/Features/BetaProgram/BetaProgramHandler.js new file mode 100644 index 0000000000..41593c3582 --- /dev/null +++ b/services/web/app/src/Features/BetaProgram/BetaProgramHandler.js @@ -0,0 +1,28 @@ +const { callbackify } = require('util') +const metrics = require('@overleaf/metrics') +const UserUpdater = require('../User/UserUpdater') + +async function optIn(userId) { + await UserUpdater.promises.updateUser(userId, { $set: { betaProgram: true } }) + metrics.inc('beta-program.opt-in') +} + +async function optOut(userId) { + await UserUpdater.promises.updateUser(userId, { + $set: { betaProgram: false }, + }) + metrics.inc('beta-program.opt-out') +} + +const BetaProgramHandler = { + optIn: callbackify(optIn), + + optOut: callbackify(optOut), +} + +BetaProgramHandler.promises = { + optIn, + optOut, +} + +module.exports = BetaProgramHandler diff --git a/services/web/app/src/Features/BrandVariations/BrandVariationsHandler.js b/services/web/app/src/Features/BrandVariations/BrandVariationsHandler.js new file mode 100644 index 0000000000..350c822488 --- /dev/null +++ b/services/web/app/src/Features/BrandVariations/BrandVariationsHandler.js @@ -0,0 +1,82 @@ +const OError = require('@overleaf/o-error') +const url = require('url') +const settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const V1Api = require('../V1/V1Api') +const sanitizeHtml = require('sanitize-html') + +module.exports = { + getBrandVariationById, +} + +function getBrandVariationById(brandVariationId, callback) { + if (brandVariationId == null || brandVariationId === '') { + return callback(new Error('Branding variation id not provided')) + } + logger.log({ brandVariationId }, 'fetching brand variation details from v1') + V1Api.request( + { + uri: `/api/v2/brand_variations/${brandVariationId}`, + }, + function (error, response, brandVariationDetails) { + if (error != null) { + OError.tag(error, 'error getting brand variation details', { + brandVariationId, + }) + return callback(error) + } + formatBrandVariationDetails(brandVariationDetails) + sanitizeBrandVariationDetails(brandVariationDetails) + callback(null, brandVariationDetails) + } + ) +} + +function formatBrandVariationDetails(details) { + if (details.export_url != null) { + details.export_url = setV1AsHostIfRelativeURL(details.export_url) + } + if (details.home_url != null) { + details.home_url = setV1AsHostIfRelativeURL(details.home_url) + } + if (details.logo_url != null) { + details.logo_url = setV1AsHostIfRelativeURL(details.logo_url) + } + if (details.journal_guidelines_url != null) { + details.journal_guidelines_url = setV1AsHostIfRelativeURL( + details.journal_guidelines_url + ) + } + if (details.journal_cover_url != null) { + details.journal_cover_url = setV1AsHostIfRelativeURL( + details.journal_cover_url + ) + } + if (details.submission_confirmation_page_logo_url != null) { + details.submission_confirmation_page_logo_url = setV1AsHostIfRelativeURL( + details.submission_confirmation_page_logo_url + ) + } + if (details.publish_menu_icon != null) { + details.publish_menu_icon = setV1AsHostIfRelativeURL( + details.publish_menu_icon + ) + } +} + +function sanitizeBrandVariationDetails(details) { + if (details.submit_button_html) { + details.submit_button_html = sanitizeHtml( + details.submit_button_html, + settings.modules.sanitize.options + ) + } +} + +function setV1AsHostIfRelativeURL(urlString) { + // The first argument is the base URL to resolve against if the second argument is not absolute. + // As it only applies if the second argument is not absolute, we can use it to transform relative URLs into + // absolute ones using v1 as the host. If the URL is absolute (e.g. a filepicker one), then the base + // argument is just ignored + return url.resolve(settings.apis.v1.url, urlString) +} diff --git a/services/web/app/src/Features/Captcha/CaptchaMiddleware.js b/services/web/app/src/Features/Captcha/CaptchaMiddleware.js new file mode 100644 index 0000000000..2bf2877345 --- /dev/null +++ b/services/web/app/src/Features/Captcha/CaptchaMiddleware.js @@ -0,0 +1,64 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CaptchaMiddleware +const request = require('request') +const logger = require('logger-sharelatex') +const Settings = require('@overleaf/settings') + +module.exports = CaptchaMiddleware = { + validateCaptcha(action) { + return function (req, res, next) { + if ( + (Settings.recaptcha != null ? Settings.recaptcha.siteKey : undefined) == + null + ) { + return next() + } + if (Settings.recaptcha.disabled[action]) { + return next() + } + const response = req.body['g-recaptcha-response'] + const options = { + form: { + secret: Settings.recaptcha.secretKey, + response, + }, + json: true, + } + return request.post( + 'https://www.google.com/recaptcha/api/siteverify', + options, + function (error, response, body) { + if (error != null) { + return next(error) + } + if (!(body != null ? body.success : undefined)) { + logger.warn( + { statusCode: response.statusCode, body }, + 'failed recaptcha siteverify request' + ) + return res.status(400).send({ + errorReason: 'cannot_verify_user_not_robot', + message: { + text: + 'Sorry, we could not verify that you are not a robot. Please check that Google reCAPTCHA is not being blocked by an ad blocker or firewall.', + }, + }) + } else { + return next() + } + } + ) + } + }, +} diff --git a/services/web/app/src/Features/Chat/ChatApiHandler.js b/services/web/app/src/Features/Chat/ChatApiHandler.js new file mode 100644 index 0000000000..8afe7319fa --- /dev/null +++ b/services/web/app/src/Features/Chat/ChatApiHandler.js @@ -0,0 +1,168 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ChatApiHandler +const OError = require('@overleaf/o-error') +const request = require('request') +const settings = require('@overleaf/settings') + +module.exports = ChatApiHandler = { + _apiRequest(opts, callback) { + if (callback == null) { + callback = function (error, data) {} + } + return request(opts, function (error, response, data) { + if (error != null) { + return callback(error) + } + if (response.statusCode >= 200 && response.statusCode < 300) { + return callback(null, data) + } else { + error = new OError( + `chat api returned non-success code: ${response.statusCode}`, + opts + ) + error.statusCode = response.statusCode + return callback(error) + } + }) + }, + + sendGlobalMessage(project_id, user_id, content, callback) { + return ChatApiHandler._apiRequest( + { + url: `${settings.apis.chat.internal_url}/project/${project_id}/messages`, + method: 'POST', + json: { user_id, content }, + }, + callback + ) + }, + + getGlobalMessages(project_id, limit, before, callback) { + const qs = {} + if (limit != null) { + qs.limit = limit + } + if (before != null) { + qs.before = before + } + + return ChatApiHandler._apiRequest( + { + url: `${settings.apis.chat.internal_url}/project/${project_id}/messages`, + method: 'GET', + qs, + json: true, + }, + callback + ) + }, + + sendComment(project_id, thread_id, user_id, content, callback) { + if (callback == null) { + callback = function (error) {} + } + return ChatApiHandler._apiRequest( + { + url: `${settings.apis.chat.internal_url}/project/${project_id}/thread/${thread_id}/messages`, + method: 'POST', + json: { user_id, content }, + }, + callback + ) + }, + + getThreads(project_id, callback) { + if (callback == null) { + callback = function (error) {} + } + return ChatApiHandler._apiRequest( + { + url: `${settings.apis.chat.internal_url}/project/${project_id}/threads`, + method: 'GET', + json: true, + }, + callback + ) + }, + + resolveThread(project_id, thread_id, user_id, callback) { + if (callback == null) { + callback = function (error) {} + } + return ChatApiHandler._apiRequest( + { + url: `${settings.apis.chat.internal_url}/project/${project_id}/thread/${thread_id}/resolve`, + method: 'POST', + json: { user_id }, + }, + callback + ) + }, + + reopenThread(project_id, thread_id, callback) { + if (callback == null) { + callback = function (error) {} + } + return ChatApiHandler._apiRequest( + { + url: `${settings.apis.chat.internal_url}/project/${project_id}/thread/${thread_id}/reopen`, + method: 'POST', + }, + callback + ) + }, + + deleteThread(project_id, thread_id, callback) { + if (callback == null) { + callback = function (error) {} + } + return ChatApiHandler._apiRequest( + { + url: `${settings.apis.chat.internal_url}/project/${project_id}/thread/${thread_id}`, + method: 'DELETE', + }, + callback + ) + }, + + editMessage(project_id, thread_id, message_id, content, callback) { + if (callback == null) { + callback = function (error) {} + } + return ChatApiHandler._apiRequest( + { + url: `${settings.apis.chat.internal_url}/project/${project_id}/thread/${thread_id}/messages/${message_id}/edit`, + method: 'POST', + json: { + content, + }, + }, + callback + ) + }, + + deleteMessage(project_id, thread_id, message_id, callback) { + if (callback == null) { + callback = function (error) {} + } + return ChatApiHandler._apiRequest( + { + url: `${settings.apis.chat.internal_url}/project/${project_id}/thread/${thread_id}/messages/${message_id}`, + method: 'DELETE', + }, + callback + ) + }, +} diff --git a/services/web/app/src/Features/Chat/ChatController.js b/services/web/app/src/Features/Chat/ChatController.js new file mode 100644 index 0000000000..4b900a3b0f --- /dev/null +++ b/services/web/app/src/Features/Chat/ChatController.js @@ -0,0 +1,134 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ChatController +const ChatApiHandler = require('./ChatApiHandler') +const EditorRealTimeController = require('../Editor/EditorRealTimeController') +const SessionManager = require('../Authentication/SessionManager') +const UserInfoManager = require('../User/UserInfoManager') +const UserInfoController = require('../User/UserInfoController') +const async = require('async') + +module.exports = ChatController = { + sendMessage(req, res, next) { + const { project_id } = req.params + const { content } = req.body + const user_id = SessionManager.getLoggedInUserId(req.session) + if (user_id == null) { + const err = new Error('no logged-in user') + return next(err) + } + return ChatApiHandler.sendGlobalMessage( + project_id, + user_id, + content, + function (err, message) { + if (err != null) { + return next(err) + } + return UserInfoManager.getPersonalInfo( + message.user_id, + function (err, user) { + if (err != null) { + return next(err) + } + message.user = UserInfoController.formatPersonalInfo(user) + EditorRealTimeController.emitToRoom( + project_id, + 'new-chat-message', + message + ) + return res.sendStatus(204) + } + ) + } + ) + }, + + getMessages(req, res, next) { + const { project_id } = req.params + const { query } = req + return ChatApiHandler.getGlobalMessages( + project_id, + query.limit, + query.before, + function (err, messages) { + if (err != null) { + return next(err) + } + return ChatController._injectUserInfoIntoThreads( + { global: { messages } }, + function (err) { + if (err != null) { + return next(err) + } + return res.json(messages) + } + ) + } + ) + }, + + _injectUserInfoIntoThreads(threads, callback) { + // There will be a lot of repitition of user_ids, so first build a list + // of unique ones to perform db look ups on, then use these to populate the + // user fields + let message, thread, thread_id, user_id + if (callback == null) { + callback = function (error, threads) {} + } + const user_ids = {} + for (thread_id in threads) { + thread = threads[thread_id] + if (thread.resolved) { + user_ids[thread.resolved_by_user_id] = true + } + for (message of Array.from(thread.messages)) { + user_ids[message.user_id] = true + } + } + + const jobs = [] + const users = {} + for (user_id in user_ids) { + const _ = user_ids[user_id] + ;(user_id => + jobs.push(cb => + UserInfoManager.getPersonalInfo(user_id, function (error, user) { + if (error != null) return cb(error) + user = UserInfoController.formatPersonalInfo(user) + users[user_id] = user + cb() + }) + ))(user_id) + } + + return async.series(jobs, function (error) { + if (error != null) { + return callback(error) + } + for (thread_id in threads) { + thread = threads[thread_id] + if (thread.resolved) { + thread.resolved_by_user = users[thread.resolved_by_user_id] + } + for (message of Array.from(thread.messages)) { + message.user = users[message.user_id] + } + } + return callback(null, threads) + }) + }, +} diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsController.js b/services/web/app/src/Features/Collaborators/CollaboratorsController.js new file mode 100644 index 0000000000..31159162a6 --- /dev/null +++ b/services/web/app/src/Features/Collaborators/CollaboratorsController.js @@ -0,0 +1,115 @@ +const OError = require('@overleaf/o-error') +const HttpErrorHandler = require('../../Features/Errors/HttpErrorHandler') +const { ObjectId } = require('mongodb') +const CollaboratorsHandler = require('./CollaboratorsHandler') +const CollaboratorsGetter = require('./CollaboratorsGetter') +const OwnershipTransferHandler = require('./OwnershipTransferHandler') +const SessionManager = require('../Authentication/SessionManager') +const EditorRealTimeController = require('../Editor/EditorRealTimeController') +const TagsHandler = require('../Tags/TagsHandler') +const Errors = require('../Errors/Errors') +const logger = require('logger-sharelatex') +const { expressify } = require('../../util/promises') + +module.exports = { + removeUserFromProject: expressify(removeUserFromProject), + removeSelfFromProject: expressify(removeSelfFromProject), + getAllMembers: expressify(getAllMembers), + setCollaboratorInfo: expressify(setCollaboratorInfo), + transferOwnership: expressify(transferOwnership), +} + +async function removeUserFromProject(req, res, next) { + const projectId = req.params.Project_id + const userId = req.params.user_id + await _removeUserIdFromProject(projectId, userId) + EditorRealTimeController.emitToRoom(projectId, 'project:membership:changed', { + members: true, + }) + res.sendStatus(204) +} + +async function removeSelfFromProject(req, res, next) { + const projectId = req.params.Project_id + const userId = SessionManager.getLoggedInUserId(req.session) + await _removeUserIdFromProject(projectId, userId) + res.sendStatus(204) +} + +async function getAllMembers(req, res, next) { + const projectId = req.params.Project_id + logger.log({ projectId }, 'getting all active members for project') + let members + try { + members = await CollaboratorsGetter.promises.getAllInvitedMembers(projectId) + } catch (err) { + throw OError.tag(err, 'error getting members for project', { projectId }) + } + res.json({ members }) +} + +async function setCollaboratorInfo(req, res, next) { + try { + const projectId = req.params.Project_id + const userId = req.params.user_id + const { privilegeLevel } = req.body + await CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel( + projectId, + userId, + privilegeLevel + ) + EditorRealTimeController.emitToRoom( + projectId, + 'project:membership:changed', + { members: true } + ) + res.sendStatus(204) + } catch (err) { + if (err instanceof Errors.NotFoundError) { + HttpErrorHandler.notFound(req, res) + } else { + next(err) + } + } +} + +async function transferOwnership(req, res, next) { + const sessionUser = SessionManager.getSessionUser(req.session) + const projectId = req.params.Project_id + const toUserId = req.body.user_id + try { + await OwnershipTransferHandler.promises.transferOwnership( + projectId, + toUserId, + { + allowTransferToNonCollaborators: sessionUser.isAdmin, + sessionUserId: ObjectId(sessionUser._id), + } + ) + res.sendStatus(204) + } catch (err) { + if (err instanceof Errors.ProjectNotFoundError) { + HttpErrorHandler.notFound(req, res, `project not found: ${projectId}`) + } else if (err instanceof Errors.UserNotFoundError) { + HttpErrorHandler.notFound(req, res, `user not found: ${toUserId}`) + } else if (err instanceof Errors.UserNotCollaboratorError) { + HttpErrorHandler.forbidden( + req, + res, + `user ${toUserId} should be a collaborator in project ${projectId} prior to ownership transfer` + ) + } else { + next(err) + } + } +} + +async function _removeUserIdFromProject(projectId, userId) { + await CollaboratorsHandler.promises.removeUserFromProject(projectId, userId) + EditorRealTimeController.emitToRoom( + projectId, + 'userRemovedFromProject', + userId + ) + await TagsHandler.promises.removeProjectFromAllTags(userId, projectId) +} diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsEmailHandler.js b/services/web/app/src/Features/Collaborators/CollaboratorsEmailHandler.js new file mode 100644 index 0000000000..4311532571 --- /dev/null +++ b/services/web/app/src/Features/Collaborators/CollaboratorsEmailHandler.js @@ -0,0 +1,47 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CollaboratorsEmailHandler +const { Project } = require('../../models/Project') +const EmailHandler = require('../Email/EmailHandler') +const Settings = require('@overleaf/settings') + +module.exports = CollaboratorsEmailHandler = { + _buildInviteUrl(project, invite) { + return ( + `${Settings.siteUrl}/project/${project._id}/invite/token/${invite.token}?` + + [ + `project_name=${encodeURIComponent(project.name)}`, + `user_first_name=${encodeURIComponent(project.owner_ref.first_name)}`, + ].join('&') + ) + }, + + notifyUserOfProjectInvite(project_id, email, invite, sendingUser, callback) { + return Project.findOne({ _id: project_id }) + .select('name owner_ref') + .populate('owner_ref') + .exec(function (err, project) { + const emailOptions = { + to: email, + replyTo: project.owner_ref.email, + project: { + name: project.name, + }, + inviteUrl: CollaboratorsEmailHandler._buildInviteUrl(project, invite), + owner: project.owner_ref, + sendingUser_id: sendingUser._id, + } + return EmailHandler.sendEmail('projectInvite', emailOptions, callback) + }) + }, +} diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsGetter.js b/services/web/app/src/Features/Collaborators/CollaboratorsGetter.js new file mode 100644 index 0000000000..3519045187 --- /dev/null +++ b/services/web/app/src/Features/Collaborators/CollaboratorsGetter.js @@ -0,0 +1,269 @@ +const { callbackify } = require('util') +const pLimit = require('p-limit') +const { ObjectId } = require('mongodb') +const OError = require('@overleaf/o-error') +const { Project } = require('../../models/Project') +const UserGetter = require('../User/UserGetter') +const ProjectGetter = require('../Project/ProjectGetter') +const PublicAccessLevels = require('../Authorization/PublicAccessLevels') +const Errors = require('../Errors/Errors') +const ProjectEditorHandler = require('../Project/ProjectEditorHandler') +const Sources = require('../Authorization/Sources') +const PrivilegeLevels = require('../Authorization/PrivilegeLevels') + +module.exports = { + getMemberIdsWithPrivilegeLevels: callbackify(getMemberIdsWithPrivilegeLevels), + getMemberIds: callbackify(getMemberIds), + getInvitedMemberIds: callbackify(getInvitedMemberIds), + getInvitedMembersWithPrivilegeLevels: callbackify( + getInvitedMembersWithPrivilegeLevels + ), + getInvitedMembersWithPrivilegeLevelsFromFields: callbackify( + getInvitedMembersWithPrivilegeLevelsFromFields + ), + getMemberIdPrivilegeLevel: callbackify(getMemberIdPrivilegeLevel), + getInvitedCollaboratorCount: callbackify(getInvitedCollaboratorCount), + getProjectsUserIsMemberOf: callbackify(getProjectsUserIsMemberOf), + isUserInvitedMemberOfProject: callbackify(isUserInvitedMemberOfProject), + userIsTokenMember: callbackify(userIsTokenMember), + getAllInvitedMembers: callbackify(getAllInvitedMembers), + promises: { + getMemberIdsWithPrivilegeLevels, + getMemberIds, + getInvitedMemberIds, + getInvitedMembersWithPrivilegeLevels, + getInvitedMembersWithPrivilegeLevelsFromFields, + getMemberIdPrivilegeLevel, + getInvitedCollaboratorCount, + getProjectsUserIsMemberOf, + isUserInvitedMemberOfProject, + userIsTokenMember, + getAllInvitedMembers, + }, +} + +async function getMemberIdsWithPrivilegeLevels(projectId) { + const project = await ProjectGetter.promises.getProject(projectId, { + owner_ref: 1, + collaberator_refs: 1, + readOnly_refs: 1, + tokenAccessReadOnly_refs: 1, + tokenAccessReadAndWrite_refs: 1, + publicAccesLevel: 1, + }) + if (!project) { + throw new Errors.NotFoundError(`no project found with id ${projectId}`) + } + const memberIds = _getMemberIdsWithPrivilegeLevelsFromFields( + project.owner_ref, + project.collaberator_refs, + project.readOnly_refs, + project.tokenAccessReadAndWrite_refs, + project.tokenAccessReadOnly_refs, + project.publicAccesLevel + ) + return memberIds +} + +async function getMemberIds(projectId) { + const members = await getMemberIdsWithPrivilegeLevels(projectId) + return members.map(m => m.id) +} + +async function getInvitedMemberIds(projectId) { + const members = await getMemberIdsWithPrivilegeLevels(projectId) + return members.filter(m => m.source !== Sources.TOKEN).map(m => m.id) +} + +async function getInvitedMembersWithPrivilegeLevels(projectId) { + let members = await getMemberIdsWithPrivilegeLevels(projectId) + members = members.filter(m => m.source !== Sources.TOKEN) + return _loadMembers(members) +} + +async function getInvitedMembersWithPrivilegeLevelsFromFields( + ownerId, + collaboratorIds, + readOnlyIds +) { + const members = _getMemberIdsWithPrivilegeLevelsFromFields( + ownerId, + collaboratorIds, + readOnlyIds, + [], + [], + null + ) + return _loadMembers(members) +} + +async function getMemberIdPrivilegeLevel(userId, projectId) { + // In future if the schema changes and getting all member ids is more expensive (multiple documents) + // then optimise this. + if (userId == null) { + return PrivilegeLevels.NONE + } + const members = await getMemberIdsWithPrivilegeLevels(projectId) + for (const member of members) { + if (member.id === userId.toString()) { + return member.privilegeLevel + } + } + return PrivilegeLevels.NONE +} + +async function getInvitedCollaboratorCount(projectId) { + const count = await _getInvitedMemberCount(projectId) + return count - 1 // Don't count project owner +} + +async function isUserInvitedMemberOfProject(userId, projectId) { + const members = await getMemberIdsWithPrivilegeLevels(projectId) + for (const member of members) { + if ( + member.id.toString() === userId.toString() && + member.source !== Sources.TOKEN + ) { + return true + } + } + return false +} + +async function getProjectsUserIsMemberOf(userId, fields) { + const limit = pLimit(2) + const [ + readAndWrite, + readOnly, + tokenReadAndWrite, + tokenReadOnly, + ] = await Promise.all([ + limit(() => Project.find({ collaberator_refs: userId }, fields).exec()), + limit(() => Project.find({ readOnly_refs: userId }, fields).exec()), + limit(() => + Project.find( + { + tokenAccessReadAndWrite_refs: userId, + publicAccesLevel: PublicAccessLevels.TOKEN_BASED, + }, + fields + ).exec() + ), + limit(() => + Project.find( + { + tokenAccessReadOnly_refs: userId, + publicAccesLevel: PublicAccessLevels.TOKEN_BASED, + }, + fields + ).exec() + ), + ]) + return { readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly } +} + +async function getAllInvitedMembers(projectId) { + try { + const rawMembers = await getInvitedMembersWithPrivilegeLevels(projectId) + const { members } = ProjectEditorHandler.buildOwnerAndMembersViews( + rawMembers + ) + return members + } catch (err) { + throw OError.tag(err, 'error getting members for project', { projectId }) + } +} + +async function userIsTokenMember(userId, projectId) { + userId = ObjectId(userId.toString()) + projectId = ObjectId(projectId.toString()) + const project = await Project.findOne( + { + _id: projectId, + $or: [ + { tokenAccessReadOnly_refs: userId }, + { tokenAccessReadAndWrite_refs: userId }, + ], + }, + { + _id: 1, + } + ).exec() + return project != null +} + +async function _getInvitedMemberCount(projectId) { + const members = await getMemberIdsWithPrivilegeLevels(projectId) + return members.filter(m => m.source !== Sources.TOKEN).length +} + +function _getMemberIdsWithPrivilegeLevelsFromFields( + ownerId, + collaboratorIds, + readOnlyIds, + tokenAccessIds, + tokenAccessReadOnlyIds, + publicAccessLevel +) { + const members = [] + members.push({ + id: ownerId.toString(), + privilegeLevel: PrivilegeLevels.OWNER, + source: Sources.OWNER, + }) + for (const memberId of collaboratorIds || []) { + members.push({ + id: memberId.toString(), + privilegeLevel: PrivilegeLevels.READ_AND_WRITE, + source: Sources.INVITE, + }) + } + for (const memberId of readOnlyIds || []) { + members.push({ + id: memberId.toString(), + privilegeLevel: PrivilegeLevels.READ_ONLY, + source: Sources.INVITE, + }) + } + if (publicAccessLevel === PublicAccessLevels.TOKEN_BASED) { + for (const memberId of tokenAccessIds || []) { + members.push({ + id: memberId.toString(), + privilegeLevel: PrivilegeLevels.READ_AND_WRITE, + source: Sources.TOKEN, + }) + } + for (const memberId of tokenAccessReadOnlyIds || []) { + members.push({ + id: memberId.toString(), + privilegeLevel: PrivilegeLevels.READ_ONLY, + source: Sources.TOKEN, + }) + } + } + return members +} + +async function _loadMembers(members) { + const limit = pLimit(3) + const results = await Promise.all( + members.map(member => + limit(async () => { + const user = await UserGetter.promises.getUser(member.id, { + _id: 1, + email: 1, + features: 1, + first_name: 1, + last_name: 1, + signUpDate: 1, + }) + if (user != null) { + return { user, privilegeLevel: member.privilegeLevel } + } else { + return null + } + }) + ) + ) + return results.filter(r => r != null) +} diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js b/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js new file mode 100644 index 0000000000..69e47259ce --- /dev/null +++ b/services/web/app/src/Features/Collaborators/CollaboratorsHandler.js @@ -0,0 +1,262 @@ +const { callbackify } = require('util') +const OError = require('@overleaf/o-error') +const { Project } = require('../../models/Project') +const ProjectGetter = require('../Project/ProjectGetter') +const ProjectHelper = require('../Project/ProjectHelper') +const logger = require('logger-sharelatex') +const ContactManager = require('../Contacts/ContactManager') +const PrivilegeLevels = require('../Authorization/PrivilegeLevels') +const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher') +const CollaboratorsGetter = require('./CollaboratorsGetter') +const Errors = require('../Errors/Errors') + +module.exports = { + userIsTokenMember: callbackify(userIsTokenMember), + removeUserFromProject: callbackify(removeUserFromProject), + removeUserFromAllProjects: callbackify(removeUserFromAllProjects), + addUserIdToProject: callbackify(addUserIdToProject), + transferProjects: callbackify(transferProjects), + promises: { + userIsTokenMember, + removeUserFromProject, + removeUserFromAllProjects, + addUserIdToProject, + transferProjects, + setCollaboratorPrivilegeLevel, + }, +} + +async function removeUserFromProject(projectId, userId) { + try { + const project = await Project.findOne({ _id: projectId }).exec() + + // Deal with the old type of boolean value for archived + // In order to clear it + if (typeof project.archived === 'boolean') { + let archived = ProjectHelper.calculateArchivedArray( + project, + userId, + 'ARCHIVE' + ) + + archived = archived.filter(id => id.toString() !== userId.toString()) + + await Project.updateOne( + { _id: projectId }, + { + $set: { archived: archived }, + $pull: { + collaberator_refs: userId, + readOnly_refs: userId, + tokenAccessReadOnly_refs: userId, + tokenAccessReadAndWrite_refs: userId, + trashed: userId, + }, + } + ) + } else { + await Project.updateOne( + { _id: projectId }, + { + $pull: { + collaberator_refs: userId, + readOnly_refs: userId, + tokenAccessReadOnly_refs: userId, + tokenAccessReadAndWrite_refs: userId, + archived: userId, + trashed: userId, + }, + } + ) + } + } catch (err) { + throw OError.tag(err, 'problem removing user from project collaborators', { + projectId, + userId, + }) + } +} + +async function removeUserFromAllProjects(userId) { + const { + readAndWrite, + readOnly, + tokenReadAndWrite, + tokenReadOnly, + } = await CollaboratorsGetter.promises.getProjectsUserIsMemberOf(userId, { + _id: 1, + }) + const allProjects = readAndWrite + .concat(readOnly) + .concat(tokenReadAndWrite) + .concat(tokenReadOnly) + for (const project of allProjects) { + await removeUserFromProject(project._id, userId) + } +} + +async function addUserIdToProject( + projectId, + addingUserId, + userId, + privilegeLevel +) { + const project = await ProjectGetter.promises.getProject(projectId, { + collaberator_refs: 1, + readOnly_refs: 1, + }) + let level + let existingUsers = project.collaberator_refs || [] + existingUsers = existingUsers.concat(project.readOnly_refs || []) + existingUsers = existingUsers.map(u => u.toString()) + if (existingUsers.includes(userId.toString())) { + return // User already in Project + } + if (privilegeLevel === PrivilegeLevels.READ_AND_WRITE) { + level = { collaberator_refs: userId } + logger.log({ privileges: 'readAndWrite', userId, projectId }, 'adding user') + } else if (privilegeLevel === PrivilegeLevels.READ_ONLY) { + level = { readOnly_refs: userId } + logger.log({ privileges: 'readOnly', userId, projectId }, 'adding user') + } else { + throw new Error(`unknown privilegeLevel: ${privilegeLevel}`) + } + + if (addingUserId) { + ContactManager.addContact(addingUserId, userId) + } + + await Project.updateOne({ _id: projectId }, { $addToSet: level }).exec() + + // Flush to TPDS in background to add files to collaborator's Dropbox + TpdsProjectFlusher.promises.flushProjectToTpds(projectId).catch(err => { + logger.error( + { err, projectId, userId }, + 'error flushing to TPDS after adding collaborator' + ) + }) +} + +async function transferProjects(fromUserId, toUserId) { + // Find all the projects this user is part of so we can flush them to TPDS + const projects = await Project.find( + { + $or: [ + { owner_ref: fromUserId }, + { collaberator_refs: fromUserId }, + { readOnly_refs: fromUserId }, + ], + }, + { _id: 1 } + ).exec() + const projectIds = projects.map(p => p._id) + logger.log({ projectIds, fromUserId, toUserId }, 'transferring projects') + + await Project.updateMany( + { owner_ref: fromUserId }, + { $set: { owner_ref: toUserId } } + ).exec() + + await Project.updateMany( + { collaberator_refs: fromUserId }, + { + $addToSet: { collaberator_refs: toUserId }, + } + ).exec() + await Project.updateMany( + { collaberator_refs: fromUserId }, + { + $pull: { collaberator_refs: fromUserId }, + } + ).exec() + + await Project.updateMany( + { readOnly_refs: fromUserId }, + { + $addToSet: { readOnly_refs: toUserId }, + } + ).exec() + await Project.updateMany( + { readOnly_refs: fromUserId }, + { + $pull: { readOnly_refs: fromUserId }, + } + ).exec() + + // Flush in background, no need to block on this + _flushProjects(projectIds).catch(err => { + logger.err( + { err, projectIds, fromUserId, toUserId }, + 'error flushing tranferred projects to TPDS' + ) + }) +} + +async function setCollaboratorPrivilegeLevel( + projectId, + userId, + privilegeLevel +) { + // Make sure we're only updating the project if the user is already a + // collaborator + const query = { + _id: projectId, + $or: [{ collaberator_refs: userId }, { readOnly_refs: userId }], + } + let update + switch (privilegeLevel) { + case PrivilegeLevels.READ_AND_WRITE: { + update = { + $pull: { readOnly_refs: userId }, + $addToSet: { collaberator_refs: userId }, + } + break + } + case PrivilegeLevels.READ_ONLY: { + update = { + $pull: { collaberator_refs: userId }, + $addToSet: { readOnly_refs: userId }, + } + break + } + default: { + throw new OError(`unknown privilege level: ${privilegeLevel}`) + } + } + const mongoResponse = await Project.updateOne(query, update).exec() + if (mongoResponse.n === 0) { + throw new Errors.NotFoundError('project or collaborator not found') + } +} + +async function userIsTokenMember(userId, projectId) { + if (!userId) { + return false + } + try { + const project = await Project.findOne( + { + _id: projectId, + $or: [ + { tokenAccessReadOnly_refs: userId }, + { tokenAccessReadAndWrite_refs: userId }, + ], + }, + { + _id: 1, + } + ) + return project != null + } catch (err) { + throw OError.tag(err, 'problem while checking if user is token member', { + userId, + projectId, + }) + } +} + +async function _flushProjects(projectIds) { + for (const projectId of projectIds) { + await TpdsProjectFlusher.promises.flushProjectToTpds(projectId) + } +} diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.js b/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.js new file mode 100644 index 0000000000..9a5383f5f7 --- /dev/null +++ b/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.js @@ -0,0 +1,392 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CollaboratorsInviteController +const OError = require('@overleaf/o-error') +const ProjectGetter = require('../Project/ProjectGetter') +const LimitationsManager = require('../Subscription/LimitationsManager') +const UserGetter = require('../User/UserGetter') +const CollaboratorsGetter = require('./CollaboratorsGetter') +const CollaboratorsInviteHandler = require('./CollaboratorsInviteHandler') +const logger = require('logger-sharelatex') +const Settings = require('@overleaf/settings') +const EmailHelper = require('../Helpers/EmailHelper') +const EditorRealTimeController = require('../Editor/EditorRealTimeController') +const AnalyticsManager = require('../Analytics/AnalyticsManager') +const SessionManager = require('../Authentication/SessionManager') +const rateLimiter = require('../../infrastructure/RateLimiter') + +module.exports = CollaboratorsInviteController = { + getAllInvites(req, res, next) { + const projectId = req.params.Project_id + logger.log({ projectId }, 'getting all active invites for project') + return CollaboratorsInviteHandler.getAllInvites( + projectId, + function (err, invites) { + if (err != null) { + OError.tag(err, 'error getting invites for project', { + projectId, + }) + return next(err) + } + return res.json({ invites }) + } + ) + }, + + _checkShouldInviteEmail(email, callback) { + if (callback == null) { + callback = function (err, shouldAllowInvite) {} + } + if (Settings.restrictInvitesToExistingAccounts === true) { + logger.log({ email }, 'checking if user exists with this email') + return UserGetter.getUserByAnyEmail( + email, + { _id: 1 }, + function (err, user) { + if (err != null) { + return callback(err) + } + const userExists = + user != null && (user != null ? user._id : undefined) != null + return callback(null, userExists) + } + ) + } else { + return callback(null, true) + } + }, + + _checkRateLimit(user_id, callback) { + if (callback == null) { + callback = function (error) {} + } + return LimitationsManager.allowedNumberOfCollaboratorsForUser( + user_id, + function (err, collabLimit) { + if (collabLimit == null) { + collabLimit = 1 + } + if (err != null) { + return callback(err) + } + if (collabLimit === -1) { + collabLimit = 20 + } + collabLimit = collabLimit * 10 + const opts = { + endpointName: 'invite-to-project-by-user-id', + timeInterval: 60 * 30, + subjectName: user_id, + throttle: collabLimit, + } + return rateLimiter.addCount(opts, callback) + } + ) + }, + + inviteToProject(req, res, next) { + const projectId = req.params.Project_id + let { email } = req.body + const sendingUser = SessionManager.getSessionUser(req.session) + const sendingUserId = sendingUser._id + if (email === sendingUser.email) { + logger.log( + { projectId, email, sendingUserId }, + 'cannot invite yourself to project' + ) + return res.json({ invite: null, error: 'cannot_invite_self' }) + } + logger.log({ projectId, email, sendingUserId }, 'inviting to project') + return LimitationsManager.canAddXCollaborators( + projectId, + 1, + (error, allowed) => { + let privileges + if (error != null) { + return next(error) + } + if (!allowed) { + logger.log( + { projectId, email, sendingUserId }, + 'not allowed to invite more users to project' + ) + return res.json({ invite: null }) + } + ;({ email, privileges } = req.body) + email = EmailHelper.parseEmail(email) + if (email == null || email === '') { + logger.log( + { projectId, email, sendingUserId }, + 'invalid email address' + ) + return res.status(400).send({ errorReason: 'invalid_email' }) + } + return CollaboratorsInviteController._checkRateLimit( + sendingUserId, + function (error, underRateLimit) { + if (error != null) { + return next(error) + } + if (!underRateLimit) { + return res.sendStatus(429) + } + return CollaboratorsInviteController._checkShouldInviteEmail( + email, + function (err, shouldAllowInvite) { + if (err != null) { + OError.tag( + err, + 'error checking if we can invite this email address', + { + email, + projectId, + sendingUserId, + } + ) + return next(err) + } + if (!shouldAllowInvite) { + logger.log( + { email, projectId, sendingUserId }, + 'not allowed to send an invite to this email address' + ) + return res.json({ + invite: null, + error: 'cannot_invite_non_user', + }) + } + return CollaboratorsInviteHandler.inviteToProject( + projectId, + sendingUser, + email, + privileges, + function (err, invite) { + if (err != null) { + OError.tag(err, 'error creating project invite', { + projectId, + email, + sendingUserId, + }) + return next(err) + } + logger.log( + { projectId, email, sendingUserId }, + 'invite created' + ) + EditorRealTimeController.emitToRoom( + projectId, + 'project:membership:changed', + { invites: true } + ) + return res.json({ invite }) + } + ) + } + ) + } + ) + } + ) + }, + + revokeInvite(req, res, next) { + const projectId = req.params.Project_id + const inviteId = req.params.invite_id + logger.log({ projectId, inviteId }, 'revoking invite') + return CollaboratorsInviteHandler.revokeInvite( + projectId, + inviteId, + function (err) { + if (err != null) { + OError.tag(err, 'error revoking invite', { + projectId, + inviteId, + }) + return next(err) + } + EditorRealTimeController.emitToRoom( + projectId, + 'project:membership:changed', + { invites: true } + ) + return res.sendStatus(201) + } + ) + }, + + resendInvite(req, res, next) { + const projectId = req.params.Project_id + const inviteId = req.params.invite_id + logger.log({ projectId, inviteId }, 'resending invite') + const sendingUser = SessionManager.getSessionUser(req.session) + return CollaboratorsInviteController._checkRateLimit( + sendingUser._id, + function (error, underRateLimit) { + if (error != null) { + return next(error) + } + if (!underRateLimit) { + return res.sendStatus(429) + } + return CollaboratorsInviteHandler.resendInvite( + projectId, + sendingUser, + inviteId, + function (err) { + if (err != null) { + OError.tag(err, 'error resending invite', { + projectId, + inviteId, + }) + return next(err) + } + return res.sendStatus(201) + } + ) + } + ) + }, + + viewInvite(req, res, next) { + const projectId = req.params.Project_id + const { token } = req.params + const _renderInvalidPage = function () { + logger.log( + { projectId, token }, + 'invite not valid, rendering not-valid page' + ) + return res.render('project/invite/not-valid', { title: 'Invalid Invite' }) + } + // check if the user is already a member of the project + const currentUser = SessionManager.getSessionUser(req.session) + return CollaboratorsGetter.isUserInvitedMemberOfProject( + currentUser._id, + projectId, + function (err, isMember) { + if (err != null) { + OError.tag(err, 'error checking if user is member of project', { + projectId, + }) + return next(err) + } + if (isMember) { + logger.log( + { projectId, userId: currentUser._id }, + 'user is already a member of this project, redirecting' + ) + return res.redirect(`/project/${projectId}`) + } + // get the invite + return CollaboratorsInviteHandler.getInviteByToken( + projectId, + token, + function (err, invite) { + if (err != null) { + OError.tag(err, 'error getting invite by token', { + projectId, + token, + }) + return next(err) + } + // check if invite is gone, or otherwise non-existent + if (invite == null) { + logger.log({ projectId, token }, 'no invite found for this token') + return _renderInvalidPage() + } + // check the user who sent the invite exists + return UserGetter.getUser( + { _id: invite.sendingUserId }, + { email: 1, first_name: 1, last_name: 1 }, + function (err, owner) { + if (err != null) { + OError.tag(err, 'error getting project owner', { + projectId, + }) + return next(err) + } + if (owner == null) { + logger.log({ projectId }, 'no project owner found') + return _renderInvalidPage() + } + // fetch the project name + return ProjectGetter.getProject( + projectId, + {}, + function (err, project) { + if (err != null) { + OError.tag(err, 'error getting project', { + projectId, + }) + return next(err) + } + if (project == null) { + logger.log({ projectId }, 'no project found') + return _renderInvalidPage() + } + // finally render the invite + return res.render('project/invite/show', { + invite, + project, + owner, + title: 'Project Invite', + }) + } + ) + } + ) + } + ) + } + ) + }, + + acceptInvite(req, res, next) { + const projectId = req.params.Project_id + const { token } = req.params + const currentUser = SessionManager.getSessionUser(req.session) + logger.log( + { projectId, userId: currentUser._id, token }, + 'got request to accept invite' + ) + return CollaboratorsInviteHandler.acceptInvite( + projectId, + token, + currentUser, + function (err) { + if (err != null) { + OError.tag(err, 'error accepting invite by token', { + projectId, + token, + }) + return next(err) + } + EditorRealTimeController.emitToRoom( + projectId, + 'project:membership:changed', + { invites: true, members: true } + ) + AnalyticsManager.recordEvent(currentUser._id, 'project-invite-accept', { + projectId, + userId: currentUser._id, + }) + if (req.xhr) { + return res.sendStatus(204) // Done async via project page notification + } else { + return res.redirect(`/project/${projectId}`) + } + } + ) + }, +} diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.js b/services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.js new file mode 100644 index 0000000000..4f10a46fb9 --- /dev/null +++ b/services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.js @@ -0,0 +1,361 @@ +/* eslint-disable + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { ProjectInvite } = require('../../models/ProjectInvite') +const OError = require('@overleaf/o-error') +const logger = require('logger-sharelatex') +const CollaboratorsEmailHandler = require('./CollaboratorsEmailHandler') +const CollaboratorsHandler = require('./CollaboratorsHandler') +const UserGetter = require('../User/UserGetter') +const ProjectGetter = require('../Project/ProjectGetter') +const Errors = require('../Errors/Errors') +const Crypto = require('crypto') +const NotificationsBuilder = require('../Notifications/NotificationsBuilder') +const { promisifyAll } = require('../../util/promises') + +const CollaboratorsInviteHandler = { + getAllInvites(projectId, callback) { + if (callback == null) { + callback = function (err, invites) {} + } + logger.log({ projectId }, 'fetching invites for project') + return ProjectInvite.find({ projectId }, function (err, invites) { + if (err != null) { + OError.tag(err, 'error getting invites from mongo', { + projectId, + }) + return callback(err) + } + logger.log( + { projectId, count: invites.length }, + 'found invites for project' + ) + return callback(null, invites) + }) + }, + + getInviteCount(projectId, callback) { + if (callback == null) { + callback = function (err, count) {} + } + logger.log({ projectId }, 'counting invites for project') + return ProjectInvite.countDocuments({ projectId }, function (err, count) { + if (err != null) { + OError.tag(err, 'error getting invites from mongo', { + projectId, + }) + return callback(err) + } + return callback(null, count) + }) + }, + + _trySendInviteNotification(projectId, sendingUser, invite, callback) { + if (callback == null) { + callback = function (err) {} + } + const { email } = invite + return UserGetter.getUserByAnyEmail( + email, + { _id: 1 }, + function (err, existingUser) { + if (err != null) { + OError.tag(err, 'error checking if user exists', { + projectId, + email, + }) + return callback(err) + } + if (existingUser == null) { + logger.log({ projectId, email }, 'no existing user found, returning') + return callback(null) + } + return ProjectGetter.getProject( + projectId, + { _id: 1, name: 1 }, + function (err, project) { + if (err != null) { + OError.tag(err, 'error getting project', { + projectId, + email, + }) + return callback(err) + } + if (project == null) { + logger.log( + { projectId }, + 'no project found while sending notification, returning' + ) + return callback(null) + } + return NotificationsBuilder.projectInvite( + invite, + project, + sendingUser, + existingUser + ).create(callback) + } + ) + } + ) + }, + + _tryCancelInviteNotification(inviteId, callback) { + if (callback == null) { + callback = function () {} + } + return NotificationsBuilder.projectInvite( + { _id: inviteId }, + null, + null, + null + ).read(callback) + }, + + _sendMessages(projectId, sendingUser, invite, callback) { + if (callback == null) { + callback = function (err) {} + } + logger.log( + { projectId, inviteId: invite._id }, + 'sending notification and email for invite' + ) + return CollaboratorsEmailHandler.notifyUserOfProjectInvite( + projectId, + invite.email, + invite, + sendingUser, + function (err) { + if (err != null) { + return callback(err) + } + return CollaboratorsInviteHandler._trySendInviteNotification( + projectId, + sendingUser, + invite, + function (err) { + if (err != null) { + return callback(err) + } + return callback() + } + ) + } + ) + }, + + inviteToProject(projectId, sendingUser, email, privileges, callback) { + if (callback == null) { + callback = function (err, invite) {} + } + logger.log( + { projectId, sendingUserId: sendingUser._id, email, privileges }, + 'adding invite' + ) + return Crypto.randomBytes(24, function (err, buffer) { + if (err != null) { + OError.tag(err, 'error generating random token', { + projectId, + sendingUserId: sendingUser._id, + email, + }) + return callback(err) + } + const token = buffer.toString('hex') + const invite = new ProjectInvite({ + email, + token, + sendingUserId: sendingUser._id, + projectId, + privileges, + }) + return invite.save(function (err, invite) { + if (err != null) { + OError.tag(err, 'error saving token', { + projectId, + sendingUserId: sendingUser._id, + email, + }) + return callback(err) + } + // Send email and notification in background + CollaboratorsInviteHandler._sendMessages( + projectId, + sendingUser, + invite, + function (err) { + if (err != null) { + return logger.err( + { err, projectId, email }, + 'error sending messages for invite' + ) + } + } + ) + return callback(null, invite) + }) + }) + }, + + revokeInvite(projectId, inviteId, callback) { + if (callback == null) { + callback = function (err) {} + } + logger.log({ projectId, inviteId }, 'removing invite') + return ProjectInvite.deleteOne( + { projectId, _id: inviteId }, + function (err) { + if (err != null) { + OError.tag(err, 'error removing invite', { + projectId, + inviteId, + }) + return callback(err) + } + CollaboratorsInviteHandler._tryCancelInviteNotification( + inviteId, + function () {} + ) + return callback(null) + } + ) + }, + + resendInvite(projectId, sendingUser, inviteId, callback) { + if (callback == null) { + callback = function (err) {} + } + logger.log({ projectId, inviteId }, 'resending invite email') + return ProjectInvite.findOne( + { _id: inviteId, projectId }, + function (err, invite) { + if (err != null) { + OError.tag(err, 'error finding invite', { + projectId, + inviteId, + }) + return callback(err) + } + if (invite == null) { + logger.err( + { err, projectId, inviteId }, + 'no invite found, nothing to resend' + ) + return callback(null) + } + return CollaboratorsInviteHandler._sendMessages( + projectId, + sendingUser, + invite, + function (err) { + if (err != null) { + OError.tag(err, 'error resending invite messages', { + projectId, + inviteId, + }) + return callback(err) + } + return callback(null) + } + ) + } + ) + }, + + getInviteByToken(projectId, tokenString, callback) { + if (callback == null) { + callback = function (err, invite) {} + } + logger.log({ projectId, tokenString }, 'fetching invite by token') + return ProjectInvite.findOne( + { projectId, token: tokenString }, + function (err, invite) { + if (err != null) { + OError.tag(err, 'error fetching invite', { + projectId, + }) + return callback(err) + } + if (invite == null) { + logger.err({ err, projectId, token: tokenString }, 'no invite found') + return callback(null, null) + } + return callback(null, invite) + } + ) + }, + + acceptInvite(projectId, tokenString, user, callback) { + if (callback == null) { + callback = function (err) {} + } + logger.log({ projectId, userId: user._id, tokenString }, 'accepting invite') + return CollaboratorsInviteHandler.getInviteByToken( + projectId, + tokenString, + function (err, invite) { + if (err != null) { + OError.tag(err, 'error finding invite', { + projectId, + tokenString, + }) + return callback(err) + } + if (!invite) { + err = new Errors.NotFoundError('no matching invite found') + logger.log( + { err, projectId, tokenString }, + 'no matching invite found' + ) + return callback(err) + } + const inviteId = invite._id + return CollaboratorsHandler.addUserIdToProject( + projectId, + invite.sendingUserId, + user._id, + invite.privileges, + function (err) { + if (err != null) { + OError.tag(err, 'error adding user to project', { + projectId, + inviteId, + userId: user._id, + }) + return callback(err) + } + // Remove invite + logger.log({ projectId, inviteId }, 'removing invite') + return ProjectInvite.deleteOne({ _id: inviteId }, function (err) { + if (err != null) { + OError.tag(err, 'error removing invite', { + projectId, + inviteId, + }) + return callback(err) + } + CollaboratorsInviteHandler._tryCancelInviteNotification( + inviteId, + function () {} + ) + return callback() + }) + } + ) + } + ) + }, +} + +module.exports = CollaboratorsInviteHandler +module.exports.promises = promisifyAll(CollaboratorsInviteHandler) diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsRouter.js b/services/web/app/src/Features/Collaborators/CollaboratorsRouter.js new file mode 100644 index 0000000000..1e3682e804 --- /dev/null +++ b/services/web/app/src/Features/Collaborators/CollaboratorsRouter.js @@ -0,0 +1,130 @@ +const CollaboratorsController = require('./CollaboratorsController') +const AuthenticationController = require('../Authentication/AuthenticationController') +const AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware') +const PrivilegeLevels = require('../Authorization/PrivilegeLevels') +const CollaboratorsInviteController = require('./CollaboratorsInviteController') +const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') +const CaptchaMiddleware = require('../Captcha/CaptchaMiddleware') +const AnalyticsRegistrationSourceMiddleware = require('../Analytics/AnalyticsRegistrationSourceMiddleware') +const { Joi, validate } = require('../../infrastructure/Validation') + +module.exports = { + apply(webRouter, apiRouter) { + webRouter.post( + '/project/:Project_id/leave', + AuthenticationController.requireLogin(), + CollaboratorsController.removeSelfFromProject + ) + + webRouter.put( + '/project/:Project_id/users/:user_id', + AuthenticationController.requireLogin(), + validate({ + params: Joi.object({ + Project_id: Joi.objectId(), + user_id: Joi.objectId(), + }), + body: Joi.object({ + privilegeLevel: Joi.string() + .valid(PrivilegeLevels.READ_ONLY, PrivilegeLevels.READ_AND_WRITE) + .required(), + }), + }), + AuthorizationMiddleware.ensureUserCanAdminProject, + CollaboratorsController.setCollaboratorInfo + ) + + webRouter.delete( + '/project/:Project_id/users/:user_id', + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanAdminProject, + CollaboratorsController.removeUserFromProject + ) + + webRouter.get( + '/project/:Project_id/members', + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanAdminProject, + CollaboratorsController.getAllMembers + ) + + webRouter.post( + '/project/:Project_id/transfer-ownership', + AuthenticationController.requireLogin(), + validate({ + params: Joi.object({ + Project_id: Joi.objectId(), + }), + body: Joi.object({ + user_id: Joi.objectId(), + }), + }), + AuthorizationMiddleware.ensureUserCanAdminProject, + CollaboratorsController.transferOwnership + ) + + // invites + webRouter.post( + '/project/:Project_id/invite', + RateLimiterMiddleware.rateLimit({ + endpointName: 'invite-to-project-by-project-id', + params: ['Project_id'], + maxRequests: 100, + timeInterval: 60 * 10, + }), + RateLimiterMiddleware.rateLimit({ + endpointName: 'invite-to-project-by-ip', + ipOnly: true, + maxRequests: 100, + timeInterval: 60 * 10, + }), + CaptchaMiddleware.validateCaptcha('invite'), + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanAdminProject, + CollaboratorsInviteController.inviteToProject + ) + + webRouter.get( + '/project/:Project_id/invites', + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanAdminProject, + CollaboratorsInviteController.getAllInvites + ) + + webRouter.delete( + '/project/:Project_id/invite/:invite_id', + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanAdminProject, + CollaboratorsInviteController.revokeInvite + ) + + webRouter.post( + '/project/:Project_id/invite/:invite_id/resend', + RateLimiterMiddleware.rateLimit({ + endpointName: 'resend-invite', + params: ['Project_id'], + maxRequests: 200, + timeInterval: 60 * 10, + }), + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanAdminProject, + CollaboratorsInviteController.resendInvite + ) + + webRouter.get( + '/project/:Project_id/invite/token/:token', + AnalyticsRegistrationSourceMiddleware.setSource('project-invite'), + AuthenticationController.requireLogin(), + CollaboratorsInviteController.viewInvite, + AnalyticsRegistrationSourceMiddleware.clearSource() + ) + + webRouter.post( + '/project/:Project_id/invite/token/:token/accept', + AnalyticsRegistrationSourceMiddleware.setSource('project-invite'), + AuthenticationController.requireLogin(), + CollaboratorsInviteController.acceptInvite, + AnalyticsRegistrationSourceMiddleware.clearSource() + ) + }, +} diff --git a/services/web/app/src/Features/Collaborators/OwnershipTransferHandler.js b/services/web/app/src/Features/Collaborators/OwnershipTransferHandler.js new file mode 100644 index 0000000000..bf31664b70 --- /dev/null +++ b/services/web/app/src/Features/Collaborators/OwnershipTransferHandler.js @@ -0,0 +1,124 @@ +const logger = require('logger-sharelatex') +const { Project } = require('../../models/Project') +const ProjectGetter = require('../Project/ProjectGetter') +const UserGetter = require('../User/UserGetter') +const CollaboratorsHandler = require('./CollaboratorsHandler') +const EmailHandler = require('../Email/EmailHandler') +const Errors = require('../Errors/Errors') +const PrivilegeLevels = require('../Authorization/PrivilegeLevels') +const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher') +const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler') + +module.exports = { + promises: { transferOwnership }, +} + +async function transferOwnership(projectId, newOwnerId, options = {}) { + const { allowTransferToNonCollaborators, sessionUserId } = options + + // Fetch project and user + const [project, newOwner] = await Promise.all([ + _getProject(projectId), + _getUser(newOwnerId), + ]) + + // Exit early if the transferee is already the project owner + const previousOwnerId = project.owner_ref + if (previousOwnerId.equals(newOwnerId)) { + return + } + + // Check that user is already a collaborator + if ( + !allowTransferToNonCollaborators && + !_userIsCollaborator(newOwner, project) + ) { + throw new Errors.UserNotCollaboratorError({ info: { userId: newOwnerId } }) + } + + // Transfer ownership + await ProjectAuditLogHandler.promises.addEntry( + projectId, + 'transfer-ownership', + sessionUserId, + { previousOwnerId, newOwnerId } + ) + await _transferOwnership(projectId, previousOwnerId, newOwnerId) + + // Flush project to TPDS + await TpdsProjectFlusher.promises.flushProjectToTpds(projectId) + + // Send confirmation emails + const previousOwner = await UserGetter.promises.getUser(previousOwnerId) + await _sendEmails(project, previousOwner, newOwner) +} + +async function _getProject(projectId) { + const project = await ProjectGetter.promises.getProject(projectId, { + owner_ref: 1, + collaberator_refs: 1, + name: 1, + }) + if (project == null) { + throw new Errors.ProjectNotFoundError({ info: { projectId } }) + } + return project +} + +async function _getUser(userId) { + const user = await UserGetter.promises.getUser(userId) + if (user == null) { + throw new Errors.UserNotFoundError({ info: { userId } }) + } + return user +} + +function _userIsCollaborator(user, project) { + const collaboratorIds = project.collaberator_refs || [] + return collaboratorIds.some(collaboratorId => collaboratorId.equals(user._id)) +} + +async function _transferOwnership(projectId, previousOwnerId, newOwnerId) { + await CollaboratorsHandler.promises.removeUserFromProject( + projectId, + newOwnerId + ) + await Project.updateOne( + { _id: projectId }, + { $set: { owner_ref: newOwnerId } } + ).exec() + await CollaboratorsHandler.promises.addUserIdToProject( + projectId, + newOwnerId, + previousOwnerId, + PrivilegeLevels.READ_AND_WRITE + ) +} + +async function _sendEmails(project, previousOwner, newOwner) { + if (previousOwner == null) { + // The previous owner didn't exist. This is not supposed to happen, but + // since we're changing the owner anyway, we'll just warn + logger.warn( + { projectId: project._id, ownerId: previousOwner._id }, + 'Project owner did not exist before ownership transfer' + ) + } else { + // Send confirmation emails + await Promise.all([ + EmailHandler.promises.sendEmail( + 'ownershipTransferConfirmationPreviousOwner', + { + to: previousOwner.email, + project, + newOwner, + } + ), + EmailHandler.promises.sendEmail('ownershipTransferConfirmationNewOwner', { + to: newOwner.email, + project, + previousOwner, + }), + ]) + } +} diff --git a/services/web/app/src/Features/Compile/ClsiCookieManager.js b/services/web/app/src/Features/Compile/ClsiCookieManager.js new file mode 100644 index 0000000000..bdfe0b3d85 --- /dev/null +++ b/services/web/app/src/Features/Compile/ClsiCookieManager.js @@ -0,0 +1,158 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let rclient_secondary +const OError = require('@overleaf/o-error') +const Settings = require('@overleaf/settings') +const request = require('request').defaults({ timeout: 30 * 1000 }) +const RedisWrapper = require('../../infrastructure/RedisWrapper') +const rclient = RedisWrapper.client('clsi_cookie') +if (Settings.redis.clsi_cookie_secondary != null) { + rclient_secondary = RedisWrapper.client('clsi_cookie_secondary') +} +const Cookie = require('cookie') +const logger = require('logger-sharelatex') + +const clsiCookiesEnabled = + (Settings.clsiCookie != null ? Settings.clsiCookie.key : undefined) != null && + Settings.clsiCookie.key.length !== 0 + +module.exports = function (backendGroup) { + return { + buildKey(project_id) { + if (backendGroup != null) { + return `clsiserver:${backendGroup}:${project_id}` + } else { + return `clsiserver:${project_id}` + } + }, + + _getServerId(project_id, callback) { + if (callback == null) { + callback = function (err, serverId) {} + } + return rclient.get(this.buildKey(project_id), (err, serverId) => { + if (err != null) { + return callback(err) + } + if (serverId == null || serverId === '') { + return this._populateServerIdViaRequest(project_id, callback) + } else { + return callback(null, serverId) + } + }) + }, + + _populateServerIdViaRequest(project_id, callback) { + if (callback == null) { + callback = function (err, serverId) {} + } + const url = `${Settings.apis.clsi.url}/project/${project_id}/status` + return request.post(url, (err, res, body) => { + if (err != null) { + OError.tag(err, 'error getting initial server id for project', { + project_id, + }) + return callback(err) + } + return this.setServerId(project_id, res, function (err, serverId) { + if (err != null) { + logger.warn( + { err, project_id }, + 'error setting server id via populate request' + ) + } + return callback(err, serverId) + }) + }) + }, + + _parseServerIdFromResponse(response) { + const cookies = Cookie.parse( + (response.headers['set-cookie'] != null + ? response.headers['set-cookie'][0] + : undefined) || '' + ) + return cookies != null ? cookies[Settings.clsiCookie.key] : undefined + }, + + setServerId(project_id, response, callback) { + if (callback == null) { + callback = function (err, serverId) {} + } + if (!clsiCookiesEnabled) { + return callback() + } + const serverId = this._parseServerIdFromResponse(response) + if (serverId == null) { + // We don't get a cookie back if it hasn't changed + return rclient.expire( + this.buildKey(project_id), + Settings.clsiCookie.ttl, + err => callback(err, undefined) + ) + } + if (rclient_secondary != null) { + this._setServerIdInRedis(rclient_secondary, project_id, serverId) + } + return this._setServerIdInRedis(rclient, project_id, serverId, err => + callback(err, serverId) + ) + }, + + _setServerIdInRedis(rclient, project_id, serverId, callback) { + if (callback == null) { + callback = function (err) {} + } + rclient.setex( + this.buildKey(project_id), + Settings.clsiCookie.ttl, + serverId, + callback + ) + }, + + clearServerId(project_id, callback) { + if (callback == null) { + callback = function (err) {} + } + if (!clsiCookiesEnabled) { + return callback() + } + return rclient.del(this.buildKey(project_id), callback) + }, + + getCookieJar(project_id, callback) { + if (callback == null) { + callback = function (err, jar, clsiServerId) {} + } + if (!clsiCookiesEnabled) { + return callback(null, request.jar(), undefined) + } + return this._getServerId(project_id, (err, serverId) => { + if (err != null) { + OError.tag(err, 'error getting server id', { + project_id, + }) + return callback(err) + } + const serverCookie = request.cookie( + `${Settings.clsiCookie.key}=${serverId}` + ) + const jar = request.jar() + jar.setCookie(serverCookie, Settings.apis.clsi.url) + return callback(null, jar, serverId) + }) + }, + } +} diff --git a/services/web/app/src/Features/Compile/ClsiFormatChecker.js b/services/web/app/src/Features/Compile/ClsiFormatChecker.js new file mode 100644 index 0000000000..cc91a2a32b --- /dev/null +++ b/services/web/app/src/Features/Compile/ClsiFormatChecker.js @@ -0,0 +1,86 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ClsiFormatChecker +const _ = require('lodash') +const async = require('async') +const settings = require('@overleaf/settings') + +module.exports = ClsiFormatChecker = { + checkRecoursesForProblems(resources, callback) { + const jobs = { + conflictedPaths(cb) { + return ClsiFormatChecker._checkForConflictingPaths(resources, cb) + }, + + sizeCheck(cb) { + return ClsiFormatChecker._checkDocsAreUnderSizeLimit(resources, cb) + }, + } + + return async.series(jobs, function (err, problems) { + if (err != null) { + return callback(err) + } + + problems = _.omitBy(problems, _.isEmpty) + + if (_.isEmpty(problems)) { + return callback() + } else { + return callback(null, problems) + } + }) + }, + + _checkForConflictingPaths(resources, callback) { + const paths = resources.map(resource => resource.path) + + const conflicts = _.filter(paths, function (path) { + const matchingPaths = _.filter( + paths, + checkPath => checkPath.indexOf(path + '/') !== -1 + ) + + return matchingPaths.length > 0 + }) + + const conflictObjects = conflicts.map(conflict => ({ path: conflict })) + + return callback(null, conflictObjects) + }, + + _checkDocsAreUnderSizeLimit(resources, callback) { + const sizeLimit = 1000 * 1000 * settings.compileBodySizeLimitMb + + let totalSize = 0 + + let sizedResources = resources.map(function (resource) { + const result = { path: resource.path } + if (resource.content != null) { + result.size = resource.content.replace(/\n/g, '').length + result.kbSize = Math.ceil(result.size / 1000) + } else { + result.size = 0 + } + totalSize += result.size + return result + }) + + const tooLarge = totalSize > sizeLimit + if (!tooLarge) { + return callback() + } else { + sizedResources = _.sortBy(sizedResources, 'size').reverse().slice(0, 10) + return callback(null, { resources: sizedResources, totalSize }) + } + }, +} diff --git a/services/web/app/src/Features/Compile/ClsiManager.js b/services/web/app/src/Features/Compile/ClsiManager.js new file mode 100644 index 0000000000..f7e9b37531 --- /dev/null +++ b/services/web/app/src/Features/Compile/ClsiManager.js @@ -0,0 +1,907 @@ +const async = require('async') +const Settings = require('@overleaf/settings') +const request = require('request') +const ProjectGetter = require('../Project/ProjectGetter') +const ProjectEntityHandler = require('../Project/ProjectEntityHandler') +const logger = require('logger-sharelatex') +const Url = require('url') +const OError = require('@overleaf/o-error') + +const ClsiCookieManager = require('./ClsiCookieManager')( + Settings.apis.clsi != null ? Settings.apis.clsi.backendGroupName : undefined +) +const NewBackendCloudClsiCookieManager = require('./ClsiCookieManager')( + Settings.apis.clsi_new != null + ? Settings.apis.clsi_new.backendGroupName + : undefined +) +const ClsiStateManager = require('./ClsiStateManager') +const _ = require('underscore') +const ClsiFormatChecker = require('./ClsiFormatChecker') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const Metrics = require('@overleaf/metrics') +const Errors = require('../Errors/Errors') + +const VALID_COMPILERS = ['pdflatex', 'latex', 'xelatex', 'lualatex'] + +const ClsiManager = { + sendRequest(projectId, userId, options, callback) { + if (options == null) { + options = {} + } + ClsiManager.sendRequestOnce( + projectId, + userId, + options, + (err, status, ...result) => { + if (err != null) { + return callback(err) + } + if (status === 'conflict') { + // Try again, with a full compile + return ClsiManager.sendRequestOnce( + projectId, + userId, + { ...options, syncType: 'full' }, + callback + ) + } else if (status === 'unavailable') { + return ClsiManager.sendRequestOnce( + projectId, + userId, + { ...options, syncType: 'full', forceNewClsiServer: true }, + callback + ) + } + callback(null, status, ...result) + } + ) + }, + + sendRequestOnce(projectId, userId, options, callback) { + if (options == null) { + options = {} + } + ClsiManager._buildRequest(projectId, options, (err, req) => { + if (err != null) { + if (err.message === 'no main file specified') { + return callback(null, 'validation-problems', null, null, { + mainFile: err.message, + }) + } else { + return callback( + OError.tag(err, 'Could not build request to CLSI', { + projectId, + options, + }) + ) + } + } + ClsiManager._sendBuiltRequest( + projectId, + userId, + req, + options, + (err, status, ...result) => { + if (err != null) { + return callback( + OError.tag(err, 'CLSI compile failed', { projectId, userId }) + ) + } + callback(null, status, ...result) + } + ) + }) + }, + + // for public API requests where there is no project id + sendExternalRequest(submissionId, clsiRequest, options, callback) { + if (options == null) { + options = {} + } + ClsiManager._sendBuiltRequest( + submissionId, + null, + clsiRequest, + options, + (err, status, ...result) => { + if (err != null) { + return callback( + OError.tag(err, 'CLSI compile failed', { + submissionId, + options, + }) + ) + } + callback(null, status, ...result) + } + ) + }, + + stopCompile(projectId, userId, options, callback) { + if (options == null) { + options = {} + } + const compilerUrl = this._getCompilerUrl( + options.compileGroup, + projectId, + userId, + 'compile/stop' + ) + const opts = { + url: compilerUrl, + method: 'POST', + } + ClsiManager._makeRequest(projectId, opts, callback) + }, + + deleteAuxFiles(projectId, userId, options, clsiserverid, callback) { + if (options == null) { + options = {} + } + const compilerUrl = this._getCompilerUrl( + options.compileGroup, + projectId, + userId + ) + const opts = { + url: compilerUrl, + method: 'DELETE', + } + ClsiManager._makeRequestWithClsiServerId( + projectId, + opts, + clsiserverid, + clsiErr => { + // always clear the project state from the docupdater, even if there + // was a problem with the request to the clsi + DocumentUpdaterHandler.clearProjectState(projectId, docUpdaterErr => { + ClsiCookieManager.clearServerId(projectId, redisError => { + if (clsiErr) { + return callback( + OError.tag(clsiErr, 'Failed to delete aux files', { projectId }) + ) + } + if (docUpdaterErr) { + return callback( + OError.tag( + docUpdaterErr, + 'Failed to clear project state in doc updater', + { projectId } + ) + ) + } + if (redisError) { + // redis errors need wrapping as the instance may be shared + return callback( + OError( + 'Failed to clear clsi persistence', + { projectId }, + redisError + ) + ) + } + callback() + }) + }) + } + ) + }, + + _sendBuiltRequest(projectId, userId, req, options, callback) { + if (options == null) { + options = {} + } + if (options.forceNewClsiServer) { + // Clear clsi cookie, then try again + return ClsiCookieManager.clearServerId(projectId, err => { + if (err) { + return callback(err) + } + options.forceNewClsiServer = false // backend has now been reset + return ClsiManager._sendBuiltRequest( + projectId, + userId, + req, + options, + callback + ) + }) + } + ClsiFormatChecker.checkRecoursesForProblems( + req.compile != null ? req.compile.resources : undefined, + (err, validationProblems) => { + if (err != null) { + return callback( + OError.tag( + err, + 'could not check resources for potential problems before sending to clsi' + ) + ) + } + if (validationProblems != null) { + logger.log( + { projectId, validationProblems }, + 'problems with users latex before compile was attempted' + ) + return callback( + null, + 'validation-problems', + null, + null, + validationProblems + ) + } + ClsiManager._postToClsi( + projectId, + userId, + req, + options.compileGroup, + (err, response, clsiServerId) => { + if (err != null) { + return callback( + OError.tag(err, 'error sending request to clsi', { + projectId, + userId, + }) + ) + } + const outputFiles = ClsiManager._parseOutputFiles( + projectId, + response && response.compile && response.compile.outputFiles + ) + const compile = (response && response.compile) || {} + const status = compile.status + const stats = compile.stats + const timings = compile.timings + const validationProblems = undefined + callback( + null, + status, + outputFiles, + clsiServerId, + validationProblems, + stats, + timings + ) + } + ) + } + ) + }, + + _makeRequestWithClsiServerId(projectId, opts, clsiserverid, callback) { + if (clsiserverid) { + // ignore cookies and newBackend, go straight to the clsi node + opts.qs = Object.assign({ clsiserverid }, opts.qs) + request(opts, (err, response, body) => { + if (err) { + return callback( + OError.tag(err, 'error making request to CLSI', { projectId }) + ) + } + callback(null, response, body) + }) + } else { + ClsiManager._makeRequest(projectId, opts, callback) + } + }, + + _makeRequest(projectId, opts, callback) { + async.series( + { + currentBackend(cb) { + const startTime = new Date() + ClsiCookieManager.getCookieJar( + projectId, + (err, jar, clsiServerId) => { + if (err != null) { + return callback( + OError.tag(err, 'error getting cookie jar for CLSI request', { + projectId, + }) + ) + } + opts.jar = jar + const timer = new Metrics.Timer('compile.currentBackend') + request(opts, (err, response, body) => { + if (err != null) { + return callback( + OError.tag(err, 'error making request to CLSI', { + projectId, + }) + ) + } + timer.done() + Metrics.inc( + `compile.currentBackend.response.${response.statusCode}` + ) + ClsiCookieManager.setServerId( + projectId, + response, + (err, newClsiServerId) => { + if (err != null) { + callback( + OError.tag(err, 'error setting server id', { + projectId, + }) + ) + } else { + // return as soon as the standard compile has returned + callback( + null, + response, + body, + newClsiServerId || clsiServerId + ) + } + cb(err, { + response, + body, + finishTime: new Date() - startTime, + }) + } + ) + }) + } + ) + }, + newBackend(cb) { + const startTime = new Date() + ClsiManager._makeNewBackendRequest( + projectId, + opts, + (err, response, body) => { + if (err != null) { + logger.warn({ err }, 'Error making request to new CLSI backend') + } + if (response != null) { + Metrics.inc( + `compile.newBackend.response.${response.statusCode}` + ) + } + cb(err, { + response, + body, + finishTime: new Date() - startTime, + }) + } + ) + }, + }, + (err, results) => { + if (err != null) { + // This was handled higher up + return + } + if (results.newBackend != null && results.newBackend.response != null) { + const currentStatusCode = results.currentBackend.response.statusCode + const newStatusCode = results.newBackend.response.statusCode + const statusCodeSame = newStatusCode === currentStatusCode + const currentCompileTime = results.currentBackend.finishTime + const newBackendCompileTime = results.newBackend.finishTime || 0 + const timeDifference = newBackendCompileTime - currentCompileTime + logger.log( + { + statusCodeSame, + timeDifference, + currentCompileTime, + newBackendCompileTime, + projectId, + }, + 'both clsi requests returned' + ) + } + } + ) + }, + + _makeNewBackendRequest(projectId, baseOpts, callback) { + if (Settings.apis.clsi_new == null || Settings.apis.clsi_new.url == null) { + return callback() + } + const opts = { + ...baseOpts, + url: baseOpts.url.replace( + Settings.apis.clsi.url, + Settings.apis.clsi_new.url + ), + } + NewBackendCloudClsiCookieManager.getCookieJar(projectId, (err, jar) => { + if (err != null) { + return callback( + OError.tag(err, 'error getting cookie jar for CLSI request', { + projectId, + }) + ) + } + opts.jar = jar + const timer = new Metrics.Timer('compile.newBackend') + request(opts, (err, response, body) => { + timer.done() + if (err != null) { + return callback( + OError.tag(err, 'error making request to new CLSI', { + projectId, + opts, + }) + ) + } + NewBackendCloudClsiCookieManager.setServerId( + projectId, + response, + err => { + if (err != null) { + return callback( + OError.tag(err, 'error setting server id on new backend', { + projectId, + }) + ) + } + callback(null, response, body) + } + ) + }) + }) + }, + + _getCompilerUrl(compileGroup, projectId, userId, action) { + const host = Settings.apis.clsi.url + let path = `/project/${projectId}` + if (userId != null) { + path += `/user/${userId}` + } + if (action != null) { + path += `/${action}` + } + return `${host}${path}` + }, + + _postToClsi(projectId, userId, req, compileGroup, callback) { + const compileUrl = this._getCompilerUrl( + compileGroup, + projectId, + userId, + 'compile' + ) + const opts = { + url: compileUrl, + json: req, + method: 'POST', + } + ClsiManager._makeRequest( + projectId, + opts, + (err, response, body, clsiServerId) => { + if (err != null) { + return callback( + new OError('failed to make request to CLSI', { + projectId, + userId, + compileOptions: req.compile.options, + rootResourcePath: req.compile.rootResourcePath, + }) + ) + } + if (response.statusCode >= 200 && response.statusCode < 300) { + callback(null, body, clsiServerId) + } else if (response.statusCode === 413) { + callback(null, { compile: { status: 'project-too-large' } }) + } else if (response.statusCode === 409) { + callback(null, { compile: { status: 'conflict' } }) + } else if (response.statusCode === 423) { + callback(null, { compile: { status: 'compile-in-progress' } }) + } else if (response.statusCode === 503) { + callback(null, { compile: { status: 'unavailable' } }) + } else { + callback( + new OError( + `CLSI returned non-success code: ${response.statusCode}`, + { + projectId, + userId, + compileOptions: req.compile.options, + rootResourcePath: req.compile.rootResourcePath, + clsiResponse: body, + statusCode: response.statusCode, + } + ) + ) + } + } + ) + }, + + _parseOutputFiles(projectId, rawOutputFiles = []) { + const outputFiles = [] + for (const file of rawOutputFiles) { + outputFiles.push({ + path: file.path, // the clsi is now sending this to web + url: Url.parse(file.url).path, // the location of the file on the clsi, excluding the host part + type: file.type, + build: file.build, + contentId: file.contentId, + ranges: file.ranges, + size: file.size, + }) + } + return outputFiles + }, + + _buildRequest(projectId, options, callback) { + if (options == null) { + options = {} + } + ProjectGetter.getProject( + projectId, + { compiler: 1, rootDoc_id: 1, imageName: 1, rootFolder: 1 }, + (err, project) => { + if (err != null) { + return callback( + OError.tag(err, 'failed to get project', { projectId }) + ) + } + if (project == null) { + return callback( + new Errors.NotFoundError(`project does not exist: ${projectId}`) + ) + } + if (!VALID_COMPILERS.includes(project.compiler)) { + project.compiler = 'pdflatex' + } + + if (options.incrementalCompilesEnabled || options.syncType != null) { + // new way, either incremental or full + const timer = new Metrics.Timer('editor.compile-getdocs-redis') + ClsiManager.getContentFromDocUpdaterIfMatch( + projectId, + project, + options, + (err, projectStateHash, docUpdaterDocs) => { + timer.done() + if (err != null) { + logger.error({ err, projectId }, 'error checking project state') + // note: we don't bail out when there's an error getting + // incremental files from the docupdater, we just fall back + // to a normal compile below + } + // see if we can send an incremental update to the CLSI + if ( + docUpdaterDocs != null && + options.syncType !== 'full' && + err == null + ) { + Metrics.inc('compile-from-redis') + ClsiManager._buildRequestFromDocupdater( + projectId, + options, + project, + projectStateHash, + docUpdaterDocs, + callback + ) + } else { + Metrics.inc('compile-from-mongo') + ClsiManager._buildRequestFromMongo( + projectId, + options, + project, + projectStateHash, + callback + ) + } + } + ) + } else { + // old way, always from mongo + const timer = new Metrics.Timer('editor.compile-getdocs-mongo') + ClsiManager._getContentFromMongo(projectId, (err, docs, files) => { + timer.done() + if (err != null) { + return callback( + OError.tag(err, 'failed to get contents from Mongo', { + projectId, + }) + ) + } + ClsiManager._finaliseRequest( + projectId, + options, + project, + docs, + files, + callback + ) + }) + } + } + ) + }, + + getContentFromDocUpdaterIfMatch(projectId, project, options, callback) { + ClsiStateManager.computeHash(project, options, (err, projectStateHash) => { + if (err != null) { + return callback( + OError.tag(err, 'Failed to compute project state hash', { projectId }) + ) + } + DocumentUpdaterHandler.getProjectDocsIfMatch( + projectId, + projectStateHash, + (err, docs) => { + if (err != null) { + return callback( + OError.tag(err, 'Failed to get project documents', { + projectId, + projectStateHash, + }) + ) + } + callback(null, projectStateHash, docs) + } + ) + }) + }, + + getOutputFileStream(projectId, userId, buildId, outputFilePath, callback) { + const url = `${Settings.apis.clsi.url}/project/${projectId}/user/${userId}/build/${buildId}/output/${outputFilePath}` + ClsiCookieManager.getCookieJar(projectId, (err, jar) => { + if (err != null) { + return callback( + OError.tag(err, 'Failed to get cookie jar', { + projectId, + userId, + buildId, + outputFilePath, + }) + ) + } + const options = { url, method: 'GET', timeout: 60 * 1000, jar } + const readStream = request(options) + callback(null, readStream) + }) + }, + + _buildRequestFromDocupdater( + projectId, + options, + project, + projectStateHash, + docUpdaterDocs, + callback + ) { + ProjectEntityHandler.getAllDocPathsFromProject(project, (err, docPath) => { + if (err != null) { + return callback( + OError.tag(err, 'Failed to get doc paths', { projectId }) + ) + } + const docs = {} + for (const doc of docUpdaterDocs || []) { + const path = docPath[doc._id] + docs[path] = doc + } + // send new docs but not files as those are already on the clsi + options = _.clone(options) + options.syncType = 'incremental' + options.syncState = projectStateHash + // create stub doc entries for any possible root docs, if not + // present in the docupdater. This allows finaliseRequest to + // identify the root doc. + const possibleRootDocIds = [options.rootDoc_id, project.rootDoc_id] + for (const rootDocId of possibleRootDocIds) { + if (rootDocId != null && rootDocId in docPath) { + const path = docPath[rootDocId] + if (docs[path] == null) { + docs[path] = { _id: rootDocId, path } + } + } + } + ClsiManager._finaliseRequest( + projectId, + options, + project, + docs, + [], + callback + ) + }) + }, + + _buildRequestFromMongo( + projectId, + options, + project, + projectStateHash, + callback + ) { + ClsiManager._getContentFromMongo(projectId, (err, docs, files) => { + if (err != null) { + return callback( + OError.tag(err, 'failed to get project contents from Mongo', { + projectId, + }) + ) + } + options = { + ...options, + syncType: 'full', + syncState: projectStateHash, + } + ClsiManager._finaliseRequest( + projectId, + options, + project, + docs, + files, + callback + ) + }) + }, + + _getContentFromMongo(projectId, callback) { + DocumentUpdaterHandler.flushProjectToMongo(projectId, err => { + if (err != null) { + return callback( + OError.tag(err, 'failed to flush project to Mongo', { projectId }) + ) + } + ProjectEntityHandler.getAllDocs(projectId, (err, docs) => { + if (err != null) { + return callback( + OError.tag(err, 'failed to get project docs', { projectId }) + ) + } + ProjectEntityHandler.getAllFiles(projectId, (err, files) => { + if (err != null) { + return callback( + OError.tag(err, 'failed to get project files', { projectId }) + ) + } + if (files == null) { + files = {} + } + callback(null, docs || {}, files || {}) + }) + }) + }) + }, + + _finaliseRequest(projectId, options, project, docs, files, callback) { + const resources = [] + let rootResourcePath = null + let rootResourcePathOverride = null + let hasMainFile = false + let numberOfDocsInProject = 0 + + for (let path in docs) { + const doc = docs[path] + path = path.replace(/^\//, '') // Remove leading / + numberOfDocsInProject++ + if (doc.lines != null) { + // add doc to resources unless it is just a stub entry + resources.push({ + path, + content: doc.lines.join('\n'), + }) + } + if ( + project.rootDoc_id != null && + doc._id.toString() === project.rootDoc_id.toString() + ) { + rootResourcePath = path + } + if ( + options.rootDoc_id != null && + doc._id.toString() === options.rootDoc_id.toString() + ) { + rootResourcePathOverride = path + } + if (path === 'main.tex') { + hasMainFile = true + } + } + + if (rootResourcePathOverride != null) { + rootResourcePath = rootResourcePathOverride + } + if (rootResourcePath == null) { + if (hasMainFile) { + rootResourcePath = 'main.tex' + } else if (numberOfDocsInProject === 1) { + // only one file, must be the main document + for (const path in docs) { + // Remove leading / + rootResourcePath = path.replace(/^\//, '') + } + } else { + return callback(new OError('no main file specified', { projectId })) + } + } + + for (let path in files) { + const file = files[path] + path = path.replace(/^\//, '') // Remove leading / + resources.push({ + path, + url: `${Settings.apis.filestore.url}/project/${project._id}/file/${file._id}`, + modified: file.created != null ? file.created.getTime() : undefined, + }) + } + + callback(null, { + compile: { + options: { + compiler: project.compiler, + timeout: options.timeout, + imageName: project.imageName, + draft: !!options.draft, + check: options.check, + syncType: options.syncType, + syncState: options.syncState, + compileGroup: options.compileGroup, + enablePdfCaching: + (Settings.enablePdfCaching && options.enablePdfCaching) || false, + }, + rootResourcePath, + resources, + }, + }) + }, + + wordCount(projectId, userId, file, options, clsiserverid, callback) { + ClsiManager._buildRequest(projectId, options, (err, req) => { + if (err != null) { + return callback( + OError.tag(err, 'Failed to build CLSI request', { + projectId, + options, + }) + ) + } + const filename = file || req.compile.rootResourcePath + const wordCountUrl = ClsiManager._getCompilerUrl( + options.compileGroup, + projectId, + userId, + 'wordcount' + ) + const opts = { + url: wordCountUrl, + qs: { + file: filename, + image: req.compile.options.imageName, + }, + method: 'GET', + } + ClsiManager._makeRequestWithClsiServerId( + projectId, + opts, + clsiserverid, + (err, response, body) => { + if (err != null) { + return callback( + OError.tag(err, 'CLSI request failed', { projectId }) + ) + } + if (response.statusCode >= 200 && response.statusCode < 300) { + callback(null, body) + } else { + callback( + new OError( + `CLSI returned non-success code: ${response.statusCode}`, + { + projectId, + clsiResponse: body, + statusCode: response.statusCode, + } + ) + ) + } + } + ) + }) + }, +} + +module.exports = ClsiManager diff --git a/services/web/app/src/Features/Compile/ClsiStateManager.js b/services/web/app/src/Features/Compile/ClsiStateManager.js new file mode 100644 index 0000000000..f19624373e --- /dev/null +++ b/services/web/app/src/Features/Compile/ClsiStateManager.js @@ -0,0 +1,81 @@ +/* eslint-disable + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ClsiStateManager +const Settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const crypto = require('crypto') +const ProjectEntityHandler = require('../Project/ProjectEntityHandler') + +// The "state" of a project is a hash of the relevant attributes in the +// project object in this case we only need the rootFolder. +// +// The idea is that it will change if any doc or file is +// created/renamed/deleted, and also if the content of any file (not +// doc) changes. +// +// When the hash changes the full set of files on the CLSI will need to +// be updated. If it doesn't change then we can overwrite changed docs +// in place on the clsi, getting them from the docupdater. +// +// The docupdater is responsible for setting the key in redis, and +// unsetting it if it removes any documents from the doc updater. + +const buildState = s => + crypto.createHash('sha1').update(s, 'utf8').digest('hex') + +module.exports = ClsiStateManager = { + computeHash(project, options, callback) { + if (callback == null) { + callback = function (err, hash) {} + } + return ProjectEntityHandler.getAllEntitiesFromProject( + project, + function (err, docs, files) { + const fileList = Array.from(files || []).map( + f => `${f.file._id}:${f.file.rev}:${f.file.created}:${f.path}` + ) + const docList = Array.from(docs || []).map( + d => `${d.doc._id}:${d.path}` + ) + const sortedEntityList = [ + ...Array.from(docList), + ...Array.from(fileList), + ].sort() + // ignore the isAutoCompile options as it doesn't affect the + // output, but include all other options e.g. draft + const optionsList = (() => { + const result = [] + const object = options || {} + for (const key in object) { + const value = object[key] + if (!['isAutoCompile'].includes(key)) { + result.push(`option ${key}:${value}`) + } + } + return result + })() + const sortedOptionsList = optionsList.sort() + const hash = buildState( + [ + ...Array.from(sortedEntityList), + ...Array.from(sortedOptionsList), + ].join('\n') + ) + return callback(null, hash) + } + ) + }, +} diff --git a/services/web/app/src/Features/Compile/CompileController.js b/services/web/app/src/Features/Compile/CompileController.js new file mode 100644 index 0000000000..5ff2ef98c0 --- /dev/null +++ b/services/web/app/src/Features/Compile/CompileController.js @@ -0,0 +1,582 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CompileController +const OError = require('@overleaf/o-error') +const Metrics = require('@overleaf/metrics') +const ProjectGetter = require('../Project/ProjectGetter') +const CompileManager = require('./CompileManager') +const ClsiManager = require('./ClsiManager') +const logger = require('logger-sharelatex') +const request = require('request') +const Settings = require('@overleaf/settings') +const SessionManager = require('../Authentication/SessionManager') +const RateLimiter = require('../../infrastructure/RateLimiter') +const ClsiCookieManager = require('./ClsiCookieManager')( + Settings.apis.clsi != null ? Settings.apis.clsi.backendGroupName : undefined +) +const Path = require('path') + +const COMPILE_TIMEOUT_MS = 10 * 60 * 1000 + +function getImageNameForProject(projectId, callback) { + ProjectGetter.getProject(projectId, { imageName: 1 }, (err, project) => { + if (err) return callback(err) + if (!project) return callback(new Error('project not found')) + callback(null, project.imageName) + }) +} + +module.exports = CompileController = { + compile(req, res, next) { + res.setTimeout(COMPILE_TIMEOUT_MS) + const project_id = req.params.Project_id + const isAutoCompile = !!req.query.auto_compile + const enablePdfCaching = !!req.query.enable_pdf_caching + const user_id = SessionManager.getLoggedInUserId(req.session) + const options = { + isAutoCompile, + enablePdfCaching, + } + + if (req.body.rootDoc_id) { + options.rootDoc_id = req.body.rootDoc_id + } else if ( + req.body.settingsOverride && + req.body.settingsOverride.rootDoc_id + ) { + // Can be removed after deploy + options.rootDoc_id = req.body.settingsOverride.rootDoc_id + } + if (req.body.compiler) { + options.compiler = req.body.compiler + } + if (req.body.draft) { + options.draft = req.body.draft + } + if (['validate', 'error', 'silent'].includes(req.body.check)) { + options.check = req.body.check + } + if (req.body.incrementalCompilesEnabled) { + options.incrementalCompilesEnabled = true + } + + CompileManager.compile( + project_id, + user_id, + options, + ( + error, + status, + outputFiles, + clsiServerId, + limits, + validationProblems, + stats, + timings + ) => { + if (error) { + Metrics.inc('compile-error') + return next(error) + } + Metrics.inc('compile-status', 1, { status: status }) + res.json({ + status, + outputFiles, + compileGroup: limits != null ? limits.compileGroup : undefined, + clsiServerId, + validationProblems, + stats, + timings, + pdfDownloadDomain: Settings.pdfDownloadDomain, + }) + } + ) + }, + + stopCompile(req, res, next) { + if (next == null) { + next = function (error) {} + } + const project_id = req.params.Project_id + const user_id = SessionManager.getLoggedInUserId(req.session) + return CompileManager.stopCompile(project_id, user_id, function (error) { + if (error != null) { + return next(error) + } + return res.status(200).send() + }) + }, + + // Used for submissions through the public API + compileSubmission(req, res, next) { + if (next == null) { + next = function (error) {} + } + res.setTimeout(COMPILE_TIMEOUT_MS) + const { submission_id } = req.params + const options = {} + if ((req.body != null ? req.body.rootResourcePath : undefined) != null) { + options.rootResourcePath = req.body.rootResourcePath + } + if (req.body != null ? req.body.compiler : undefined) { + options.compiler = req.body.compiler + } + if (req.body != null ? req.body.draft : undefined) { + options.draft = req.body.draft + } + if ( + ['validate', 'error', 'silent'].includes( + req.body != null ? req.body.check : undefined + ) + ) { + options.check = req.body.check + } + options.compileGroup = + (req.body != null ? req.body.compileGroup : undefined) || + Settings.defaultFeatures.compileGroup + options.timeout = + (req.body != null ? req.body.timeout : undefined) || + Settings.defaultFeatures.compileTimeout + return ClsiManager.sendExternalRequest( + submission_id, + req.body, + options, + function (error, status, outputFiles, clsiServerId, validationProblems) { + if (error != null) { + return next(error) + } + res.contentType('application/json') + return res.status(200).send( + JSON.stringify({ + status, + outputFiles, + clsiServerId, + validationProblems, + }) + ) + } + ) + }, + + _compileAsUser(req, callback) { + // callback with user_id if per-user, undefined otherwise + if (!Settings.disablePerUserCompiles) { + const user_id = SessionManager.getLoggedInUserId(req.session) + return callback(null, user_id) + } else { + return callback() + } + }, // do a per-project compile, not per-user + + _downloadAsUser(req, callback) { + // callback with user_id if per-user, undefined otherwise + if (!Settings.disablePerUserCompiles) { + const user_id = SessionManager.getLoggedInUserId(req.session) + return callback(null, user_id) + } else { + return callback() + } + }, // do a per-project compile, not per-user + + downloadPdf(req, res, next) { + if (next == null) { + next = function (error) {} + } + Metrics.inc('pdf-downloads') + const project_id = req.params.Project_id + const isPdfjsPartialDownload = + req.query != null ? req.query.pdfng : undefined + const rateLimit = function (callback) { + if (isPdfjsPartialDownload) { + return callback(null, true) + } else { + const rateLimitOpts = { + endpointName: 'full-pdf-download', + throttle: 1000, + subjectName: req.ip, + timeInterval: 60 * 60, + } + return RateLimiter.addCount(rateLimitOpts, callback) + } + } + + return ProjectGetter.getProject( + project_id, + { name: 1 }, + function (err, project) { + res.contentType('application/pdf') + const filename = `${CompileController._getSafeProjectName(project)}.pdf` + + if (req.query.popupDownload) { + res.setContentDisposition('attachment', { filename }) + } else { + res.setContentDisposition('', { filename }) + } + + return rateLimit(function (err, canContinue) { + if (err != null) { + logger.err({ err }, 'error checking rate limit for pdf download') + return res.sendStatus(500) + } else if (!canContinue) { + logger.log( + { project_id, ip: req.ip }, + 'rate limit hit downloading pdf' + ) + return res.sendStatus(500) + } else { + return CompileController._downloadAsUser( + req, + function (error, user_id) { + const url = CompileController._getFileUrl( + project_id, + user_id, + req.params.build_id, + 'output.pdf' + ) + return CompileController.proxyToClsi( + project_id, + url, + req, + res, + next + ) + } + ) + } + }) + } + ) + }, + + _getSafeProjectName(project) { + const wordRegExp = /\W/g + const safeProjectName = project.name.replace(wordRegExp, '_') + return safeProjectName + }, + + deleteAuxFiles(req, res, next) { + const project_id = req.params.Project_id + const { clsiserverid } = req.query + return CompileController._compileAsUser(req, function (error, user_id) { + if (error != null) { + return next(error) + } + CompileManager.deleteAuxFiles( + project_id, + user_id, + clsiserverid, + function (error) { + if (error != null) { + return next(error) + } + return res.sendStatus(200) + } + ) + }) + }, + + // this is only used by templates, so is not called with a user_id + compileAndDownloadPdf(req, res, next) { + const { project_id } = req.params + // pass user_id as null, since templates are an "anonymous" compile + return CompileManager.compile(project_id, null, {}, function (err) { + if (err != null) { + logger.err( + { err, project_id }, + 'something went wrong compile and downloading pdf' + ) + res.sendStatus(500) + } + const url = `/project/${project_id}/output/output.pdf` + return CompileController.proxyToClsi(project_id, url, req, res, next) + }) + }, + + getFileFromClsi(req, res, next) { + if (next == null) { + next = function (error) {} + } + const project_id = req.params.Project_id + return CompileController._downloadAsUser(req, function (error, user_id) { + if (error != null) { + return next(error) + } + const url = CompileController._getFileUrl( + project_id, + user_id, + req.params.build_id, + req.params.file + ) + return CompileController.proxyToClsi(project_id, url, req, res, next) + }) + }, + + getFileFromClsiWithoutUser(req, res, next) { + if (next == null) { + next = function (error) {} + } + const { submission_id } = req.params + const url = CompileController._getFileUrl( + submission_id, + null, + req.params.build_id, + req.params.file + ) + const limits = { + compileGroup: + (req.body != null ? req.body.compileGroup : undefined) || + Settings.defaultFeatures.compileGroup, + } + return CompileController.proxyToClsiWithLimits( + submission_id, + url, + limits, + req, + res, + next + ) + }, + + // compute a GET file url for a given project, user (optional), build (optional) and file + _getFileUrl(project_id, user_id, build_id, file) { + let url + if (user_id != null && build_id != null) { + url = `/project/${project_id}/user/${user_id}/build/${build_id}/output/${file}` + } else if (user_id != null) { + url = `/project/${project_id}/user/${user_id}/output/${file}` + } else if (build_id != null) { + url = `/project/${project_id}/build/${build_id}/output/${file}` + } else { + url = `/project/${project_id}/output/${file}` + } + return url + }, + + // compute a POST url for a project, user (optional) and action + _getUrl(project_id, user_id, action) { + let path = `/project/${project_id}` + if (user_id != null) { + path += `/user/${user_id}` + } + return `${path}/${action}` + }, + + proxySyncPdf(req, res, next) { + if (next == null) { + next = function (error) {} + } + const project_id = req.params.Project_id + const { page, h, v } = req.query + if (!(page != null ? page.match(/^\d+$/) : undefined)) { + return next(new Error('invalid page parameter')) + } + if (!(h != null ? h.match(/^-?\d+\.\d+$/) : undefined)) { + return next(new Error('invalid h parameter')) + } + if (!(v != null ? v.match(/^-?\d+\.\d+$/) : undefined)) { + return next(new Error('invalid v parameter')) + } + // whether this request is going to a per-user container + return CompileController._compileAsUser(req, function (error, user_id) { + if (error != null) { + return next(error) + } + getImageNameForProject(project_id, (error, imageName) => { + if (error) return next(error) + + const url = CompileController._getUrl(project_id, user_id, 'sync/pdf') + const destination = { url, qs: { page, h, v, imageName } } + return CompileController.proxyToClsi( + project_id, + destination, + req, + res, + next + ) + }) + }) + }, + + proxySyncCode(req, res, next) { + if (next == null) { + next = function (error) {} + } + const project_id = req.params.Project_id + const { file, line, column } = req.query + if (file == null) { + return next(new Error('missing file parameter')) + } + // Check that we are dealing with a simple file path (this is not + // strictly needed because synctex uses this parameter as a label + // to look up in the synctex output, and does not open the file + // itself). Since we have valid synctex paths like foo/./bar we + // allow those by replacing /./ with / + const testPath = file.replace('/./', '/') + if (Path.resolve('/', testPath) !== `/${testPath}`) { + return next(new Error('invalid file parameter')) + } + if (!(line != null ? line.match(/^\d+$/) : undefined)) { + return next(new Error('invalid line parameter')) + } + if (!(column != null ? column.match(/^\d+$/) : undefined)) { + return next(new Error('invalid column parameter')) + } + return CompileController._compileAsUser(req, function (error, user_id) { + if (error != null) { + return next(error) + } + getImageNameForProject(project_id, (error, imageName) => { + if (error) return next(error) + + const url = CompileController._getUrl(project_id, user_id, 'sync/code') + const destination = { url, qs: { file, line, column, imageName } } + return CompileController.proxyToClsi( + project_id, + destination, + req, + res, + next + ) + }) + }) + }, + + proxyToClsi(project_id, url, req, res, next) { + if (next == null) { + next = function (error) {} + } + if (req.query != null ? req.query.compileGroup : undefined) { + return CompileController.proxyToClsiWithLimits( + project_id, + url, + { compileGroup: req.query.compileGroup }, + req, + res, + next + ) + } else { + return CompileManager.getProjectCompileLimits( + project_id, + function (error, limits) { + if (error != null) { + return next(error) + } + return CompileController.proxyToClsiWithLimits( + project_id, + url, + limits, + req, + res, + next + ) + } + ) + } + }, + + proxyToClsiWithLimits(project_id, url, limits, req, res, next) { + if (next == null) { + next = function (error) {} + } + _getPersistenceOptions(req, project_id, (err, persistenceOptions) => { + let qs + if (err != null) { + OError.tag(err, 'error getting cookie jar for clsi request') + return next(err) + } + // expand any url parameter passed in as {url:..., qs:...} + if (typeof url === 'object') { + ;({ url, qs } = url) + } + const compilerUrl = Settings.apis.clsi.url + url = `${compilerUrl}${url}` + const oneMinute = 60 * 1000 + // the base request + const options = { + url, + method: req.method, + timeout: oneMinute, + ...persistenceOptions, + } + // add any provided query string + if (qs != null) { + options.qs = Object.assign(options.qs || {}, qs) + } + // if we have a build parameter, pass it through to the clsi + if ( + (req.query != null ? req.query.pdfng : undefined) && + (req.query != null ? req.query.build : undefined) != null + ) { + // only for new pdf viewer + if (options.qs == null) { + options.qs = {} + } + options.qs.build = req.query.build + } + // if we are byte serving pdfs, pass through If-* and Range headers + // do not send any others, there's a proxying loop if Host: is passed! + if (req.query != null ? req.query.pdfng : undefined) { + const newHeaders = {} + for (const h in req.headers) { + const v = req.headers[h] + if (/^(If-|Range)/i.test(h)) { + newHeaders[h] = req.headers[h] + } + } + options.headers = newHeaders + } + const proxy = request(options) + proxy.pipe(res) + return proxy.on('error', error => + logger.warn({ err: error, url }, 'CLSI proxy error') + ) + }) + }, + + wordCount(req, res, next) { + const project_id = req.params.Project_id + const file = req.query.file || false + const { clsiserverid } = req.query + return CompileController._compileAsUser(req, function (error, user_id) { + if (error != null) { + return next(error) + } + CompileManager.wordCount( + project_id, + user_id, + file, + clsiserverid, + function (error, body) { + if (error != null) { + return next(error) + } + res.contentType('application/json') + return res.send(body) + } + ) + }) + }, +} + +function _getPersistenceOptions(req, projectId, callback) { + const { clsiserverid } = req.query + if (clsiserverid && typeof clsiserverid === 'string') { + callback(null, { qs: { clsiserverid } }) + } else { + ClsiCookieManager.getCookieJar(projectId, (err, jar) => { + callback(err, { jar }) + }) + } +} diff --git a/services/web/app/src/Features/Compile/CompileManager.js b/services/web/app/src/Features/Compile/CompileManager.js new file mode 100644 index 0000000000..c4c7bd3561 --- /dev/null +++ b/services/web/app/src/Features/Compile/CompileManager.js @@ -0,0 +1,292 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CompileManager +const Settings = require('@overleaf/settings') +const RedisWrapper = require('../../infrastructure/RedisWrapper') +const rclient = RedisWrapper.client('clsi_recently_compiled') +const ProjectGetter = require('../Project/ProjectGetter') +const ProjectRootDocManager = require('../Project/ProjectRootDocManager') +const UserGetter = require('../User/UserGetter') +const ClsiManager = require('./ClsiManager') +const Metrics = require('@overleaf/metrics') +const rateLimiter = require('../../infrastructure/RateLimiter') + +module.exports = CompileManager = { + compile(project_id, user_id, options, _callback) { + if (options == null) { + options = {} + } + if (_callback == null) { + _callback = function (error) {} + } + const timer = new Metrics.Timer('editor.compile') + const callback = function (...args) { + timer.done() + return _callback(...Array.from(args || [])) + } + + return CompileManager._checkIfRecentlyCompiled( + project_id, + user_id, + function (error, recentlyCompiled) { + if (error != null) { + return callback(error) + } + if (recentlyCompiled) { + return callback(null, 'too-recently-compiled', []) + } + + return CompileManager._checkIfAutoCompileLimitHasBeenHit( + options.isAutoCompile, + 'everyone', + function (err, canCompile) { + if (!canCompile) { + return callback(null, 'autocompile-backoff', []) + } + + return ProjectRootDocManager.ensureRootDocumentIsSet( + project_id, + function (error) { + if (error != null) { + return callback(error) + } + return CompileManager.getProjectCompileLimits( + project_id, + function (error, limits) { + if (error != null) { + return callback(error) + } + for (const key in limits) { + const value = limits[key] + options[key] = value + } + // Put a lower limit on autocompiles for free users, based on compileGroup + return CompileManager._checkCompileGroupAutoCompileLimit( + options.isAutoCompile, + limits.compileGroup, + function (err, canCompile) { + if (!canCompile) { + return callback(null, 'autocompile-backoff', []) + } + // only pass user_id down to clsi if this is a per-user compile + const compileAsUser = Settings.disablePerUserCompiles + ? undefined + : user_id + return ClsiManager.sendRequest( + project_id, + compileAsUser, + options, + function ( + error, + status, + outputFiles, + clsiServerId, + validationProblems, + stats, + timings + ) { + if (error != null) { + return callback(error) + } + return callback( + null, + status, + outputFiles, + clsiServerId, + limits, + validationProblems, + stats, + timings + ) + } + ) + } + ) + } + ) + } + ) + } + ) + } + ) + }, + + stopCompile(project_id, user_id, callback) { + if (callback == null) { + callback = function (error) {} + } + return CompileManager.getProjectCompileLimits( + project_id, + function (error, limits) { + if (error != null) { + return callback(error) + } + return ClsiManager.stopCompile(project_id, user_id, limits, callback) + } + ) + }, + + deleteAuxFiles(project_id, user_id, clsiserverid, callback) { + if (callback == null) { + callback = function (error) {} + } + return CompileManager.getProjectCompileLimits( + project_id, + function (error, limits) { + if (error != null) { + return callback(error) + } + ClsiManager.deleteAuxFiles( + project_id, + user_id, + limits, + clsiserverid, + callback + ) + } + ) + }, + + getProjectCompileLimits(project_id, callback) { + if (callback == null) { + callback = function (error, limits) {} + } + return ProjectGetter.getProject( + project_id, + { owner_ref: 1 }, + function (error, project) { + if (error != null) { + return callback(error) + } + return UserGetter.getUser( + project.owner_ref, + { alphaProgram: 1, betaProgram: 1, features: 1 }, + function (err, owner) { + if (error != null) { + return callback(error) + } + const ownerFeatures = (owner && owner.features) || {} + // put alpha users into their own compile group + if (owner && owner.alphaProgram) { + ownerFeatures.compileGroup = 'alpha' + } + return callback(null, { + timeout: + ownerFeatures.compileTimeout || + Settings.defaultFeatures.compileTimeout, + compileGroup: + ownerFeatures.compileGroup || + Settings.defaultFeatures.compileGroup, + }) + } + ) + } + ) + }, + + COMPILE_DELAY: 1, // seconds + _checkIfRecentlyCompiled(project_id, user_id, callback) { + if (callback == null) { + callback = function (error, recentlyCompiled) {} + } + const key = `compile:${project_id}:${user_id}` + return rclient.set( + key, + true, + 'EX', + this.COMPILE_DELAY, + 'NX', + function (error, ok) { + if (error != null) { + return callback(error) + } + if (ok === 'OK') { + return callback(null, false) + } else { + return callback(null, true) + } + } + ) + }, + + _checkCompileGroupAutoCompileLimit(isAutoCompile, compileGroup, callback) { + if (callback == null) { + callback = function (err, canCompile) {} + } + if (!isAutoCompile) { + return callback(null, true) + } + if (compileGroup === 'standard') { + // apply extra limits to the standard compile group + return CompileManager._checkIfAutoCompileLimitHasBeenHit( + isAutoCompile, + compileGroup, + callback + ) + } else { + Metrics.inc(`auto-compile-${compileGroup}`) + return callback(null, true) + } + }, // always allow priority group users to compile + + _checkIfAutoCompileLimitHasBeenHit(isAutoCompile, compileGroup, callback) { + if (callback == null) { + callback = function (err, canCompile) {} + } + if (!isAutoCompile) { + return callback(null, true) + } + Metrics.inc(`auto-compile-${compileGroup}`) + const opts = { + endpointName: 'auto_compile', + timeInterval: 20, + subjectName: compileGroup, + throttle: Settings.rateLimit.autoCompile[compileGroup] || 25, + } + return rateLimiter.addCount(opts, function (err, canCompile) { + if (err != null) { + canCompile = false + } + if (!canCompile) { + Metrics.inc(`auto-compile-${compileGroup}-limited`) + } + return callback(err, canCompile) + }) + }, + + wordCount(project_id, user_id, file, clsiserverid, callback) { + if (callback == null) { + callback = function (error) {} + } + return CompileManager.getProjectCompileLimits( + project_id, + function (error, limits) { + if (error != null) { + return callback(error) + } + ClsiManager.wordCount( + project_id, + user_id, + file, + limits, + clsiserverid, + callback + ) + } + ) + }, +} diff --git a/services/web/app/src/Features/Contacts/ContactController.js b/services/web/app/src/Features/Contacts/ContactController.js new file mode 100644 index 0000000000..292578cdae --- /dev/null +++ b/services/web/app/src/Features/Contacts/ContactController.js @@ -0,0 +1,93 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ContactsController +const SessionManager = require('../Authentication/SessionManager') +const ContactManager = require('./ContactManager') +const UserGetter = require('../User/UserGetter') +const logger = require('logger-sharelatex') +const Modules = require('../../infrastructure/Modules') + +module.exports = ContactsController = { + getContacts(req, res, next) { + const user_id = SessionManager.getLoggedInUserId(req.session) + return ContactManager.getContactIds( + user_id, + { limit: 50 }, + function (error, contact_ids) { + if (error != null) { + return next(error) + } + return UserGetter.getUsers( + contact_ids, + { + email: 1, + first_name: 1, + last_name: 1, + holdingAccount: 1, + }, + function (error, contacts) { + if (error != null) { + return next(error) + } + + // UserGetter.getUsers may not preserve order so put them back in order + const positions = {} + for (let i = 0; i < contact_ids.length; i++) { + const contact_id = contact_ids[i] + positions[contact_id] = i + } + contacts.sort( + (a, b) => + positions[a._id != null ? a._id.toString() : undefined] - + positions[b._id != null ? b._id.toString() : undefined] + ) + + // Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc) + contacts = contacts.filter(c => !c.holdingAccount) + + contacts = contacts.map(ContactsController._formatContact) + + return Modules.hooks.fire( + 'getContacts', + user_id, + contacts, + function (error, additional_contacts) { + if (error != null) { + return next(error) + } + contacts = contacts.concat( + ...Array.from(additional_contacts || []) + ) + return res.send({ + contacts, + }) + } + ) + } + ) + } + ) + }, + + _formatContact(contact) { + return { + id: contact._id != null ? contact._id.toString() : undefined, + email: contact.email || '', + first_name: contact.first_name || '', + last_name: contact.last_name || '', + type: 'user', + } + }, +} diff --git a/services/web/app/src/Features/Contacts/ContactManager.js b/services/web/app/src/Features/Contacts/ContactManager.js new file mode 100644 index 0000000000..11caf6c1eb --- /dev/null +++ b/services/web/app/src/Features/Contacts/ContactManager.js @@ -0,0 +1,91 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ContactManager +const OError = require('@overleaf/o-error') +const request = require('request') +const settings = require('@overleaf/settings') + +module.exports = ContactManager = { + getContactIds(user_id, options, callback) { + if (options == null) { + options = { limits: 50 } + } + if (callback == null) { + callback = function (error, contacts) {} + } + const url = `${settings.apis.contacts.url}/user/${user_id}/contacts` + return request.get( + { + url, + qs: options, + json: true, + jar: false, + }, + function (error, res, data) { + if (error != null) { + return callback(error) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + return callback( + null, + (data != null ? data.contact_ids : undefined) || [] + ) + } else { + error = new OError( + `contacts api responded with non-success code: ${res.statusCode}`, + { user_id } + ) + return callback(error) + } + } + ) + }, + + addContact(user_id, contact_id, callback) { + if (callback == null) { + callback = function (error) {} + } + const url = `${settings.apis.contacts.url}/user/${user_id}/contacts` + return request.post( + { + url, + json: { + contact_id, + }, + jar: false, + }, + function (error, res, data) { + if (error != null) { + return callback(error) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + return callback( + null, + (data != null ? data.contact_ids : undefined) || [] + ) + } else { + error = new OError( + `contacts api responded with non-success code: ${res.statusCode}`, + { + user_id, + contact_id, + } + ) + return callback(error) + } + } + ) + }, +} diff --git a/services/web/app/src/Features/Contacts/ContactRouter.js b/services/web/app/src/Features/Contacts/ContactRouter.js new file mode 100644 index 0000000000..2c5236edb0 --- /dev/null +++ b/services/web/app/src/Features/Contacts/ContactRouter.js @@ -0,0 +1,28 @@ +const AuthenticationController = require('../Authentication/AuthenticationController') +const SessionManager = require('../Authentication/SessionManager') +const ContactController = require('./ContactController') +const Settings = require('@overleaf/settings') + +function contactsAuthenticationMiddleware() { + if (!Settings.allowAnonymousReadAndWriteSharing) { + return AuthenticationController.requireLogin() + } else { + return (req, res, next) => { + if (SessionManager.isUserLoggedIn(req.session)) { + next() + } else { + res.send({ contacts: [] }) + } + } + } +} + +module.exports = { + apply(webRouter) { + webRouter.get( + '/user/contacts', + contactsAuthenticationMiddleware(), + ContactController.getContacts + ) + }, +} diff --git a/services/web/app/src/Features/Cooldown/CooldownManager.js b/services/web/app/src/Features/Cooldown/CooldownManager.js new file mode 100644 index 0000000000..645b1245a7 --- /dev/null +++ b/services/web/app/src/Features/Cooldown/CooldownManager.js @@ -0,0 +1,56 @@ +/* eslint-disable + node/handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CooldownManager +const RedisWrapper = require('../../infrastructure/RedisWrapper') +const rclient = RedisWrapper.client('cooldown') +const logger = require('logger-sharelatex') + +const COOLDOWN_IN_SECONDS = 60 * 10 + +module.exports = CooldownManager = { + _buildKey(projectId) { + return `Cooldown:{${projectId}}` + }, + + putProjectOnCooldown(projectId, callback) { + if (callback == null) { + callback = function (err) {} + } + logger.log( + { projectId }, + `[Cooldown] putting project on cooldown for ${COOLDOWN_IN_SECONDS} seconds` + ) + return rclient.set( + CooldownManager._buildKey(projectId), + '1', + 'EX', + COOLDOWN_IN_SECONDS, + callback + ) + }, + + isProjectOnCooldown(projectId, callback) { + if (callback == null) { + callback = function (err, isOnCooldown) {} + } + return rclient.get( + CooldownManager._buildKey(projectId), + function (err, result) { + if (err != null) { + return callback(err) + } + return callback(null, result === '1') + } + ) + }, +} diff --git a/services/web/app/src/Features/Cooldown/CooldownMiddleware.js b/services/web/app/src/Features/Cooldown/CooldownMiddleware.js new file mode 100644 index 0000000000..0f5f98fb2d --- /dev/null +++ b/services/web/app/src/Features/Cooldown/CooldownMiddleware.js @@ -0,0 +1,40 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let CooldownMiddleware +const CooldownManager = require('./CooldownManager') +const logger = require('logger-sharelatex') + +module.exports = CooldownMiddleware = { + freezeProject(req, res, next) { + const projectId = req.params.Project_id + if (projectId == null) { + return next(new Error('[Cooldown] No projectId parameter on route')) + } + return CooldownManager.isProjectOnCooldown( + projectId, + function (err, projectIsOnCooldown) { + if (err != null) { + return next(err) + } + if (projectIsOnCooldown) { + logger.log( + { projectId }, + '[Cooldown] project is on cooldown, denying request' + ) + return res.sendStatus(429) + } + return next() + } + ) + }, +} diff --git a/services/web/app/src/Features/Docstore/DocstoreManager.js b/services/web/app/src/Features/Docstore/DocstoreManager.js new file mode 100644 index 0000000000..f7327a5f3a --- /dev/null +++ b/services/web/app/src/Features/Docstore/DocstoreManager.js @@ -0,0 +1,303 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const request = require('request').defaults({ jar: false }) +const OError = require('@overleaf/o-error') +const logger = require('logger-sharelatex') +const settings = require('@overleaf/settings') +const Errors = require('../Errors/Errors') +const { promisifyAll } = require('../../util/promises') + +const TIMEOUT = 30 * 1000 // request timeout + +const DocstoreManager = { + deleteDoc(project_id, doc_id, name, deletedAt, callback) { + if (callback == null) { + callback = function (error) {} + } + const url = `${settings.apis.docstore.url}/project/${project_id}/doc/${doc_id}` + const docMetaData = { deleted: true, deletedAt, name } + const options = { url, json: docMetaData, timeout: TIMEOUT } + request.patch(options, function (error, res) { + if (error != null) { + return callback(error) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + return callback(null) + } else if (res.statusCode === 404) { + error = new Errors.NotFoundError({ + message: 'tried to delete doc not in docstore', + info: { + project_id, + doc_id, + }, + }) + return callback(error) // maybe suppress the error when delete doc which is not present? + } else { + error = new OError( + `docstore api responded with non-success code: ${res.statusCode}`, + { + project_id, + doc_id, + } + ) + return callback(error) + } + }) + }, + + getAllDocs(project_id, callback) { + if (callback == null) { + callback = function (error) {} + } + const url = `${settings.apis.docstore.url}/project/${project_id}/doc` + return request.get( + { + url, + timeout: TIMEOUT, + json: true, + }, + function (error, res, docs) { + if (error != null) { + return callback(error) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + return callback(null, docs) + } else { + error = new OError( + `docstore api responded with non-success code: ${res.statusCode}`, + { project_id } + ) + return callback(error) + } + } + ) + }, + + getAllDeletedDocs(project_id, callback) { + const url = `${settings.apis.docstore.url}/project/${project_id}/doc-deleted` + request.get( + { url, timeout: TIMEOUT, json: true }, + function (error, res, docs) { + if (error) { + callback( + OError.tag(error, 'could not get deleted docs from docstore') + ) + } else if (res.statusCode === 200) { + callback(null, docs) + } else { + callback( + new OError( + `docstore api responded with non-success code: ${res.statusCode}`, + { project_id } + ) + ) + } + } + ) + }, + + getAllRanges(project_id, callback) { + if (callback == null) { + callback = function (error) {} + } + const url = `${settings.apis.docstore.url}/project/${project_id}/ranges` + return request.get( + { + url, + timeout: TIMEOUT, + json: true, + }, + function (error, res, docs) { + if (error != null) { + return callback(error) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + return callback(null, docs) + } else { + error = new OError( + `docstore api responded with non-success code: ${res.statusCode}`, + { project_id } + ) + return callback(error) + } + } + ) + }, + + getDoc(project_id, doc_id, options, callback) { + if (options == null) { + options = {} + } + if (callback == null) { + callback = function (error, lines, rev, version) {} + } + if (typeof options === 'function') { + callback = options + options = {} + } + let url = `${settings.apis.docstore.url}/project/${project_id}/doc/${doc_id}` + if (options.include_deleted) { + url += '?include_deleted=true' + } + return request.get( + { + url, + timeout: TIMEOUT, + json: true, + }, + function (error, res, doc) { + if (error != null) { + return callback(error) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + logger.log( + { doc_id, project_id, version: doc.version, rev: doc.rev }, + 'got doc from docstore api' + ) + return callback(null, doc.lines, doc.rev, doc.version, doc.ranges) + } else if (res.statusCode === 404) { + error = new Errors.NotFoundError({ + message: 'doc not found in docstore', + info: { + project_id, + doc_id, + }, + }) + return callback(error) + } else { + error = new OError( + `docstore api responded with non-success code: ${res.statusCode}`, + { + project_id, + doc_id, + } + ) + return callback(error) + } + } + ) + }, + + isDocDeleted(project_id, doc_id, callback) { + const url = `${settings.apis.docstore.url}/project/${project_id}/doc/${doc_id}/deleted` + request.get( + { url, timeout: TIMEOUT, json: true }, + function (err, res, body) { + if (err) { + callback(err) + } else if (res.statusCode === 200) { + callback(null, body.deleted) + } else if (res.statusCode === 404) { + callback( + new Errors.NotFoundError({ + message: 'doc does not exist in project', + info: { project_id, doc_id }, + }) + ) + } else { + callback( + new OError( + `docstore api responded with non-success code: ${res.statusCode}`, + { project_id, doc_id } + ) + ) + } + } + ) + }, + + updateDoc(project_id, doc_id, lines, version, ranges, callback) { + if (callback == null) { + callback = function (error, modified, rev) {} + } + const url = `${settings.apis.docstore.url}/project/${project_id}/doc/${doc_id}` + return request.post( + { + url, + timeout: TIMEOUT, + json: { + lines, + version, + ranges, + }, + }, + function (error, res, result) { + if (error != null) { + return callback(error) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + logger.log( + { project_id, doc_id }, + 'update doc in docstore url finished' + ) + return callback(null, result.modified, result.rev) + } else { + error = new OError( + `docstore api responded with non-success code: ${res.statusCode}`, + { project_id, doc_id } + ) + return callback(error) + } + } + ) + }, + + archiveProject(project_id, callback) { + DocstoreManager._operateOnProject(project_id, 'archive', callback) + }, + + unarchiveProject(project_id, callback) { + DocstoreManager._operateOnProject(project_id, 'unarchive', callback) + }, + + destroyProject(project_id, callback) { + DocstoreManager._operateOnProject(project_id, 'destroy', callback) + }, + + _operateOnProject(project_id, method, callback) { + const url = `${settings.apis.docstore.url}/project/${project_id}/${method}` + logger.log({ project_id }, `calling ${method} for project in docstore`) + // use default timeout for archiving/unarchiving/destroying + request.post(url, function (err, res, docs) { + if (err != null) { + OError.tag(err, `error calling ${method} project in docstore`, { + project_id, + }) + return callback(err) + } + + if (res.statusCode >= 200 && res.statusCode < 300) { + callback() + } else { + const error = new Error( + `docstore api responded with non-success code: ${res.statusCode}` + ) + logger.warn( + { err: error, project_id }, + `error calling ${method} project in docstore` + ) + callback(error) + } + }) + }, +} + +module.exports = DocstoreManager +module.exports.promises = promisifyAll(DocstoreManager, { + multiResult: { + getDoc: ['lines', 'rev', 'version', 'ranges'], + updateDoc: ['modified', 'rev'], + }, +}) diff --git a/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js b/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js new file mode 100644 index 0000000000..3aa5eacc75 --- /dev/null +++ b/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js @@ -0,0 +1,393 @@ +const request = require('request').defaults({ timeout: 30 * 100 }) +const OError = require('@overleaf/o-error') +const settings = require('@overleaf/settings') +const _ = require('underscore') +const async = require('async') +const logger = require('logger-sharelatex') +const metrics = require('@overleaf/metrics') +const { promisify } = require('util') + +module.exports = { + flushProjectToMongo, + flushMultipleProjectsToMongo, + flushProjectToMongoAndDelete, + flushDocToMongo, + deleteDoc, + getDocument, + setDocument, + getProjectDocsIfMatch, + clearProjectState, + acceptChanges, + deleteThread, + resyncProjectHistory, + updateProjectStructure, + promises: { + flushProjectToMongo: promisify(flushProjectToMongo), + flushMultipleProjectsToMongo: promisify(flushMultipleProjectsToMongo), + flushProjectToMongoAndDelete: promisify(flushProjectToMongoAndDelete), + flushDocToMongo: promisify(flushDocToMongo), + deleteDoc: promisify(deleteDoc), + getDocument: promisify(getDocument), + setDocument: promisify(setDocument), + getProjectDocsIfMatch: promisify(getProjectDocsIfMatch), + clearProjectState: promisify(clearProjectState), + acceptChanges: promisify(acceptChanges), + deleteThread: promisify(deleteThread), + resyncProjectHistory: promisify(resyncProjectHistory), + updateProjectStructure: promisify(updateProjectStructure), + }, +} + +function flushProjectToMongo(projectId, callback) { + _makeRequest( + { + path: `/project/${projectId}/flush`, + method: 'POST', + }, + projectId, + 'flushing.mongo.project', + callback + ) +} + +function flushMultipleProjectsToMongo(projectIds, callback) { + const jobs = projectIds.map(projectId => callback => { + flushProjectToMongo(projectId, callback) + }) + async.series(jobs, callback) +} + +function flushProjectToMongoAndDelete(projectId, callback) { + _makeRequest( + { + path: `/project/${projectId}`, + method: 'DELETE', + }, + projectId, + 'flushing.mongo.project', + callback + ) +} + +function flushDocToMongo(projectId, docId, callback) { + _makeRequest( + { + path: `/project/${projectId}/doc/${docId}/flush`, + method: 'POST', + }, + projectId, + 'flushing.mongo.doc', + callback + ) +} + +function deleteDoc(projectId, docId, callback) { + _makeRequest( + { + path: `/project/${projectId}/doc/${docId}`, + method: 'DELETE', + }, + projectId, + 'delete.mongo.doc', + callback + ) +} + +function getDocument(projectId, docId, fromVersion, callback) { + _makeRequest( + { + path: `/project/${projectId}/doc/${docId}?fromVersion=${fromVersion}`, + json: true, + }, + projectId, + 'get-document', + function (error, doc) { + if (error) { + return callback(error) + } + callback(null, doc.lines, doc.version, doc.ranges, doc.ops) + } + ) +} + +function setDocument(projectId, docId, userId, docLines, source, callback) { + _makeRequest( + { + path: `/project/${projectId}/doc/${docId}`, + method: 'POST', + json: { + lines: docLines, + source, + user_id: userId, + }, + }, + projectId, + 'set-document', + callback + ) +} + +function getProjectDocsIfMatch(projectId, projectStateHash, callback) { + // If the project state hasn't changed, we can get all the latest + // docs from redis via the docupdater. Otherwise we will need to + // fall back to getting them from mongo. + const timer = new metrics.Timer('get-project-docs') + const url = `${settings.apis.documentupdater.url}/project/${projectId}/get_and_flush_if_old?state=${projectStateHash}` + request.post(url, function (error, res, body) { + timer.done() + if (error) { + OError.tag(error, 'error getting project docs from doc updater', { + url, + projectId, + }) + return callback(error) + } + if (res.statusCode === 409) { + // HTTP response code "409 Conflict" + // Docupdater has checked the projectStateHash and found that + // it has changed. This means that the docs currently in redis + // aren't the only change to the project and the full set of + // docs/files should be retreived from docstore/filestore + // instead. + callback() + } else if (res.statusCode >= 200 && res.statusCode < 300) { + let docs + try { + docs = JSON.parse(body) + } catch (error1) { + return callback(OError.tag(error1)) + } + callback(null, docs) + } else { + callback( + new OError( + `doc updater returned a non-success status code: ${res.statusCode}`, + { + projectId, + url, + } + ) + ) + } + }) +} + +function clearProjectState(projectId, callback) { + _makeRequest( + { + path: `/project/${projectId}/clearState`, + method: 'POST', + }, + projectId, + 'clear-project-state', + callback + ) +} + +function acceptChanges(projectId, docId, changeIds, callback) { + _makeRequest( + { + path: `/project/${projectId}/doc/${docId}/change/accept`, + json: { change_ids: changeIds }, + method: 'POST', + }, + projectId, + 'accept-changes', + callback + ) +} + +function deleteThread(projectId, docId, threadId, callback) { + _makeRequest( + { + path: `/project/${projectId}/doc/${docId}/comment/${threadId}`, + method: 'DELETE', + }, + projectId, + 'delete-thread', + callback + ) +} + +function resyncProjectHistory( + projectId, + projectHistoryId, + docs, + files, + callback +) { + _makeRequest( + { + path: `/project/${projectId}/history/resync`, + json: { docs, files, projectHistoryId }, + method: 'POST', + }, + projectId, + 'resync-project-history', + callback + ) +} + +function updateProjectStructure( + projectId, + projectHistoryId, + userId, + changes, + callback +) { + if ( + settings.apis.project_history == null || + !settings.apis.project_history.sendProjectStructureOps + ) { + return callback() + } + + const { + deletes: docDeletes, + adds: docAdds, + renames: docRenames, + } = _getUpdates('doc', changes.oldDocs, changes.newDocs) + const { + deletes: fileDeletes, + adds: fileAdds, + renames: fileRenames, + } = _getUpdates('file', changes.oldFiles, changes.newFiles) + const updates = [].concat( + docDeletes, + fileDeletes, + docAdds, + fileAdds, + docRenames, + fileRenames + ) + const projectVersion = + changes && changes.newProject && changes.newProject.version + + if (updates.length < 1) { + return callback() + } + + if (projectVersion == null) { + logger.warn( + { projectId, changes, projectVersion }, + 'did not receive project version in changes' + ) + return callback(new Error('did not receive project version in changes')) + } + + _makeRequest( + { + path: `/project/${projectId}`, + json: { + updates, + userId, + version: projectVersion, + projectHistoryId, + }, + method: 'POST', + }, + projectId, + 'update-project-structure', + callback + ) +} + +function _makeRequest(options, projectId, metricsKey, callback) { + const timer = new metrics.Timer(metricsKey) + request( + { + url: `${settings.apis.documentupdater.url}${options.path}`, + json: options.json, + method: options.method || 'GET', + }, + function (error, res, body) { + timer.done() + if (error) { + logger.warn( + { error, projectId }, + 'error making request to document updater' + ) + callback(error) + } else if (res.statusCode >= 200 && res.statusCode < 300) { + callback(null, body) + } else { + error = new Error( + `document updater returned a failure status code: ${res.statusCode}` + ) + logger.warn( + { error, projectId }, + `document updater returned failure status code: ${res.statusCode}` + ) + callback(error) + } + } + ) +} + +function _getUpdates(entityType, oldEntities, newEntities) { + if (!oldEntities) { + oldEntities = [] + } + if (!newEntities) { + newEntities = [] + } + const deletes = [] + const adds = [] + const renames = [] + + const oldEntitiesHash = _.indexBy(oldEntities, entity => + entity[entityType]._id.toString() + ) + const newEntitiesHash = _.indexBy(newEntities, entity => + entity[entityType]._id.toString() + ) + + // Send deletes before adds (and renames) to keep a 1:1 mapping between + // paths and ids + // + // When a file is replaced, we first delete the old file and then add the + // new file. If the 'add' operation is sent to project history before the + // 'delete' then we would have two files with the same path at that point + // in time. + for (const id in oldEntitiesHash) { + const oldEntity = oldEntitiesHash[id] + const newEntity = newEntitiesHash[id] + + if (newEntity == null) { + // entity deleted + deletes.push({ + type: `rename-${entityType}`, + id, + pathname: oldEntity.path, + newPathname: '', + }) + } + } + + for (const id in newEntitiesHash) { + const newEntity = newEntitiesHash[id] + const oldEntity = oldEntitiesHash[id] + + if (oldEntity == null) { + // entity added + adds.push({ + type: `add-${entityType}`, + id, + pathname: newEntity.path, + docLines: newEntity.docLines, + url: newEntity.url, + hash: newEntity.file != null ? newEntity.file.hash : undefined, + }) + } else if (newEntity.path !== oldEntity.path) { + // entity renamed + renames.push({ + type: `rename-${entityType}`, + id, + pathname: oldEntity.path, + newPathname: newEntity.path, + }) + } + } + + return { deletes, adds, renames } +} diff --git a/services/web/app/src/Features/Documents/DocumentController.js b/services/web/app/src/Features/Documents/DocumentController.js new file mode 100644 index 0000000000..1d89b2f1f7 --- /dev/null +++ b/services/web/app/src/Features/Documents/DocumentController.js @@ -0,0 +1,132 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const ProjectGetter = require('../Project/ProjectGetter') +const OError = require('@overleaf/o-error') +const ProjectLocator = require('../Project/ProjectLocator') +const ProjectEntityHandler = require('../Project/ProjectEntityHandler') +const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler') +const logger = require('logger-sharelatex') +const _ = require('lodash') + +module.exports = { + getDocument(req, res, next) { + if (next == null) { + next = function (error) {} + } + const project_id = req.params.Project_id + const { doc_id } = req.params + const plain = + __guard__(req != null ? req.query : undefined, x => x.plain) === 'true' + return ProjectGetter.getProject( + project_id, + { rootFolder: true, overleaf: true }, + function (error, project) { + if (error != null) { + return next(error) + } + if (project == null) { + return res.sendStatus(404) + } + return ProjectLocator.findElement( + { project, element_id: doc_id, type: 'doc' }, + function (error, doc, path) { + if (error != null) { + OError.tag(error, 'error finding element for getDocument', { + doc_id, + project_id, + }) + return next(error) + } + return ProjectEntityHandler.getDoc( + project_id, + doc_id, + function (error, lines, rev, version, ranges) { + if (error != null) { + OError.tag( + error, + 'error finding doc contents for getDocument', + { + doc_id, + project_id, + } + ) + return next(error) + } + if (plain) { + res.type('text/plain') + return res.send(lines.join('\n')) + } else { + const projectHistoryId = _.get(project, 'overleaf.history.id') + const projectHistoryType = _.get( + project, + 'overleaf.history.display' + ) + ? 'project-history' + : undefined // for backwards compatibility, don't send anything if the project is still on track-changes + return res.json({ + lines, + version, + ranges, + pathname: path.fileSystem, + projectHistoryId, + projectHistoryType, + }) + } + } + ) + } + ) + } + ) + }, + + setDocument(req, res, next) { + if (next == null) { + next = function (error) {} + } + const project_id = req.params.Project_id + const { doc_id } = req.params + const { lines, version, ranges, lastUpdatedAt, lastUpdatedBy } = req.body + return ProjectEntityUpdateHandler.updateDocLines( + project_id, + doc_id, + lines, + version, + ranges, + lastUpdatedAt, + lastUpdatedBy, + function (error) { + if (error != null) { + OError.tag(error, 'error finding element for getDocument', { + doc_id, + project_id, + }) + return next(error) + } + logger.log( + { doc_id, project_id }, + 'finished receiving set document request from api (docupdater)' + ) + return res.sendStatus(200) + } + ) + }, +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Documents/DocumentHelper.js b/services/web/app/src/Features/Documents/DocumentHelper.js new file mode 100644 index 0000000000..be9840bb64 --- /dev/null +++ b/services/web/app/src/Features/Documents/DocumentHelper.js @@ -0,0 +1,78 @@ +/* eslint-disable + max-len, + no-cond-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let DocumentHelper +module.exports = DocumentHelper = { + getTitleFromTexContent(content, maxContentToScan) { + if (maxContentToScan == null) { + maxContentToScan = 30000 + } + const TITLE_WITH_CURLY_BRACES = /\\[tT]itle\*?\s*{([^}]+)}/ + const TITLE_WITH_SQUARE_BRACES = /\\[tT]itle\s*\[([^\]]+)\]/ + for (const line of Array.from( + DocumentHelper._getLinesFromContent(content, maxContentToScan) + )) { + var match + if ( + (match = + line.match(TITLE_WITH_CURLY_BRACES) || + line.match(TITLE_WITH_SQUARE_BRACES)) + ) { + return DocumentHelper.detex(match[1]) + } + } + + return null + }, + + contentHasDocumentclass(content, maxContentToScan) { + if (maxContentToScan == null) { + maxContentToScan = 30000 + } + for (const line of Array.from( + DocumentHelper._getLinesFromContent(content, maxContentToScan) + )) { + // We've had problems with this regex locking up CPU. + // Previously /.*\\documentclass/ would totally lock up on lines of 500kb (data text files :() + // This regex will only look from the start of the line, including whitespace so will return quickly + // regardless of line length. + if (line.match(/^\s*\\documentclass/)) { + return true + } + } + + return false + }, + + detex(string) { + return string + .replace(/\\LaTeX/g, 'LaTeX') + .replace(/\\TeX/g, 'TeX') + .replace(/\\TikZ/g, 'TikZ') + .replace(/\\BibTeX/g, 'BibTeX') + .replace(/\\\[[A-Za-z0-9. ]*\]/g, ' ') // line spacing + .replace(/\\(?:[a-zA-Z]+|.|)/g, '') + .replace(/{}|~/g, ' ') + .replace(/[${}]/g, '') + .replace(/ +/g, ' ') + .trim() + }, + + _getLinesFromContent(content, maxContentToScan) { + if (typeof content === 'string') { + return content.substring(0, maxContentToScan).split('\n') + } else { + return content + } + }, +} diff --git a/services/web/app/src/Features/Downloads/ProjectDownloadsController.js b/services/web/app/src/Features/Downloads/ProjectDownloadsController.js new file mode 100644 index 0000000000..0e16eb6c57 --- /dev/null +++ b/services/web/app/src/Features/Downloads/ProjectDownloadsController.js @@ -0,0 +1,82 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectDownloadsController +const logger = require('logger-sharelatex') +const Metrics = require('@overleaf/metrics') +const ProjectGetter = require('../Project/ProjectGetter') +const ProjectZipStreamManager = require('./ProjectZipStreamManager') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') + +module.exports = ProjectDownloadsController = { + downloadProject(req, res, next) { + const project_id = req.params.Project_id + Metrics.inc('zip-downloads') + return DocumentUpdaterHandler.flushProjectToMongo( + project_id, + function (error) { + if (error != null) { + return next(error) + } + return ProjectGetter.getProject( + project_id, + { name: true }, + function (error, project) { + if (error != null) { + return next(error) + } + return ProjectZipStreamManager.createZipStreamForProject( + project_id, + function (error, stream) { + if (error != null) { + return next(error) + } + res.setContentDisposition('attachment', { + filename: `${project.name}.zip`, + }) + res.contentType('application/zip') + return stream.pipe(res) + } + ) + } + ) + } + ) + }, + + downloadMultipleProjects(req, res, next) { + const project_ids = req.query.project_ids.split(',') + Metrics.inc('zip-downloads-multiple') + return DocumentUpdaterHandler.flushMultipleProjectsToMongo( + project_ids, + function (error) { + if (error != null) { + return next(error) + } + return ProjectZipStreamManager.createZipStreamForMultipleProjects( + project_ids, + function (error, stream) { + if (error != null) { + return next(error) + } + res.setContentDisposition('attachment', { + filename: `Overleaf Projects (${project_ids.length} items).zip`, + }) + res.contentType('application/zip') + return stream.pipe(res) + } + ) + } + ) + }, +} diff --git a/services/web/app/src/Features/Downloads/ProjectZipStreamManager.js b/services/web/app/src/Features/Downloads/ProjectZipStreamManager.js new file mode 100644 index 0000000000..8412b46750 --- /dev/null +++ b/services/web/app/src/Features/Downloads/ProjectZipStreamManager.js @@ -0,0 +1,182 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectZipStreamManager +const archiver = require('archiver') +const async = require('async') +const logger = require('logger-sharelatex') +const ProjectEntityHandler = require('../Project/ProjectEntityHandler') +const ProjectGetter = require('../Project/ProjectGetter') +const FileStoreHandler = require('../FileStore/FileStoreHandler') + +module.exports = ProjectZipStreamManager = { + createZipStreamForMultipleProjects(project_ids, callback) { + // We'll build up a zip file that contains multiple zip files + + if (callback == null) { + callback = function (error, stream) {} + } + const archive = archiver('zip') + archive.on('error', err => + logger.err( + { err, project_ids }, + 'something went wrong building archive of project' + ) + ) + callback(null, archive) + + const jobs = [] + for (const project_id of Array.from(project_ids || [])) { + ;(project_id => + jobs.push(callback => + ProjectGetter.getProject( + project_id, + { name: true }, + function (error, project) { + if (error != null) { + return callback(error) + } + logger.log( + { project_id, name: project.name }, + 'appending project to zip stream' + ) + return ProjectZipStreamManager.createZipStreamForProject( + project_id, + function (error, stream) { + if (error != null) { + return callback(error) + } + archive.append(stream, { name: `${project.name}.zip` }) + return stream.on('end', function () { + logger.log( + { project_id, name: project.name }, + 'zip stream ended' + ) + return callback() + }) + } + ) + } + ) + ))(project_id) + } + + return async.series(jobs, function () { + logger.log( + { project_ids }, + 'finished creating zip stream of multiple projects' + ) + return archive.finalize() + }) + }, + + createZipStreamForProject(project_id, callback) { + if (callback == null) { + callback = function (error, stream) {} + } + const archive = archiver('zip') + // return stream immediately before we start adding things to it + archive.on('error', err => + logger.err( + { err, project_id }, + 'something went wrong building archive of project' + ) + ) + callback(null, archive) + return this.addAllDocsToArchive(project_id, archive, error => { + if (error != null) { + logger.error( + { err: error, project_id }, + 'error adding docs to zip stream' + ) + } + return this.addAllFilesToArchive(project_id, archive, error => { + if (error != null) { + logger.error( + { err: error, project_id }, + 'error adding files to zip stream' + ) + } + return archive.finalize() + }) + }) + }, + + addAllDocsToArchive(project_id, archive, callback) { + if (callback == null) { + callback = function (error) {} + } + return ProjectEntityHandler.getAllDocs(project_id, function (error, docs) { + if (error != null) { + return callback(error) + } + const jobs = [] + for (const path in docs) { + const doc = docs[path] + ;(function (path, doc) { + if (path[0] === '/') { + path = path.slice(1) + } + return jobs.push(function (callback) { + logger.log({ project_id }, 'Adding doc') + archive.append(doc.lines.join('\n'), { name: path }) + return callback() + }) + })(path, doc) + } + return async.series(jobs, callback) + }) + }, + + addAllFilesToArchive(project_id, archive, callback) { + if (callback == null) { + callback = function (error) {} + } + return ProjectEntityHandler.getAllFiles( + project_id, + function (error, files) { + if (error != null) { + return callback(error) + } + const jobs = [] + for (const path in files) { + const file = files[path] + ;((path, file) => + jobs.push(callback => + FileStoreHandler.getFileStream( + project_id, + file._id, + {}, + function (error, stream) { + if (error != null) { + logger.warn( + { err: error, project_id, file_id: file._id }, + 'something went wrong adding file to zip archive' + ) + return callback(error) + } + if (path[0] === '/') { + path = path.slice(1) + } + archive.append(stream, { name: path }) + return stream.on('end', () => callback()) + } + ) + ))(path, file) + } + return async.parallelLimit(jobs, 5, callback) + } + ) + }, +} diff --git a/services/web/app/src/Features/Editor/EditorController.js b/services/web/app/src/Features/Editor/EditorController.js new file mode 100644 index 0000000000..21ba77d9f7 --- /dev/null +++ b/services/web/app/src/Features/Editor/EditorController.js @@ -0,0 +1,727 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-dupe-keys, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const logger = require('logger-sharelatex') +const OError = require('@overleaf/o-error') +const Metrics = require('@overleaf/metrics') +const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler') +const ProjectOptionsHandler = require('../Project/ProjectOptionsHandler') +const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler') +const ProjectDeleter = require('../Project/ProjectDeleter') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const EditorRealTimeController = require('./EditorRealTimeController') +const async = require('async') +const PublicAccessLevels = require('../Authorization/PublicAccessLevels') +const _ = require('underscore') +const { promisifyAll } = require('../../util/promises') + +const EditorController = { + addDoc(project_id, folder_id, docName, docLines, source, user_id, callback) { + if (callback == null) { + callback = function (error, doc) {} + } + return EditorController.addDocWithRanges( + project_id, + folder_id, + docName, + docLines, + {}, + source, + user_id, + callback + ) + }, + + addDocWithRanges( + project_id, + folder_id, + docName, + docLines, + docRanges, + source, + user_id, + callback + ) { + if (callback == null) { + callback = function (error, doc) {} + } + docName = docName.trim() + Metrics.inc('editor.add-doc') + return ProjectEntityUpdateHandler.addDocWithRanges( + project_id, + folder_id, + docName, + docLines, + docRanges, + user_id, + (err, doc, folder_id) => { + if (err != null) { + OError.tag(err, 'error adding doc without lock', { + project_id, + docName, + }) + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'reciveNewDoc', + folder_id, + doc, + source, + user_id + ) + return callback(err, doc) + } + ) + }, + + addFile( + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + source, + user_id, + callback + ) { + if (callback == null) { + callback = function (error, file) {} + } + fileName = fileName.trim() + Metrics.inc('editor.add-file') + return ProjectEntityUpdateHandler.addFile( + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + user_id, + (err, fileRef, folder_id) => { + if (err != null) { + OError.tag(err, 'error adding file without lock', { + project_id, + folder_id, + fileName, + }) + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'reciveNewFile', + folder_id, + fileRef, + source, + linkedFileData, + user_id + ) + return callback(err, fileRef) + } + ) + }, + + upsertDoc( + project_id, + folder_id, + docName, + docLines, + source, + user_id, + callback + ) { + if (callback == null) { + callback = function (err) {} + } + return ProjectEntityUpdateHandler.upsertDoc( + project_id, + folder_id, + docName, + docLines, + source, + user_id, + function (err, doc, didAddNewDoc) { + if (didAddNewDoc) { + EditorRealTimeController.emitToRoom( + project_id, + 'reciveNewDoc', + folder_id, + doc, + source, + user_id + ) + } + return callback(err, doc) + } + ) + }, + + upsertFile( + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + source, + user_id, + callback + ) { + if (callback == null) { + callback = function (err, file) {} + } + return ProjectEntityUpdateHandler.upsertFile( + project_id, + folder_id, + fileName, + fsPath, + linkedFileData, + user_id, + function (err, newFile, didAddFile, existingFile) { + if (err != null) { + return callback(err) + } + if (!didAddFile) { + // replacement, so remove the existing file from the client + EditorRealTimeController.emitToRoom( + project_id, + 'removeEntity', + existingFile._id, + source + ) + } + // now add the new file on the client + EditorRealTimeController.emitToRoom( + project_id, + 'reciveNewFile', + folder_id, + newFile, + source, + linkedFileData, + user_id + ) + return callback(null, newFile) + } + ) + }, + + upsertDocWithPath( + project_id, + elementPath, + docLines, + source, + user_id, + callback + ) { + return ProjectEntityUpdateHandler.upsertDocWithPath( + project_id, + elementPath, + docLines, + source, + user_id, + function (err, doc, didAddNewDoc, newFolders, lastFolder) { + if (err != null) { + return callback(err) + } + return EditorController._notifyProjectUsersOfNewFolders( + project_id, + newFolders, + function (err) { + if (err != null) { + return callback(err) + } + if (didAddNewDoc) { + EditorRealTimeController.emitToRoom( + project_id, + 'reciveNewDoc', + lastFolder._id, + doc, + source, + user_id + ) + } + return callback() + } + ) + } + ) + }, + + upsertFileWithPath( + project_id, + elementPath, + fsPath, + linkedFileData, + source, + user_id, + callback + ) { + return ProjectEntityUpdateHandler.upsertFileWithPath( + project_id, + elementPath, + fsPath, + linkedFileData, + user_id, + function ( + err, + newFile, + didAddFile, + existingFile, + newFolders, + lastFolder + ) { + if (err != null) { + return callback(err) + } + return EditorController._notifyProjectUsersOfNewFolders( + project_id, + newFolders, + function (err) { + if (err != null) { + return callback(err) + } + if (!didAddFile) { + // replacement, so remove the existing file from the client + EditorRealTimeController.emitToRoom( + project_id, + 'removeEntity', + existingFile._id, + source + ) + } + // now add the new file on the client + EditorRealTimeController.emitToRoom( + project_id, + 'reciveNewFile', + lastFolder._id, + newFile, + source, + linkedFileData, + user_id + ) + return callback() + } + ) + } + ) + }, + + addFolder(project_id, folder_id, folderName, source, userId, callback) { + if (callback == null) { + callback = function (error, folder) {} + } + folderName = folderName.trim() + Metrics.inc('editor.add-folder') + return ProjectEntityUpdateHandler.addFolder( + project_id, + folder_id, + folderName, + (err, folder, folder_id) => { + if (err != null) { + OError.tag(err, 'could not add folder', { + project_id, + folder_id, + folderName, + source, + }) + return callback(err) + } + return EditorController._notifyProjectUsersOfNewFolder( + project_id, + folder_id, + folder, + userId, + function (err) { + if (err != null) { + return callback(err) + } + return callback(null, folder) + } + ) + } + ) + }, + + mkdirp(project_id, path, callback) { + if (callback == null) { + callback = function (error, newFolders, lastFolder) {} + } + logger.log({ project_id, path }, "making directories if they don't exist") + return ProjectEntityUpdateHandler.mkdirp( + project_id, + path, + (err, newFolders, lastFolder) => { + if (err != null) { + OError.tag(err, 'could not mkdirp', { + project_id, + path, + }) + return callback(err) + } + + return EditorController._notifyProjectUsersOfNewFolders( + project_id, + newFolders, + function (err) { + if (err != null) { + return callback(err) + } + return callback(null, newFolders, lastFolder) + } + ) + } + ) + }, + + deleteEntity(project_id, entity_id, entityType, source, userId, callback) { + if (callback == null) { + callback = function (error) {} + } + Metrics.inc('editor.delete-entity') + return ProjectEntityUpdateHandler.deleteEntity( + project_id, + entity_id, + entityType, + userId, + function (err) { + if (err != null) { + OError.tag(err, 'could not delete entity', { + project_id, + entity_id, + entityType, + }) + return callback(err) + } + logger.log( + { project_id, entity_id, entityType }, + 'telling users entity has been deleted' + ) + EditorRealTimeController.emitToRoom( + project_id, + 'removeEntity', + entity_id, + source + ) + return callback() + } + ) + }, + + deleteEntityWithPath(project_id, path, source, user_id, callback) { + return ProjectEntityUpdateHandler.deleteEntityWithPath( + project_id, + path, + user_id, + function (err, entity_id) { + if (err != null) { + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'removeEntity', + entity_id, + source + ) + return callback(null, entity_id) + } + ) + }, + + updateProjectDescription(project_id, description, callback) { + if (callback == null) { + callback = function () {} + } + logger.log({ project_id, description }, 'updating project description') + return ProjectDetailsHandler.setProjectDescription( + project_id, + description, + function (err) { + if (err != null) { + OError.tag( + err, + 'something went wrong setting the project description', + { + project_id, + description, + } + ) + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'projectDescriptionUpdated', + description + ) + return callback() + } + ) + }, + + deleteProject(project_id, callback) { + Metrics.inc('editor.delete-project') + return ProjectDeleter.deleteProject(project_id, callback) + }, + + renameEntity(project_id, entity_id, entityType, newName, userId, callback) { + if (callback == null) { + callback = function (error) {} + } + Metrics.inc('editor.rename-entity') + return ProjectEntityUpdateHandler.renameEntity( + project_id, + entity_id, + entityType, + newName, + userId, + function (err) { + if (err != null) { + OError.tag(err, 'error renaming entity', { + project_id, + entity_id, + entityType, + newName, + }) + return callback(err) + } + if (newName.length > 0) { + EditorRealTimeController.emitToRoom( + project_id, + 'reciveEntityRename', + entity_id, + newName + ) + } + return callback() + } + ) + }, + + moveEntity(project_id, entity_id, folder_id, entityType, userId, callback) { + if (callback == null) { + callback = function (error) {} + } + Metrics.inc('editor.move-entity') + return ProjectEntityUpdateHandler.moveEntity( + project_id, + entity_id, + folder_id, + entityType, + userId, + function (err) { + if (err != null) { + OError.tag(err, 'error moving entity', { + project_id, + entity_id, + folder_id, + }) + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'reciveEntityMove', + entity_id, + folder_id + ) + return callback() + } + ) + }, + + renameProject(project_id, newName, callback) { + if (callback == null) { + callback = function (err) {} + } + return ProjectDetailsHandler.renameProject( + project_id, + newName, + function (err) { + if (err != null) { + OError.tag(err, 'error renaming project', { + project_id, + newName, + }) + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'projectNameUpdated', + newName + ) + return callback() + } + ) + }, + + setCompiler(project_id, compiler, callback) { + if (callback == null) { + callback = function (err) {} + } + return ProjectOptionsHandler.setCompiler( + project_id, + compiler, + function (err) { + if (err != null) { + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'compilerUpdated', + compiler + ) + return callback() + } + ) + }, + + setImageName(project_id, imageName, callback) { + if (callback == null) { + callback = function (err) {} + } + return ProjectOptionsHandler.setImageName( + project_id, + imageName, + function (err) { + if (err != null) { + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'imageNameUpdated', + imageName + ) + return callback() + } + ) + }, + + setSpellCheckLanguage(project_id, languageCode, callback) { + if (callback == null) { + callback = function (err) {} + } + return ProjectOptionsHandler.setSpellCheckLanguage( + project_id, + languageCode, + function (err) { + if (err != null) { + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'spellCheckLanguageUpdated', + languageCode + ) + return callback() + } + ) + }, + + setPublicAccessLevel(project_id, newAccessLevel, callback) { + if (callback == null) { + callback = function (err) {} + } + return ProjectDetailsHandler.setPublicAccessLevel( + project_id, + newAccessLevel, + function (err) { + if (err != null) { + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'project:publicAccessLevel:changed', + { newAccessLevel } + ) + if (newAccessLevel === PublicAccessLevels.TOKEN_BASED) { + return ProjectDetailsHandler.ensureTokensArePresent( + project_id, + function (err, tokens) { + if (err != null) { + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'project:tokens:changed', + { tokens } + ) + return callback() + } + ) + } else { + return callback() + } + } + ) + }, + + setRootDoc(project_id, newRootDocID, callback) { + if (callback == null) { + callback = function (err) {} + } + return ProjectEntityUpdateHandler.setRootDoc( + project_id, + newRootDocID, + function (err) { + if (err != null) { + return callback(err) + } + EditorRealTimeController.emitToRoom( + project_id, + 'rootDocUpdated', + newRootDocID + ) + return callback() + } + ) + }, + + _notifyProjectUsersOfNewFolders(project_id, folders, callback) { + if (callback == null) { + callback = function (error) {} + } + return async.eachSeries( + folders, + (folder, cb) => + EditorController._notifyProjectUsersOfNewFolder( + project_id, + folder.parentFolder_id, + folder, + null, + cb + ), + callback + ) + }, + + _notifyProjectUsersOfNewFolder( + project_id, + folder_id, + folder, + userId, + callback + ) { + if (callback == null) { + callback = function (error) {} + } + EditorRealTimeController.emitToRoom( + project_id, + 'reciveNewFolder', + folder_id, + folder, + userId + ) + return callback() + }, +} + +EditorController.promises = promisifyAll(EditorController) +module.exports = EditorController diff --git a/services/web/app/src/Features/Editor/EditorHttpController.js b/services/web/app/src/Features/Editor/EditorHttpController.js new file mode 100644 index 0000000000..549a69bcac --- /dev/null +++ b/services/web/app/src/Features/Editor/EditorHttpController.js @@ -0,0 +1,304 @@ +const ProjectDeleter = require('../Project/ProjectDeleter') +const EditorController = require('./EditorController') +const ProjectGetter = require('../Project/ProjectGetter') +const AuthorizationManager = require('../Authorization/AuthorizationManager') +const ProjectEditorHandler = require('../Project/ProjectEditorHandler') +const Metrics = require('@overleaf/metrics') +const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') +const CollaboratorsInviteHandler = require('../Collaborators/CollaboratorsInviteHandler') +const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') +const PrivilegeLevels = require('../Authorization/PrivilegeLevels') +const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler') +const SessionManager = require('../Authentication/SessionManager') +const Errors = require('../Errors/Errors') +const HttpErrorHandler = require('../Errors/HttpErrorHandler') +const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler') +const DocstoreManager = require('../Docstore/DocstoreManager') +const logger = require('logger-sharelatex') +const { expressify } = require('../../util/promises') + +module.exports = { + joinProject: expressify(joinProject), + addDoc: expressify(addDoc), + addFolder: expressify(addFolder), + renameEntity: expressify(renameEntity), + moveEntity: expressify(moveEntity), + deleteDoc: expressify(deleteDoc), + deleteFile: expressify(deleteFile), + deleteFolder: expressify(deleteFolder), + deleteEntity: expressify(deleteEntity), + convertDocToFile: expressify(convertDocToFile), + _nameIsAcceptableLength, +} + +const unsupportedSpellcheckLanguages = [ + 'am', + 'hy', + 'bn', + 'gu', + 'he', + 'hi', + 'hu', + 'is', + 'kn', + 'ml', + 'mr', + 'or', + 'ss', + 'ta', + 'te', + 'uk', + 'uz', + 'zu', + 'fi', +] + +async function joinProject(req, res, next) { + const projectId = req.params.Project_id + let userId = req.query.user_id + if (userId === 'anonymous-user') { + userId = null + } + Metrics.inc('editor.join-project') + const { + project, + privilegeLevel, + isRestrictedUser, + } = await _buildJoinProjectView(req, projectId, userId) + if (!project) { + return res.sendStatus(403) + } + // Hide access tokens if this is not the project owner + TokenAccessHandler.protectTokens(project, privilegeLevel) + // Hide sensitive data if the user is restricted + if (isRestrictedUser) { + project.owner = { _id: project.owner._id } + project.members = [] + } + // Only show the 'renamed or deleted' message once + if (project.deletedByExternalDataSource) { + await ProjectDeleter.promises.unmarkAsDeletedByExternalSource(projectId) + } + // disable spellchecking for currently unsupported spell check languages + // preserve the value in the db so they can use it again once we add back + // support. + if ( + unsupportedSpellcheckLanguages.indexOf(project.spellCheckLanguage) !== -1 + ) { + project.spellCheckLanguage = '' + } + res.json({ + project, + privilegeLevel, + isRestrictedUser, + }) +} + +async function _buildJoinProjectView(req, projectId, userId) { + const project = await ProjectGetter.promises.getProjectWithoutDocLines( + projectId + ) + if (project == null) { + throw new Errors.NotFoundError('project not found') + } + let deletedDocsFromDocstore = [] + try { + deletedDocsFromDocstore = await DocstoreManager.promises.getAllDeletedDocs( + projectId + ) + } catch (err) { + // The query in docstore is not optimized at this time and fails for + // projects with many very large, deleted documents. + // Not serving the user with deletedDocs from docstore may cause a minor + // UI issue with deleted files that are no longer available for restore. + logger.warn( + { err, projectId }, + 'soft-failure when fetching deletedDocs from docstore' + ) + } + const members = await CollaboratorsGetter.promises.getInvitedMembersWithPrivilegeLevels( + projectId + ) + const token = TokenAccessHandler.getRequestToken(req, projectId) + const privilegeLevel = await AuthorizationManager.promises.getPrivilegeLevelForProject( + userId, + projectId, + token + ) + if (privilegeLevel == null || privilegeLevel === PrivilegeLevels.NONE) { + return { project: null, privilegeLevel: null, isRestrictedUser: false } + } + const invites = await CollaboratorsInviteHandler.promises.getAllInvites( + projectId + ) + const isTokenMember = await CollaboratorsHandler.promises.userIsTokenMember( + userId, + projectId + ) + const isRestrictedUser = AuthorizationManager.isRestrictedUser( + userId, + privilegeLevel, + isTokenMember + ) + return { + project: ProjectEditorHandler.buildProjectModelView( + project, + members, + invites, + deletedDocsFromDocstore + ), + privilegeLevel, + isRestrictedUser, + } +} + +function _nameIsAcceptableLength(name) { + return name != null && name.length < 150 && name.length !== 0 +} + +async function addDoc(req, res, next) { + const projectId = req.params.Project_id + const { name } = req.body + const parentFolderId = req.body.parent_folder_id + const userId = SessionManager.getLoggedInUserId(req.session) + + if (!_nameIsAcceptableLength(name)) { + return res.sendStatus(400) + } + try { + const doc = await EditorController.promises.addDoc( + projectId, + parentFolderId, + name, + [], + 'editor', + userId + ) + res.json(doc) + } catch (err) { + if (err.message === 'project_has_too_many_files') { + res.status(400).json(req.i18n.translate('project_has_too_many_files')) + } else { + next(err) + } + } +} + +async function addFolder(req, res, next) { + const projectId = req.params.Project_id + const { name } = req.body + const parentFolderId = req.body.parent_folder_id + const userId = SessionManager.getLoggedInUserId(req.session) + if (!_nameIsAcceptableLength(name)) { + return res.sendStatus(400) + } + try { + const doc = await EditorController.promises.addFolder( + projectId, + parentFolderId, + name, + 'editor', + userId + ) + res.json(doc) + } catch (err) { + if (err.message === 'project_has_too_many_files') { + res.status(400).json(req.i18n.translate('project_has_too_many_files')) + } else if (err.message === 'invalid element name') { + res.status(400).json(req.i18n.translate('invalid_file_name')) + } else { + next(err) + } + } +} + +async function renameEntity(req, res, next) { + const projectId = req.params.Project_id + const entityId = req.params.entity_id + const entityType = req.params.entity_type + const { name } = req.body + if (!_nameIsAcceptableLength(name)) { + return res.sendStatus(400) + } + const userId = SessionManager.getLoggedInUserId(req.session) + await EditorController.promises.renameEntity( + projectId, + entityId, + entityType, + name, + userId + ) + res.sendStatus(204) +} + +async function moveEntity(req, res, next) { + const projectId = req.params.Project_id + const entityId = req.params.entity_id + const entityType = req.params.entity_type + const folderId = req.body.folder_id + const userId = SessionManager.getLoggedInUserId(req.session) + await EditorController.promises.moveEntity( + projectId, + entityId, + folderId, + entityType, + userId + ) + res.sendStatus(204) +} + +async function deleteDoc(req, res, next) { + req.params.entity_type = 'doc' + await deleteEntity(req, res, next) +} + +async function deleteFile(req, res, next) { + req.params.entity_type = 'file' + await deleteEntity(req, res, next) +} + +async function deleteFolder(req, res, next) { + req.params.entity_type = 'folder' + await deleteEntity(req, res, next) +} + +async function deleteEntity(req, res, next) { + const projectId = req.params.Project_id + const entityId = req.params.entity_id + const entityType = req.params.entity_type + const userId = SessionManager.getLoggedInUserId(req.session) + await EditorController.promises.deleteEntity( + projectId, + entityId, + entityType, + 'editor', + userId + ) + res.sendStatus(204) +} + +async function convertDocToFile(req, res, next) { + const projectId = req.params.Project_id + const docId = req.params.entity_id + const { userId } = req.body + try { + const fileRef = await ProjectEntityUpdateHandler.promises.convertDocToFile( + projectId, + docId, + userId + ) + res.json({ fileId: fileRef._id.toString() }) + } catch (err) { + if (err instanceof Errors.NotFoundError) { + return HttpErrorHandler.notFound(req, res, 'Document not found') + } else if (err instanceof Errors.DocHasRangesError) { + return HttpErrorHandler.unprocessableEntity( + req, + res, + 'Document has comments or tracked changes' + ) + } else { + throw err + } + } +} diff --git a/services/web/app/src/Features/Editor/EditorRealTimeController.js b/services/web/app/src/Features/Editor/EditorRealTimeController.js new file mode 100644 index 0000000000..6f551950fd --- /dev/null +++ b/services/web/app/src/Features/Editor/EditorRealTimeController.js @@ -0,0 +1,51 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let EditorRealTimeController +const Settings = require('@overleaf/settings') +const Metrics = require('@overleaf/metrics') +const RedisWrapper = require('../../infrastructure/RedisWrapper') +const rclient = RedisWrapper.client('pubsub') +const os = require('os') +const crypto = require('crypto') + +const HOST = os.hostname() +const RND = crypto.randomBytes(4).toString('hex') // generate a random key for this process +let COUNT = 0 + +module.exports = EditorRealTimeController = { + emitToRoom(room_id, message, ...payload) { + // create a unique message id using a counter + const message_id = `web:${HOST}:${RND}-${COUNT++}` + var channel + if (room_id === 'all' || !Settings.publishOnIndividualChannels) { + channel = 'editor-events' + } else { + channel = `editor-events:${room_id}` + } + const blob = JSON.stringify({ + room_id, + message, + payload, + _id: message_id, + }) + Metrics.summary('redis.publish.editor-events', blob.length, { + status: message, + }) + return rclient.publish(channel, blob) + }, + + emitToAll(message, ...payload) { + return this.emitToRoom('all', message, ...Array.from(payload)) + }, +} diff --git a/services/web/app/src/Features/Editor/EditorRouter.js b/services/web/app/src/Features/Editor/EditorRouter.js new file mode 100644 index 0000000000..35e012e4aa --- /dev/null +++ b/services/web/app/src/Features/Editor/EditorRouter.js @@ -0,0 +1,84 @@ +const EditorHttpController = require('./EditorHttpController') +const AuthenticationController = require('../Authentication/AuthenticationController') +const AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware') +const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') +const { Joi, validate } = require('../../infrastructure/Validation') + +module.exports = { + apply(webRouter, apiRouter) { + webRouter.post( + '/project/:Project_id/doc', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + RateLimiterMiddleware.rateLimit({ + endpointName: 'add-doc-to-project', + params: ['Project_id'], + maxRequests: 30, + timeInterval: 60, + }), + EditorHttpController.addDoc + ) + webRouter.post( + '/project/:Project_id/folder', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + RateLimiterMiddleware.rateLimit({ + endpointName: 'add-folder-to-project', + params: ['Project_id'], + maxRequests: 60, + timeInterval: 60, + }), + EditorHttpController.addFolder + ) + + webRouter.post( + '/project/:Project_id/:entity_type/:entity_id/rename', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + EditorHttpController.renameEntity + ) + webRouter.post( + '/project/:Project_id/:entity_type/:entity_id/move', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + EditorHttpController.moveEntity + ) + + webRouter.delete( + '/project/:Project_id/file/:entity_id', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + EditorHttpController.deleteFile + ) + webRouter.delete( + '/project/:Project_id/doc/:entity_id', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + EditorHttpController.deleteDoc + ) + webRouter.delete( + '/project/:Project_id/folder/:entity_id', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + EditorHttpController.deleteFolder + ) + apiRouter.post( + '/project/:Project_id/doc/:entity_id/convert-to-file', + AuthenticationController.requirePrivateApiAuth(), + validate({ + body: Joi.object({ + userId: Joi.objectId().required(), + }), + }), + EditorHttpController.convertDocToFile + ) + + // Called by the real-time API to load up the current project state. + // This is a post request because it's more than just a getting of data. We take actions + // whenever a user joins a project, like updating the deleted status. + apiRouter.post( + '/project/:Project_id/join', + AuthenticationController.requirePrivateApiAuth(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'join-project', + params: ['Project_id'], + maxRequests: 45, + timeInterval: 60, + }), + EditorHttpController.joinProject + ) + }, +} diff --git a/services/web/app/src/Features/Email/Bodies/NoCTAEmailBody.js b/services/web/app/src/Features/Email/Bodies/NoCTAEmailBody.js new file mode 100644 index 0000000000..d0a97e5edc --- /dev/null +++ b/services/web/app/src/Features/Email/Bodies/NoCTAEmailBody.js @@ -0,0 +1,41 @@ +const _ = require('underscore') + +module.exports = _.template(`\ + + + + + + +
+ + + + + + + +
+ <% if (title) { %> +

+ <%= title %> +

+ <% } %> +
+

 

+ + <% if (greeting) { %> +

+ <%= greeting %> +

+ <% } %> + + <% (message).forEach(function(paragraph) { %> +

+ <%= paragraph %> +

+ <% }) %> +
+
+\ +`) diff --git a/services/web/app/src/Features/Email/Bodies/cta-email.js b/services/web/app/src/Features/Email/Bodies/cta-email.js new file mode 100644 index 0000000000..d2902d5d95 --- /dev/null +++ b/services/web/app/src/Features/Email/Bodies/cta-email.js @@ -0,0 +1,96 @@ +const _ = require('underscore') + +module.exports = _.template(`\ + + + + + + +
+ + + + + + + +
+ <% if (title) { %> +

+ <%= title %> +

+ <% } %> +
+

 

+ + <% if (greeting) { %> +

+ <%= greeting %> +

+ <% } %> + + <% (message).forEach(function(paragraph) { %> +

+ <%= paragraph %> +

+ <% }) %> + +

 

+ + + + + +
+ + + + +
+ + <%= ctaText %> + +
+
+ + <% if (secondaryMessage && secondaryMessage.length > 0) { %> +

 

+ + <% (secondaryMessage).forEach(function(paragraph) { %> +

+ <%= paragraph %> +

+ <% }) %> + <% } %> + +

 

+ +

+ If the button above does not appear, please copy and paste this link into your browser's address bar: +

+ +

+ <%= ctaURL %> +

+
+
+ <% if (gmailGoToAction) { %> + + <% } %> +\ +`) diff --git a/services/web/app/src/Features/Email/EmailBuilder.js b/services/web/app/src/Features/Email/EmailBuilder.js new file mode 100644 index 0000000000..4ba27d0cab --- /dev/null +++ b/services/web/app/src/Features/Email/EmailBuilder.js @@ -0,0 +1,559 @@ +const _ = require('underscore') +const settings = require('@overleaf/settings') +const moment = require('moment') +const EmailMessageHelper = require('./EmailMessageHelper') +const StringHelper = require('../Helpers/StringHelper') +const BaseWithHeaderEmailLayout = require('./Layouts/BaseWithHeaderEmailLayout') +const SpamSafe = require('./SpamSafe') +const ctaEmailBody = require('./Bodies/cta-email') +const NoCTAEmailBody = require('./Bodies/NoCTAEmailBody') + +function _emailBodyPlainText(content, opts, ctaEmail) { + let emailBody = `${content.greeting(opts, true)}` + emailBody += `\r\n\r\n` + emailBody += `${content.message(opts, true).join('\r\n\r\n')}` + + if (ctaEmail) { + emailBody += `\r\n\r\n` + emailBody += `${content.ctaText(opts, true)}: ${content.ctaURL(opts, true)}` + } + + if ( + content.secondaryMessage(opts, true) && + content.secondaryMessage(opts, true).length > 0 + ) { + emailBody += `\r\n\r\n` + emailBody += `${content.secondaryMessage(opts, true).join('\r\n\r\n')}` + } + + emailBody += `\r\n\r\n` + emailBody += `Regards,\r\nThe ${settings.appName} Team - ${settings.siteUrl}` + + if ( + settings.email && + settings.email.template && + settings.email.template.customFooter + ) { + emailBody += `\r\n\r\n` + emailBody += settings.email.template.customFooter + } + + return emailBody +} + +function ctaTemplate(content) { + if ( + !content.ctaURL || + !content.ctaText || + !content.message || + !content.subject + ) { + throw new Error('missing required CTA email content') + } + if (!content.title) { + content.title = () => {} + } + if (!content.greeting) { + content.greeting = () => 'Hi,' + } + if (!content.secondaryMessage) { + content.secondaryMessage = () => [] + } + if (!content.gmailGoToAction) { + content.gmailGoToAction = () => {} + } + return { + subject(opts) { + return content.subject(opts) + }, + layout: BaseWithHeaderEmailLayout, + plainTextTemplate(opts) { + return _emailBodyPlainText(content, opts, true) + }, + compiledTemplate(opts) { + return ctaEmailBody({ + title: content.title(opts), + greeting: content.greeting(opts), + message: content.message(opts), + secondaryMessage: content.secondaryMessage(opts), + ctaText: content.ctaText(opts), + ctaURL: content.ctaURL(opts), + gmailGoToAction: content.gmailGoToAction(opts), + StringHelper, + }) + }, + } +} + +function NoCTAEmailTemplate(content) { + if (content.greeting == null) { + content.greeting = () => 'Hi,' + } + if (!content.message) { + throw new Error('missing message') + } + return { + subject(opts) { + return content.subject(opts) + }, + layout: BaseWithHeaderEmailLayout, + plainTextTemplate(opts) { + return `\ +${content.greeting(opts)} + +${content.message(opts, true).join('\r\n\r\n')} + +Regards, +The ${settings.appName} Team - ${settings.siteUrl}\ +` + }, + compiledTemplate(opts) { + return NoCTAEmailBody({ + title: + typeof content.title === 'function' ? content.title(opts) : undefined, + greeting: content.greeting(opts), + message: content.message(opts), + StringHelper, + }) + }, + } +} + +function buildEmail(templateName, opts) { + const template = templates[templateName] + opts.siteUrl = settings.siteUrl + opts.body = template.compiledTemplate(opts) + return { + subject: template.subject(opts), + html: template.layout(opts), + text: template.plainTextTemplate && template.plainTextTemplate(opts), + } +} + +const templates = {} + +templates.registered = ctaTemplate({ + subject() { + return `Activate your ${settings.appName} Account` + }, + message(opts) { + return [ + `Congratulations, you've just had an account created for you on ${ + settings.appName + } with the email address '${_.escape(opts.to)}'.`, + 'Click here to set your password and log in:', + ] + }, + secondaryMessage() { + return [ + `If you have any questions or problems, please contact ${settings.adminEmail}`, + ] + }, + ctaText() { + return 'Set password' + }, + ctaURL(opts) { + return opts.setNewPasswordUrl + }, +}) + +templates.canceledSubscription = ctaTemplate({ + subject() { + return `${settings.appName} thoughts` + }, + message() { + return [ + `We are sorry to see you cancelled your ${settings.appName} premium subscription. Would you mind giving us some feedback on what the site is lacking at the moment via this quick survey?`, + ] + }, + secondaryMessage() { + return ['Thank you in advance!'] + }, + ctaText() { + return 'Leave Feedback' + }, + ctaURL(opts) { + return 'https://docs.google.com/forms/d/e/1FAIpQLSfa7z_s-cucRRXm70N4jEcSbFsZeb0yuKThHGQL8ySEaQzF0Q/viewform?usp=sf_link' + }, +}) + +templates.reactivatedSubscription = ctaTemplate({ + subject() { + return `Subscription Reactivated - ${settings.appName}` + }, + message(opts) { + return ['Your subscription was reactivated successfully.'] + }, + ctaText() { + return 'View Subscription Dashboard' + }, + ctaURL(opts) { + return `${settings.siteUrl}/user/subscription` + }, +}) + +templates.passwordResetRequested = ctaTemplate({ + subject() { + return `Password Reset - ${settings.appName}` + }, + title() { + return 'Password Reset' + }, + message() { + return [`We got a request to reset your ${settings.appName} password.`] + }, + secondaryMessage() { + return [ + "If you ignore this message, your password won't be changed.", + "If you didn't request a password reset, let us know.", + ] + }, + ctaText() { + return 'Reset password' + }, + ctaURL(opts) { + return opts.setNewPasswordUrl + }, +}) + +templates.confirmEmail = ctaTemplate({ + subject() { + return `Confirm Email - ${settings.appName}` + }, + title() { + return 'Confirm Email' + }, + message(opts) { + return [ + `Please confirm that you have added a new email, ${opts.to}, to your ${settings.appName} account.`, + ] + }, + secondaryMessage() { + return [ + 'If you did not request this, you can simply ignore this message.', + `If you have any questions or trouble confirming your email address, please get in touch with our support team at ${settings.adminEmail}.`, + ] + }, + ctaText() { + return 'Confirm Email' + }, + ctaURL(opts) { + return opts.confirmEmailUrl + }, +}) + +templates.projectInvite = ctaTemplate({ + subject(opts) { + return `${_.escape( + SpamSafe.safeProjectName(opts.project.name, 'New Project') + )} - shared by ${_.escape( + SpamSafe.safeEmail(opts.owner.email, 'a collaborator') + )}` + }, + title(opts) { + return `${_.escape( + SpamSafe.safeProjectName(opts.project.name, 'New Project') + )} - shared by ${_.escape( + SpamSafe.safeEmail(opts.owner.email, 'a collaborator') + )}` + }, + message(opts) { + return [ + `${_.escape( + SpamSafe.safeEmail(opts.owner.email, 'a collaborator') + )} wants to share ${_.escape( + SpamSafe.safeProjectName(opts.project.name, 'a new project') + )} with you.`, + ] + }, + ctaText() { + return 'View project' + }, + ctaURL(opts) { + return opts.inviteUrl + }, + gmailGoToAction(opts) { + return { + target: opts.inviteUrl, + name: 'View project', + description: `Join ${_.escape( + SpamSafe.safeProjectName(opts.project.name, 'project') + )} at ${settings.appName}`, + } + }, +}) + +templates.reconfirmEmail = ctaTemplate({ + subject() { + return `Reconfirm Email - ${settings.appName}` + }, + title() { + return 'Reconfirm Email' + }, + message(opts) { + return [ + `Please reconfirm your email address, ${opts.to}, on your ${settings.appName} account.`, + ] + }, + secondaryMessage() { + return [ + 'If you did not request this, you can simply ignore this message.', + `If you have any questions or trouble confirming your email address, please get in touch with our support team at ${settings.adminEmail}.`, + ] + }, + ctaText() { + return 'Reconfirm Email' + }, + ctaURL(opts) { + return opts.confirmEmailUrl + }, +}) + +templates.verifyEmailToJoinTeam = ctaTemplate({ + subject(opts) { + return `${_.escape( + _formatUserNameAndEmail(opts.inviter, 'A collaborator') + )} has invited you to join a team on ${settings.appName}` + }, + title(opts) { + return `${_.escape( + _formatUserNameAndEmail(opts.inviter, 'A collaborator') + )} has invited you to join a team on ${settings.appName}` + }, + message(opts) { + return [ + `Please click the button below to join the team and enjoy the benefits of an upgraded ${settings.appName} account.`, + ] + }, + ctaText(opts) { + return 'Join now' + }, + ctaURL(opts) { + return opts.acceptInviteUrl + }, +}) + +templates.testEmail = ctaTemplate({ + subject() { + return `A Test Email from ${settings.appName}` + }, + title() { + return `A Test Email from ${settings.appName}` + }, + greeting() { + return 'Hi,' + }, + message() { + return [`This is a test Email from ${settings.appName}`] + }, + ctaText() { + return `Open ${settings.appName}` + }, + ctaURL() { + return settings.siteUrl + }, +}) + +templates.ownershipTransferConfirmationPreviousOwner = NoCTAEmailTemplate({ + subject(opts) { + return `Project ownership transfer - ${settings.appName}` + }, + title(opts) { + const projectName = _.escape( + SpamSafe.safeProjectName(opts.project.name, 'Your project') + ) + return `${projectName} - Owner change` + }, + message(opts, isPlainText) { + const nameAndEmail = _.escape( + _formatUserNameAndEmail(opts.newOwner, 'a collaborator') + ) + const projectName = _.escape( + SpamSafe.safeProjectName(opts.project.name, 'your project') + ) + const projectNameDisplay = isPlainText + ? projectName + : `${projectName}` + return [ + `As per your request, we have made ${nameAndEmail} the owner of ${projectNameDisplay}.`, + `If you haven't asked to change the owner of ${projectNameDisplay}, please get in touch with us via ${settings.adminEmail}.`, + ] + }, +}) + +templates.ownershipTransferConfirmationNewOwner = ctaTemplate({ + subject(opts) { + return `Project ownership transfer - ${settings.appName}` + }, + title(opts) { + const projectName = _.escape( + SpamSafe.safeProjectName(opts.project.name, 'Your project') + ) + return `${projectName} - Owner change` + }, + message(opts, isPlainText) { + const nameAndEmail = _.escape( + _formatUserNameAndEmail(opts.previousOwner, 'A collaborator') + ) + const projectName = _.escape( + SpamSafe.safeProjectName(opts.project.name, 'a project') + ) + const projectNameEmphasized = isPlainText + ? projectName + : `${projectName}` + return [ + `${nameAndEmail} has made you the owner of ${projectNameEmphasized}. You can now manage ${projectName} sharing settings.`, + ] + }, + ctaText(opts) { + return 'View project' + }, + ctaURL(opts) { + const projectUrl = `${ + settings.siteUrl + }/project/${opts.project._id.toString()}` + return projectUrl + }, +}) + +templates.userOnboardingEmail = NoCTAEmailTemplate({ + subject(opts) { + return `Getting more out of ${settings.appName}` + }, + greeting(opts) { + return '' + }, + title(opts) { + return `Getting more out of ${settings.appName}` + }, + message(opts, isPlainText) { + const learnLatexLink = EmailMessageHelper.displayLink( + 'Learn LaTeX in 30 minutes', + `${settings.siteUrl}/learn/latex/Learn_LaTeX_in_30_minutes?utm_source=overleaf&utm_medium=email&utm_campaign=onboarding`, + isPlainText + ) + const templatesLinks = EmailMessageHelper.displayLink( + 'Find a beautiful template', + `${settings.siteUrl}/latex/templates?utm_source=overleaf&utm_medium=email&utm_campaign=onboarding`, + isPlainText + ) + const collaboratorsLink = EmailMessageHelper.displayLink( + 'Work with your collaborators', + `${settings.siteUrl}/learn/how-to/Sharing_a_project?utm_source=overleaf&utm_medium=email&utm_campaign=onboarding`, + isPlainText + ) + const siteLink = EmailMessageHelper.displayLink( + 'www.overleaf.com', + settings.siteUrl, + isPlainText + ) + const userSettingsLink = EmailMessageHelper.displayLink( + 'here', + `${settings.siteUrl}/user/settings`, + isPlainText + ) + const onboardingSurveyLink = EmailMessageHelper.displayLink( + 'Join our user feedback programme', + 'https://forms.gle/DB7pdk2B1VFQqVVB9', + isPlainText + ) + return [ + `Thanks for signing up for ${settings.appName} recently. We hope you've been finding it useful! Here are some key features to help you get the most out of the service:`, + `${learnLatexLink}: In this tutorial we provide a quick and easy first introduction to LaTeX with no prior knowledge required. By the time you are finished, you will have written your first LaTeX document!`, + `${templatesLinks}: If you're looking for a template or example to get started, we've a large selection available in our template gallery, including CVs, project reports, journal articles and more.`, + `${collaboratorsLink}: One of the key features of Overleaf is the ability to share projects and collaborate on them with other users. Find out how to share your projecs with your colleagues in this quick how-to guide.`, + `${onboardingSurveyLink} to help us make Overleaf even better!`, + 'Thanks again for using Overleaf :)', + `John`, + `Dr John Hammersley
Co-founder & CEO
${siteLink}
`, + `Don't want onboarding emails like this from us? Don't worry, this is the only one. If you've previously subscribed to emails about product offers and company news and events, you can unsubscribe ${userSettingsLink}.`, + ] + }, +}) + +templates.securityAlert = NoCTAEmailTemplate({ + subject(opts) { + return `Overleaf security note: ${opts.action}` + }, + title(opts) { + return opts.action.charAt(0).toUpperCase() + opts.action.slice(1) + }, + message(opts, isPlainText) { + const dateFormatted = moment().format('dddd D MMMM YYYY') + const timeFormatted = moment().format('HH:mm') + const helpLink = EmailMessageHelper.displayLink( + 'quick guide', + `${settings.siteUrl}/learn/how-to/Keeping_your_account_secure`, + isPlainText + ) + + const actionDescribed = EmailMessageHelper.cleanHTML( + opts.actionDescribed, + isPlainText + ) + + if (!opts.message) { + opts.message = [] + } + const message = opts.message.map(m => { + return EmailMessageHelper.cleanHTML(m, isPlainText) + }) + + return [ + `We are writing to let you know that ${actionDescribed} on ${dateFormatted} at ${timeFormatted} GMT.`, + ...message, + `If this was you, you can ignore this email.`, + `If this was not you, we recommend getting in touch with our support team at ${settings.adminEmail} to report this as potentially suspicious activity on your account.`, + `We also encourage you to read our ${helpLink} to keeping your ${settings.appName} account safe.`, + ] + }, +}) + +templates.SAMLDataCleared = ctaTemplate({ + subject(opts) { + return `Institutional Login No Longer Linked - ${settings.appName}` + }, + title(opts) { + return 'Institutional Login No Longer Linked' + }, + message(opts, isPlainText) { + return [ + `We're writing to let you know that due to a bug on our end, we've had to temporarily disable logging into your ${settings.appName} through your institution.`, + `To get it going again, you'll need to relink your institutional email address to your ${settings.appName} account via your settings.`, + ] + }, + secondaryMessage() { + return [ + `If you ordinarily log in to your ${settings.appName} account through your institution, you may need to set or reset your password to regain access to your account first.`, + 'This bug did not affect the security of any accounts, but it may have affected license entitlements for a small number of users. We are sorry for any inconvenience that this may cause for you.', + `If you have any questions, please get in touch with our support team at ${settings.adminEmail} or by replying to this email.`, + ] + }, + ctaText(opts) { + return 'Update my Emails and Affiliations' + }, + ctaURL(opts) { + return `${settings.siteUrl}/user/settings` + }, +}) + +function _formatUserNameAndEmail(user, placeholder) { + if (user.first_name && user.last_name) { + const fullName = `${user.first_name} ${user.last_name}` + if (SpamSafe.isSafeUserName(fullName)) { + if (SpamSafe.isSafeEmail(user.email)) { + return `${fullName} (${user.email})` + } else { + return fullName + } + } + } + return SpamSafe.safeEmail(user.email, placeholder) +} + +module.exports = { + templates, + ctaTemplate, + NoCTAEmailTemplate, + buildEmail, +} diff --git a/services/web/app/src/Features/Email/EmailHandler.js b/services/web/app/src/Features/Email/EmailHandler.js new file mode 100644 index 0000000000..6c17752f8b --- /dev/null +++ b/services/web/app/src/Features/Email/EmailHandler.js @@ -0,0 +1,24 @@ +const { callbackify } = require('util') +const Settings = require('@overleaf/settings') +const EmailBuilder = require('./EmailBuilder') +const EmailSender = require('./EmailSender') + +const EMAIL_SETTINGS = Settings.email || {} + +module.exports = { + sendEmail: callbackify(sendEmail), + promises: { + sendEmail, + }, +} + +async function sendEmail(emailType, opts) { + const email = EmailBuilder.buildEmail(emailType, opts) + if (email.type === 'lifecycle' && !EMAIL_SETTINGS.lifecycle) { + return + } + opts.html = email.html + opts.text = email.text + opts.subject = email.subject + await EmailSender.promises.sendEmail(opts) +} diff --git a/services/web/app/src/Features/Email/EmailMessageHelper.js b/services/web/app/src/Features/Email/EmailMessageHelper.js new file mode 100644 index 0000000000..8c6b2fd3bd --- /dev/null +++ b/services/web/app/src/Features/Email/EmailMessageHelper.js @@ -0,0 +1,27 @@ +const sanitizeHtml = require('sanitize-html') +const sanitizeOptions = { + html: { + allowedTags: ['span', 'b', 'br', 'i'], + allowedAttributes: { + span: ['style', 'class'], + }, + }, + plainText: { + allowedTags: [], + allowedAttributes: {}, + }, +} + +function cleanHTML(text, isPlainText) { + if (!isPlainText) return sanitizeHtml(text, sanitizeOptions.html) + return sanitizeHtml(text, sanitizeOptions.plainText) +} + +function displayLink(text, url, isPlainText) { + return isPlainText ? `${text} (${url})` : `${text}` +} + +module.exports = { + cleanHTML, + displayLink, +} diff --git a/services/web/app/src/Features/Email/EmailOptionsHelper.js b/services/web/app/src/Features/Email/EmailOptionsHelper.js new file mode 100644 index 0000000000..2d064008b2 --- /dev/null +++ b/services/web/app/src/Features/Email/EmailOptionsHelper.js @@ -0,0 +1,20 @@ +function _getIndefiniteArticle(providerName) { + const vowels = ['a', 'e', 'i', 'o', 'u'] + if (vowels.includes(providerName.charAt(0).toLowerCase())) return 'an' + return 'a' +} + +function linkOrUnlink(accountLinked, providerName, email) { + const action = accountLinked ? 'linked' : 'no longer linked' + const actionDescribed = accountLinked ? 'was linked to' : 'was unlinked from' + const indefiniteArticle = _getIndefiniteArticle(providerName) + return { + to: email, + action: `${providerName} account ${action}`, + actionDescribed: `${indefiniteArticle} ${providerName} account ${actionDescribed} your account ${email}`, + } +} + +module.exports = { + linkOrUnlink, +} diff --git a/services/web/app/src/Features/Email/EmailSender.js b/services/web/app/src/Features/Email/EmailSender.js new file mode 100644 index 0000000000..ff2bc0f2c5 --- /dev/null +++ b/services/web/app/src/Features/Email/EmailSender.js @@ -0,0 +1,117 @@ +const { callbackify } = require('util') +const logger = require('logger-sharelatex') +const metrics = require('@overleaf/metrics') +const Settings = require('@overleaf/settings') +const nodemailer = require('nodemailer') +const sesTransport = require('nodemailer-ses-transport') +const mandrillTransport = require('nodemailer-mandrill-transport') +const OError = require('@overleaf/o-error') +const RateLimiter = require('../../infrastructure/RateLimiter') +const _ = require('underscore') + +const EMAIL_SETTINGS = Settings.email || {} + +module.exports = { + sendEmail: callbackify(sendEmail), + promises: { + sendEmail, + }, +} + +const client = getClient() + +function getClient() { + let client + if (EMAIL_SETTINGS.parameters) { + const emailParameters = EMAIL_SETTINGS.parameters + if (emailParameters.AWSAccessKeyID || EMAIL_SETTINGS.driver === 'ses') { + logger.log('using aws ses for email') + client = nodemailer.createTransport(sesTransport(emailParameters)) + } else if (emailParameters.sendgridApiKey) { + throw new OError( + 'sendgridApiKey configuration option is deprecated, use SMTP instead' + ) + } else if (emailParameters.MandrillApiKey) { + logger.log('using mandril for email') + client = nodemailer.createTransport( + mandrillTransport({ + auth: { + apiKey: emailParameters.MandrillApiKey, + }, + }) + ) + } else { + logger.log('using smtp for email') + const smtp = _.pick( + emailParameters, + 'host', + 'port', + 'secure', + 'auth', + 'ignoreTLS', + 'logger', + 'name' + ) + client = nodemailer.createTransport(smtp) + } + } else { + logger.warn( + 'Email transport and/or parameters not defined. No emails will be sent.' + ) + client = { + async sendMail(options) { + logger.log({ options }, 'Would send email if enabled.') + }, + } + } + return client +} + +async function sendEmail(options) { + try { + const canContinue = await checkCanSendEmail(options) + if (!canContinue) { + logger.log( + { + sendingUser_id: options.sendingUser_id, + to: options.to, + subject: options.subject, + canContinue, + }, + 'rate limit hit for sending email, not sending' + ) + throw new OError('rate limit hit sending email') + } + metrics.inc('email') + const sendMailOptions = { + to: options.to, + from: EMAIL_SETTINGS.fromAddress || '', + subject: options.subject, + html: options.html, + text: options.text, + replyTo: options.replyTo || EMAIL_SETTINGS.replyToAddress, + socketTimeout: 30 * 1000, + } + if (EMAIL_SETTINGS.textEncoding != null) { + sendMailOptions.textEncoding = EMAIL_SETTINGS.textEncoding + } + await client.sendMail(sendMailOptions) + } catch (err) { + throw new OError('error sending message').withCause(err) + } +} + +async function checkCanSendEmail(options) { + if (options.sendingUser_id == null) { + // email not sent from user, not rate limited + return true + } + const opts = { + endpointName: 'send_email', + timeInterval: 60 * 60 * 3, + subjectName: options.sendingUser_id, + throttle: 100, + } + const allowed = await RateLimiter.promises.addCount(opts) + return allowed +} diff --git a/services/web/app/src/Features/Email/Layouts/BaseWithHeaderEmailLayout.js b/services/web/app/src/Features/Email/Layouts/BaseWithHeaderEmailLayout.js new file mode 100644 index 0000000000..851ca417ba --- /dev/null +++ b/services/web/app/src/Features/Email/Layouts/BaseWithHeaderEmailLayout.js @@ -0,0 +1,390 @@ +const _ = require('underscore') +const settings = require('@overleaf/settings') + +module.exports = _.template(`\ + + + + + + + + + + + + + + + +
+
+ +
+
+ + +
+
+

+ ${settings.appName} +

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

+ ${ + settings.email && + settings.email.template && + settings.email.template.customFooter + ? `${settings.email.template.customFooter}
` + : '' + }${settings.appName} • ${ + settings.siteUrl +} +

+
+
+ +
+
+ +
                                                           
+ +\ +`) diff --git a/services/web/app/src/Features/Email/SpamSafe.js b/services/web/app/src/Features/Email/SpamSafe.js new file mode 100644 index 0000000000..7961113487 --- /dev/null +++ b/services/web/app/src/Features/Email/SpamSafe.js @@ -0,0 +1,56 @@ +const XRegExp = require('xregexp') + +// A note about SAFE_REGEX: +// We have to escape the escape characters because XRegExp compiles it first. +// So it's equivalent to `^[\p{L}\p{N}\s\-_!&\(\)]+$] +// \p{L} = any letter in any language +// \p{N} = any kind of numeric character +// https://www.regular-expressions.info/unicode.html#prop is a good resource for +// more obscure regex features. standard RegExp does not support these + +const HAN_REGEX = XRegExp('\\p{Han}') +const SAFE_REGEX = XRegExp("^[\\p{L}\\p{N}\\s\\-_!'&\\(\\)]+$") +const EMAIL_REGEX = XRegExp('^[\\p{L}\\p{N}.+_-]+@[\\w.-]+$') + +const SpamSafe = { + isSafeUserName(name) { + return SAFE_REGEX.test(name) && name.length <= 30 + }, + + isSafeProjectName(name) { + if (HAN_REGEX.test(name)) { + return SAFE_REGEX.test(name) && name.length <= 30 + } + return SAFE_REGEX.test(name) && name.length <= 100 + }, + + isSafeEmail(email) { + return EMAIL_REGEX.test(email) && email.length <= 40 + }, + + safeUserName(name, alternative, project) { + if (project == null) { + project = false + } + if (SpamSafe.isSafeUserName(name)) { + return name + } + return alternative + }, + + safeProjectName(name, alternative) { + if (SpamSafe.isSafeProjectName(name)) { + return name + } + return alternative + }, + + safeEmail(email, alternative) { + if (SpamSafe.isSafeEmail(email)) { + return email + } + return alternative + }, +} + +module.exports = SpamSafe diff --git a/services/web/app/src/Features/Errors/ErrorController.js b/services/web/app/src/Features/Errors/ErrorController.js new file mode 100644 index 0000000000..d223abbf3c --- /dev/null +++ b/services/web/app/src/Features/Errors/ErrorController.js @@ -0,0 +1,103 @@ +let ErrorController +const Errors = require('./Errors') +const logger = require('logger-sharelatex') +const SessionManager = require('../Authentication/SessionManager') +const SamlLogHandler = require('../SamlLog/SamlLogHandler') +const HttpErrorHandler = require('./HttpErrorHandler') + +module.exports = ErrorController = { + notFound(req, res) { + res.status(404) + res.render('general/404', { title: 'page_not_found' }) + }, + + forbidden(req, res) { + res.status(403) + res.render('user/restricted') + }, + + serverError(req, res) { + res.status(500) + res.render('general/500', { title: 'Server Error' }) + }, + + handleError(error, req, res, next) { + const user = SessionManager.getSessionUser(req.session) + // log errors related to SAML flow + if (req.session && req.session.saml) { + SamlLogHandler.log(req.session.saml.universityId, req.sessionID, { + error: { + message: error && error.message, + stack: error && error.stack, + }, + body: req.body, + path: req.path, + query: req.query, + saml: req.session.saml, + user_id: user && user._id, + }) + } + if (error.code === 'EBADCSRFTOKEN') { + logger.warn( + { err: error, url: req.url, method: req.method, user }, + 'invalid csrf' + ) + res.sendStatus(403) + } else if (error instanceof Errors.NotFoundError) { + logger.warn({ err: error, url: req.url }, 'not found error') + ErrorController.notFound(req, res) + } else if ( + error instanceof URIError && + error.message.match(/^Failed to decode param/) + ) { + logger.warn({ err: error, url: req.url }, 'Express URIError') + res.status(400) + res.render('general/500', { title: 'Invalid Error' }) + } else if (error instanceof Errors.ForbiddenError) { + logger.error({ err: error }, 'forbidden error') + ErrorController.forbidden(req, res) + } else if (error instanceof Errors.TooManyRequestsError) { + logger.warn({ err: error, url: req.url }, 'too many requests error') + res.sendStatus(429) + } else if (error instanceof Errors.InvalidError) { + logger.warn({ err: error, url: req.url }, 'invalid error') + res.status(400) + res.send(error.message) + } else if (error instanceof Errors.InvalidNameError) { + logger.warn({ err: error, url: req.url }, 'invalid name error') + res.status(400) + res.send(error.message) + } else if (error instanceof Errors.SAMLSessionDataMissing) { + logger.warn( + { err: error, url: req.url }, + 'missing SAML session data error' + ) + HttpErrorHandler.badRequest(req, res, error.message) + } else { + logger.error( + { err: error, url: req.url, method: req.method, user }, + 'error passed to top level next middleware' + ) + ErrorController.serverError(req, res) + } + }, + + handleApiError(error, req, res, next) { + if (error instanceof Errors.NotFoundError) { + logger.warn({ err: error, url: req.url }, 'not found error') + res.sendStatus(404) + } else if ( + error instanceof URIError && + error.message.match(/^Failed to decode param/) + ) { + logger.warn({ err: error, url: req.url }, 'Express URIError') + res.sendStatus(400) + } else { + logger.error( + { err: error, url: req.url, method: req.method }, + 'error passed to top level next middleware' + ) + res.sendStatus(500) + } + }, +} diff --git a/services/web/app/src/Features/Errors/Errors.js b/services/web/app/src/Features/Errors/Errors.js new file mode 100644 index 0000000000..a94153a917 --- /dev/null +++ b/services/web/app/src/Features/Errors/Errors.js @@ -0,0 +1,227 @@ +const OError = require('@overleaf/o-error') +const settings = require('@overleaf/settings') + +// Error class for legacy errors so they inherit OError while staying +// backward-compatible (can be instantiated with string as argument instead +// of object) +class BackwardCompatibleError extends OError { + constructor(messageOrOptions) { + if (typeof messageOrOptions === 'string') { + super(messageOrOptions) + } else if (messageOrOptions) { + const { message, info } = messageOrOptions + super(message, info) + } else { + super() + } + } +} + +// Error class that facilitates the migration to OError v3 by providing +// a signature in which the 2nd argument can be an object containing +// the `info` object. +class OErrorV2CompatibleError extends OError { + constructor(message, options) { + if (options) { + super(message, options.info) + } else { + super(message) + } + } +} + +class NotFoundError extends BackwardCompatibleError {} + +class ForbiddenError extends BackwardCompatibleError {} + +class ServiceNotConfiguredError extends BackwardCompatibleError {} + +class TooManyRequestsError extends BackwardCompatibleError {} + +class InvalidNameError extends BackwardCompatibleError {} + +class UnsupportedFileTypeError extends BackwardCompatibleError {} + +class FileTooLargeError extends BackwardCompatibleError {} + +class UnsupportedExportRecordsError extends BackwardCompatibleError {} + +class V1HistoryNotSyncedError extends BackwardCompatibleError {} + +class ProjectHistoryDisabledError extends BackwardCompatibleError {} + +class V1ConnectionError extends BackwardCompatibleError {} + +class UnconfirmedEmailError extends BackwardCompatibleError {} + +class EmailExistsError extends OErrorV2CompatibleError { + constructor(options) { + super('Email already exists', options) + } +} + +class InvalidError extends BackwardCompatibleError {} + +class NotInV2Error extends BackwardCompatibleError {} + +class SLInV2Error extends BackwardCompatibleError {} + +class SAMLIdentityExistsError extends OError { + get i18nKey() { + return 'institution_account_tried_to_add_already_registered' + } +} + +class SAMLAlreadyLinkedError extends OError { + get i18nKey() { + return 'institution_account_tried_to_add_already_linked' + } +} + +class SAMLEmailNotAffiliatedError extends OError { + get i18nKey() { + return 'institution_account_tried_to_add_not_affiliated' + } +} + +class SAMLEmailAffiliatedWithAnotherInstitutionError extends OError { + get i18nKey() { + return 'institution_account_tried_to_add_affiliated_with_another_institution' + } +} + +class SAMLSessionDataMissing extends BackwardCompatibleError { + constructor(arg) { + super(arg) + + const samlSession = + typeof arg === 'object' && arg !== null && arg.samlSession + ? arg.samlSession + : {} + this.tryAgain = true + const { + universityId, + universityName, + externalUserId, + institutionEmail, + } = samlSession + + if ( + !universityId && + !universityName && + !externalUserId && + !institutionEmail + ) { + this.message = 'Missing session data.' + } else if ( + !institutionEmail && + samlSession && + samlSession.userEmailAttributeUnreliable + ) { + this.tryAgain = false + this.message = `Your account settings at your institution prevent us from accessing your email address. You will need to make your email address public at your institution in order to link with ${settings.appName}. Please contact your IT department if you have any questions.` + } else if (!institutionEmail) { + this.message = + 'Unable to confirm your institutional email address. The institutional identity provider did not provide an email address in the expected attribute. Please contact us if this keeps happening.' + } + } +} + +class ThirdPartyIdentityExistsError extends BackwardCompatibleError { + constructor(arg) { + super(arg) + if (!this.message) { + this.message = + 'provider and external id already linked to another account' + } + } +} + +class ThirdPartyUserNotFoundError extends BackwardCompatibleError { + constructor(arg) { + super(arg) + if (!this.message) { + this.message = 'user not found for provider and external id' + } + } +} + +class SubscriptionAdminDeletionError extends OErrorV2CompatibleError { + constructor(options) { + super('subscription admins cannot be deleted', options) + } +} + +class ProjectNotFoundError extends OErrorV2CompatibleError { + constructor(options) { + super('project not found', options) + } +} + +class UserNotFoundError extends OErrorV2CompatibleError { + constructor(options) { + super('user not found', options) + } +} + +class UserNotCollaboratorError extends OErrorV2CompatibleError { + constructor(options) { + super('user not a collaborator', options) + } +} + +class DocHasRangesError extends OErrorV2CompatibleError { + constructor(options) { + super('document has ranges', options) + } +} + +class InvalidQueryError extends OErrorV2CompatibleError { + constructor(options) { + super('invalid search query', options) + } +} + +class AffiliationError extends OError {} + +class InvalidInstitutionalEmailError extends OError { + get i18nKey() { + return 'invalid_institutional_email' + } +} + +module.exports = { + OError, + BackwardCompatibleError, + NotFoundError, + ForbiddenError, + ServiceNotConfiguredError, + TooManyRequestsError, + InvalidNameError, + UnsupportedFileTypeError, + FileTooLargeError, + UnsupportedExportRecordsError, + V1HistoryNotSyncedError, + ProjectHistoryDisabledError, + V1ConnectionError, + UnconfirmedEmailError, + EmailExistsError, + InvalidError, + NotInV2Error, + SAMLIdentityExistsError, + SAMLAlreadyLinkedError, + SAMLEmailNotAffiliatedError, + SAMLEmailAffiliatedWithAnotherInstitutionError, + SAMLSessionDataMissing, + SLInV2Error, + ThirdPartyIdentityExistsError, + ThirdPartyUserNotFoundError, + SubscriptionAdminDeletionError, + ProjectNotFoundError, + UserNotFoundError, + UserNotCollaboratorError, + DocHasRangesError, + InvalidQueryError, + AffiliationError, + InvalidInstitutionalEmailError, +} diff --git a/services/web/app/src/Features/Errors/HttpErrorHandler.js b/services/web/app/src/Features/Errors/HttpErrorHandler.js new file mode 100644 index 0000000000..cdbd8caec6 --- /dev/null +++ b/services/web/app/src/Features/Errors/HttpErrorHandler.js @@ -0,0 +1,161 @@ +const logger = require('logger-sharelatex') +const Settings = require('@overleaf/settings') + +function renderJSONError(res, message, info = {}) { + if (info.message) { + logger.warn( + info, + `http error info shouldn't contain a 'message' field, will be overridden` + ) + } + if (message != null) { + res.json({ ...info, message }) + } else { + res.json(info) + } +} + +function handleGeneric500Error(req, res, statusCode, message) { + res.status(statusCode) + switch (req.accepts(['html', 'json'])) { + case 'html': + return res.render('general/500', { title: 'Server Error' }) + case 'json': + return renderJSONError(res, message) + default: + return res.send('internal server error') + } +} + +function handleGeneric400Error(req, res, statusCode, message, info = {}) { + res.status(statusCode) + switch (req.accepts(['html', 'json'])) { + case 'html': + return res.render('general/400', { + title: 'Client Error', + message: message, + }) + case 'json': + return renderJSONError(res, message, info) + default: + return res.send('client error') + } +} + +let HttpErrorHandler +module.exports = HttpErrorHandler = { + handleErrorByStatusCode(req, res, error, statusCode) { + const is400Error = statusCode >= 400 && statusCode < 500 + const is500Error = statusCode >= 500 && statusCode < 600 + + if (is400Error) { + logger.warn(error) + } else if (is500Error) { + logger.error(error) + } + + if (statusCode === 403) { + HttpErrorHandler.forbidden(req, res) + } else if (statusCode === 404) { + HttpErrorHandler.notFound(req, res) + } else if (statusCode === 409) { + HttpErrorHandler.conflict(req, res, '') + } else if (statusCode === 422) { + HttpErrorHandler.unprocessableEntity(req, res) + } else if (is400Error) { + handleGeneric400Error(req, res, statusCode) + } else if (is500Error) { + handleGeneric500Error(req, res, statusCode) + } else { + logger.error( + { err: error, statusCode }, + `unable to handle error with status code ${statusCode}` + ) + res.sendStatus(500) + } + }, + + badRequest(req, res, message, info = {}) { + handleGeneric400Error(req, res, 400, message, info) + }, + + conflict(req, res, message, info = {}) { + res.status(409) + switch (req.accepts(['html', 'json'])) { + case 'html': + return res.render('general/400', { + title: 'Client Error', + message: message, + }) + case 'json': + return renderJSONError(res, message, info) + default: + return res.send('conflict') + } + }, + + forbidden(req, res, message = 'restricted', info = {}) { + res.status(403) + switch (req.accepts(['html', 'json'])) { + case 'html': + return res.render('user/restricted', { title: 'restricted' }) + case 'json': + return renderJSONError(res, message, info) + default: + return res.send('restricted') + } + }, + + notFound(req, res, message = 'not found', info = {}) { + res.status(404) + switch (req.accepts(['html', 'json'])) { + case 'html': + return res.render('general/404', { title: 'page_not_found' }) + case 'json': + return renderJSONError(res, message, info) + default: + return res.send('not found') + } + }, + + unprocessableEntity(req, res, message = 'unprocessable entity', info = {}) { + res.status(422) + switch (req.accepts(['html', 'json'])) { + case 'html': + return res.render('general/400', { + title: 'Client Error', + message: message, + }) + case 'json': + return renderJSONError(res, message, info) + default: + return res.send('unprocessable entity') + } + }, + + legacyInternal(req, res, message, error) { + logger.error(error) + handleGeneric500Error(req, res, 500, message) + }, + + maintenance(req, res) { + // load balancer health checks require a success response for / + if (req.url === '/') { + res.status(200) + } else { + res.status(503) + } + let message = `${Settings.appName} is currently down for maintenance.` + if (Settings.statusPageUrl) { + message += ` Please check https://${Settings.statusPageUrl} for updates.` + } + switch (req.accepts(['html', 'json'])) { + case 'html': + return res.render('general/closed', { title: 'maintenance' }) + case 'json': + return renderJSONError(res, message, {}) + default: + return res.send(message) + } + }, +} diff --git a/services/web/app/src/Features/Exports/ExportsController.js b/services/web/app/src/Features/Exports/ExportsController.js new file mode 100644 index 0000000000..7d31f479aa --- /dev/null +++ b/services/web/app/src/Features/Exports/ExportsController.js @@ -0,0 +1,127 @@ +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const ExportsHandler = require('./ExportsHandler') +const SessionManager = require('../Authentication/SessionManager') +const logger = require('logger-sharelatex') + +module.exports = { + exportProject(req, res, next) { + const { project_id, brand_variation_id } = req.params + const user_id = SessionManager.getLoggedInUserId(req.session) + const export_params = { + project_id, + brand_variation_id, + user_id, + } + + if (req.body) { + if (req.body.firstName) { + export_params.first_name = req.body.firstName.trim() + } + if (req.body.lastName) { + export_params.last_name = req.body.lastName.trim() + } + // additional parameters for gallery exports + if (req.body.title) { + export_params.title = req.body.title.trim() + } + if (req.body.description) { + export_params.description = req.body.description.trim() + } + if (req.body.author) { + export_params.author = req.body.author.trim() + } + if (req.body.license) { + export_params.license = req.body.license.trim() + } + if (req.body.showSource != null) { + export_params.show_source = req.body.showSource + } + } + + return ExportsHandler.exportProject( + export_params, + function (err, export_data) { + if (err != null) { + if (err.forwardResponse != null) { + logger.log( + { responseError: err.forwardResponse }, + 'forwarding response' + ) + const statusCode = err.forwardResponse.status || 500 + return res.status(statusCode).json(err.forwardResponse) + } else { + return next(err) + } + } + logger.log( + { + user_id, + project_id, + brand_variation_id, + export_v1_id: export_data.v1_id, + }, + 'exported project' + ) + return res.json({ + export_v1_id: export_data.v1_id, + message: export_data.message, + }) + } + ) + }, + + exportStatus(req, res) { + const { export_id } = req.params + return ExportsHandler.fetchExport(export_id, function (err, export_json) { + let json + if (err != null) { + json = { + status_summary: 'failed', + status_detail: err.toString, + } + res.json({ export_json: json }) + return err + } + const parsed_export = JSON.parse(export_json) + json = { + status_summary: parsed_export.status_summary, + status_detail: parsed_export.status_detail, + partner_submission_id: parsed_export.partner_submission_id, + v2_user_email: parsed_export.v2_user_email, + v2_user_first_name: parsed_export.v2_user_first_name, + v2_user_last_name: parsed_export.v2_user_last_name, + title: parsed_export.title, + token: parsed_export.token, + } + return res.json({ export_json: json }) + }) + }, + + exportDownload(req, res, next) { + const { type, export_id } = req.params + + SessionManager.getLoggedInUserId(req.session) + return ExportsHandler.fetchDownload( + export_id, + type, + function (err, export_file_url) { + if (err != null) { + return next(err) + } + + return res.redirect(export_file_url) + } + ) + }, +} diff --git a/services/web/app/src/Features/Exports/ExportsHandler.js b/services/web/app/src/Features/Exports/ExportsHandler.js new file mode 100644 index 0000000000..f057e1d8ed --- /dev/null +++ b/services/web/app/src/Features/Exports/ExportsHandler.js @@ -0,0 +1,288 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ExportsHandler, self +const OError = require('@overleaf/o-error') +const ProjectGetter = require('../Project/ProjectGetter') +const ProjectHistoryHandler = require('../Project/ProjectHistoryHandler') +const ProjectLocator = require('../Project/ProjectLocator') +const ProjectRootDocManager = require('../Project/ProjectRootDocManager') +const UserGetter = require('../User/UserGetter') +const logger = require('logger-sharelatex') +let settings = require('@overleaf/settings') +const async = require('async') +let request = require('request') +request = request.defaults() +settings = require('@overleaf/settings') + +module.exports = ExportsHandler = self = { + exportProject(export_params, callback) { + if (callback == null) { + callback = function (error, export_data) {} + } + return self._buildExport(export_params, function (err, export_data) { + if (err != null) { + return callback(err) + } + return self._requestExport(export_data, function (err, body) { + if (err != null) { + return callback(err) + } + export_data.v1_id = body.exportId + export_data.message = body.message + // TODO: possibly store the export data in Mongo + return callback(null, export_data) + }) + }) + }, + + _buildExport(export_params, callback) { + if (callback == null) { + callback = function (err, export_data) {} + } + const { + project_id, + user_id, + brand_variation_id, + title, + description, + author, + license, + show_source, + } = export_params + const jobs = { + project(cb) { + return ProjectGetter.getProject(project_id, cb) + }, + // TODO: when we update async, signature will change from (cb, results) to (results, cb) + rootDoc: [ + 'project', + (cb, results) => + ProjectRootDocManager.ensureRootDocumentIsValid( + project_id, + function (error) { + if (error != null) { + return callback(error) + } + return ProjectLocator.findRootDoc( + { project: results.project, project_id }, + cb + ) + } + ), + ], + user(cb) { + return UserGetter.getUser( + user_id, + { first_name: 1, last_name: 1, email: 1, overleaf: 1 }, + cb + ) + }, + historyVersion(cb) { + return ProjectHistoryHandler.ensureHistoryExistsForProject( + project_id, + function (error) { + if (error != null) { + return callback(error) + } + return self._requestVersion(project_id, cb) + } + ) + }, + } + + return async.auto(jobs, function (err, results) { + if (err != null) { + OError.tag(err, 'error building project export', { + project_id, + user_id, + brand_variation_id, + }) + return callback(err) + } + + const { project, rootDoc, user, historyVersion } = results + if (!rootDoc || rootDoc[1] == null) { + err = new OError('cannot export project without root doc', { + project_id, + }) + return callback(err) + } + + if (export_params.first_name && export_params.last_name) { + user.first_name = export_params.first_name + user.last_name = export_params.last_name + } + + const export_data = { + project: { + id: project_id, + rootDocPath: rootDoc[1] != null ? rootDoc[1].fileSystem : undefined, + historyId: __guard__( + project.overleaf != null ? project.overleaf.history : undefined, + x => x.id + ), + historyVersion, + v1ProjectId: + project.overleaf != null ? project.overleaf.id : undefined, + metadata: { + compiler: project.compiler, + imageName: project.imageName, + title, + description, + author, + license, + showSource: show_source, + }, + }, + user: { + id: user_id, + firstName: user.first_name, + lastName: user.last_name, + email: user.email, + orcidId: null, // until v2 gets ORCID + v1UserId: user.overleaf != null ? user.overleaf.id : undefined, + }, + destination: { + brandVariationId: brand_variation_id, + }, + options: { + callbackUrl: null, + }, // for now, until we want v1 to call us back + } + return callback(null, export_data) + }) + }, + + _requestExport(export_data, callback) { + if (callback == null) { + callback = function (err, export_v1_id) {} + } + return request.post( + { + url: `${settings.apis.v1.url}/api/v1/sharelatex/exports`, + auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }, + json: export_data, + }, + function (err, res, body) { + if (err != null) { + OError.tag(err, 'error making request to v1 export', { + export: export_data, + }) + return callback(err) + } else if (res.statusCode >= 200 && res.statusCode < 300) { + return callback(null, body) + } else { + logger.warn( + { export: export_data }, + `v1 export returned failure; forwarding: ${body}` + ) + // pass the v1 error along for the publish modal to handle + return callback({ forwardResponse: body }) + } + } + ) + }, + + _requestVersion(project_id, callback) { + if (callback == null) { + callback = function (err, export_v1_id) {} + } + return request.get( + { + url: `${settings.apis.project_history.url}/project/${project_id}/version`, + json: true, + }, + function (err, res, body) { + if (err != null) { + OError.tag(err, 'error making request to project history', { + project_id, + }) + return callback(err) + } else if (res.statusCode >= 200 && res.statusCode < 300) { + return callback(null, body.version) + } else { + err = new OError( + `project history version returned a failure status code: ${res.statusCode}`, + { project_id } + ) + return callback(err) + } + } + ) + }, + + fetchExport(export_id, callback) { + if (callback == null) { + callback = function (err, export_json) {} + } + return request.get( + { + url: `${settings.apis.v1.url}/api/v1/sharelatex/exports/${export_id}`, + auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }, + }, + function (err, res, body) { + if (err != null) { + OError.tag(err, 'error making request to v1 export', { + export: export_id, + }) + return callback(err) + } else if (res.statusCode >= 200 && res.statusCode < 300) { + return callback(null, body) + } else { + err = new OError( + `v1 export returned a failure status code: ${res.statusCode}`, + { export: export_id } + ) + return callback(err) + } + } + ) + }, + + fetchDownload(export_id, type, callback) { + if (callback == null) { + callback = function (err, file_url) {} + } + return request.get( + { + url: `${settings.apis.v1.url}/api/v1/sharelatex/exports/${export_id}/${type}_url`, + auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }, + }, + function (err, res, body) { + if (err != null) { + OError.tag(err, 'error making request to v1 export', { + export: export_id, + }) + return callback(err) + } else if (res.statusCode >= 200 && res.statusCode < 300) { + return callback(null, body) + } else { + err = new OError( + `v1 export returned a failure status code: ${res.statusCode}`, + { export: export_id } + ) + return callback(err) + } + } + ) + }, +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/FileStore/FileHashManager.js b/services/web/app/src/Features/FileStore/FileHashManager.js new file mode 100644 index 0000000000..05c88c1aad --- /dev/null +++ b/services/web/app/src/Features/FileStore/FileHashManager.js @@ -0,0 +1,61 @@ +/* eslint-disable + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let FileHashManager +const crypto = require('crypto') +const logger = require('logger-sharelatex') +const fs = require('fs') +const _ = require('underscore') + +module.exports = FileHashManager = { + computeHash(filePath, callback) { + if (callback == null) { + callback = function (error, hashValue) {} + } + callback = _.once(callback) // avoid double callbacks + + // taken from v1/history/storage/lib/blob_hash.js + const getGitBlobHeader = byteLength => `blob ${byteLength}` + '\x00' + + const getByteLengthOfFile = cb => + fs.stat(filePath, function (err, stats) { + if (err != null) { + return cb(err) + } + return cb(null, stats.size) + }) + + return getByteLengthOfFile(function (err, byteLength) { + if (err != null) { + return callback(err) + } + + const input = fs.createReadStream(filePath) + input.on('error', function (err) { + logger.warn({ filePath, err }, 'error opening file in computeHash') + return callback(err) + }) + + const hash = crypto.createHash('sha1') + hash.setEncoding('hex') + hash.update(getGitBlobHeader(byteLength)) + hash.on('readable', function () { + const result = hash.read() + if (result != null) { + return callback(null, result.toString('hex')) + } + }) + return input.pipe(hash) + }) + }, +} diff --git a/services/web/app/src/Features/FileStore/FileStoreController.js b/services/web/app/src/Features/FileStore/FileStoreController.js new file mode 100644 index 0000000000..9ebae11893 --- /dev/null +++ b/services/web/app/src/Features/FileStore/FileStoreController.js @@ -0,0 +1,87 @@ +const logger = require('logger-sharelatex') + +const FileStoreHandler = require('./FileStoreHandler') +const ProjectLocator = require('../Project/ProjectLocator') +const Errors = require('../Errors/Errors') + +module.exports = { + getFile(req, res) { + const projectId = req.params.Project_id + const fileId = req.params.File_id + const queryString = req.query + const userAgent = req.get('User-Agent') + ProjectLocator.findElement( + { project_id: projectId, element_id: fileId, type: 'file' }, + function (err, file) { + if (err) { + logger.err( + { err, projectId, fileId, queryString }, + 'error finding element for downloading file' + ) + return res.sendStatus(500) + } + FileStoreHandler.getFileStream( + projectId, + fileId, + queryString, + function (err, stream) { + if (err) { + logger.err( + { err, projectId, fileId, queryString }, + 'error getting file stream for downloading file' + ) + return res.sendStatus(500) + } + // mobile safari will try to render html files, prevent this + if (isMobileSafari(userAgent) && isHtml(file)) { + res.setHeader('Content-Type', 'text/plain') + } + res.setContentDisposition('attachment', { filename: file.name }) + stream.pipe(res) + } + ) + } + ) + }, + + getFileHead(req, res) { + const projectId = req.params.Project_id + const fileId = req.params.File_id + FileStoreHandler.getFileSize(projectId, fileId, (err, fileSize) => { + if (err) { + if (err instanceof Errors.NotFoundError) { + res.status(404).end() + } else { + logger.err({ err, projectId, fileId }, 'error getting file size') + res.status(500).end() + } + return + } + res.set('Content-Length', fileSize) + res.status(200).end() + }) + }, +} + +function isHtml(file) { + return ( + fileEndsWith(file, '.html') || + fileEndsWith(file, '.htm') || + fileEndsWith(file, '.xhtml') + ) +} + +function fileEndsWith(file, ext) { + return ( + file.name != null && + file.name.length > ext.length && + file.name.lastIndexOf(ext) === file.name.length - ext.length + ) +} + +function isMobileSafari(userAgent) { + return ( + userAgent && + (userAgent.indexOf('iPhone') >= 0 || userAgent.indexOf('iPad') >= 0) + ) +} diff --git a/services/web/app/src/Features/FileStore/FileStoreHandler.js b/services/web/app/src/Features/FileStore/FileStoreHandler.js new file mode 100644 index 0000000000..bb8e57d598 --- /dev/null +++ b/services/web/app/src/Features/FileStore/FileStoreHandler.js @@ -0,0 +1,262 @@ +const _ = require('underscore') +const logger = require('logger-sharelatex') +const fs = require('fs') +const request = require('request') +const settings = require('@overleaf/settings') +const Async = require('async') +const FileHashManager = require('./FileHashManager') +const { File } = require('../../models/File') +const Errors = require('../Errors/Errors') +const OError = require('@overleaf/o-error') +const { promisifyAll } = require('../../util/promises') + +const ONE_MIN_IN_MS = 60 * 1000 +const FIVE_MINS_IN_MS = ONE_MIN_IN_MS * 5 + +const FileStoreHandler = { + RETRY_ATTEMPTS: 3, + + uploadFileFromDisk(projectId, fileArgs, fsPath, callback) { + fs.lstat(fsPath, function (err, stat) { + if (err) { + logger.warn({ err, projectId, fileArgs, fsPath }, 'error stating file') + callback(err) + } + if (!stat) { + logger.warn( + { projectId, fileArgs, fsPath }, + 'stat is not available, can not check file from disk' + ) + return callback(new Error('error getting stat, not available')) + } + if (!stat.isFile()) { + logger.log( + { projectId, fileArgs, fsPath }, + 'tried to upload symlink, not continuing' + ) + return callback(new Error('can not upload symlink')) + } + Async.retry( + FileStoreHandler.RETRY_ATTEMPTS, + (cb, results) => + FileStoreHandler._doUploadFileFromDisk( + projectId, + fileArgs, + fsPath, + cb + ), + function (err, result) { + if (err) { + OError.tag(err, 'Error uploading file, retries failed', { + projectId, + fileArgs, + }) + return callback(err) + } + callback(err, result.url, result.fileRef) + } + ) + }) + }, + + _doUploadFileFromDisk(projectId, fileArgs, fsPath, callback) { + const callbackOnce = _.once(callback) + + FileHashManager.computeHash(fsPath, function (err, hashValue) { + if (err) { + return callbackOnce(err) + } + const fileRef = new File(Object.assign({}, fileArgs, { hash: hashValue })) + const fileId = fileRef._id + const readStream = fs.createReadStream(fsPath) + readStream.on('error', function (err) { + logger.warn( + { err, projectId, fileId, fsPath }, + 'something went wrong on the read stream of uploadFileFromDisk' + ) + callbackOnce(err) + }) + readStream.on('open', function () { + const url = FileStoreHandler._buildUrl(projectId, fileId) + const opts = { + method: 'post', + uri: url, + timeout: FIVE_MINS_IN_MS, + headers: { + 'X-File-Hash-From-Web': hashValue, + }, // send the hash to the filestore as a custom header so it can be checked + } + const writeStream = request(opts) + writeStream.on('error', function (err) { + logger.warn( + { err, projectId, fileId, fsPath }, + 'something went wrong on the write stream of uploadFileFromDisk' + ) + callbackOnce(err) + }) + writeStream.on('response', function (response) { + if (![200, 201].includes(response.statusCode)) { + err = new OError( + `non-ok response from filestore for upload: ${response.statusCode}`, + { statusCode: response.statusCode } + ) + return callbackOnce(err) + } + callbackOnce(null, { url, fileRef }) + }) // have to pass back an object because async.retry only accepts a single result argument + readStream.pipe(writeStream) + }) + }) + }, + + getFileStream(projectId, fileId, query, callback) { + let queryString = '' + if (query != null && query.format != null) { + queryString = `?format=${query.format}` + } + const opts = { + method: 'get', + uri: `${this._buildUrl(projectId, fileId)}${queryString}`, + timeout: FIVE_MINS_IN_MS, + headers: {}, + } + if (query != null && query.range != null) { + const rangeText = query.range + if (rangeText && rangeText.match != null && rangeText.match(/\d+-\d+/)) { + opts.headers.range = `bytes=${query.range}` + } + } + const readStream = request(opts) + readStream.on('error', err => + logger.err( + { err, projectId, fileId, query, opts }, + 'error in file stream' + ) + ) + return callback(null, readStream) + }, + + getFileSize(projectId, fileId, callback) { + const url = this._buildUrl(projectId, fileId) + request.head(url, (err, res) => { + if (err) { + OError.tag(err, 'failed to get file size from filestore', { + projectId, + fileId, + }) + return callback(err) + } + if (res.statusCode === 404) { + return callback(new Errors.NotFoundError('file not found in filestore')) + } + if (res.statusCode !== 200) { + logger.warn( + { projectId, fileId, statusCode: res.statusCode }, + 'filestore returned non-200 response' + ) + return callback(new Error('filestore returned non-200 response')) + } + const fileSize = res.headers['content-length'] + callback(null, fileSize) + }) + }, + + deleteFile(projectId, fileId, callback) { + logger.log({ projectId, fileId }, 'telling file store to delete file') + const opts = { + method: 'delete', + uri: this._buildUrl(projectId, fileId), + timeout: FIVE_MINS_IN_MS, + } + return request(opts, function (err, response) { + if (err) { + logger.warn( + { err, projectId, fileId }, + 'something went wrong deleting file from filestore' + ) + } + return callback(err) + }) + }, + + deleteProject(projectId, callback) { + request( + { + method: 'delete', + uri: this._buildUrl(projectId), + timeout: FIVE_MINS_IN_MS, + }, + err => { + if (err) { + return callback( + OError.tag( + err, + 'something went wrong deleting a project in filestore', + { projectId } + ) + ) + } + callback() + } + ) + }, + + copyFile(oldProjectId, oldFileId, newProjectId, newFileId, callback) { + logger.log( + { oldProjectId, oldFileId, newProjectId, newFileId }, + 'telling filestore to copy a file' + ) + const opts = { + method: 'put', + json: { + source: { + project_id: oldProjectId, + file_id: oldFileId, + }, + }, + uri: this._buildUrl(newProjectId, newFileId), + timeout: FIVE_MINS_IN_MS, + } + return request(opts, function (err, response) { + if (err) { + OError.tag( + err, + 'something went wrong telling filestore api to copy file', + { + oldProjectId, + oldFileId, + newProjectId, + newFileId, + } + ) + return callback(err) + } else if (response.statusCode >= 200 && response.statusCode < 300) { + // successful response + return callback(null, opts.uri) + } else { + err = new OError( + `non-ok response from filestore for copyFile: ${response.statusCode}`, + { + uri: opts.uri, + statusCode: response.statusCode, + } + ) + return callback(err) + } + }) + }, + + _buildUrl(projectId, fileId) { + return ( + `${settings.apis.filestore.url}/project/${projectId}` + + (fileId ? `/file/${fileId}` : '') + ) + }, +} + +module.exports = FileStoreHandler +module.exports.promises = promisifyAll(FileStoreHandler, { + multiResult: { + uploadFileFromDisk: ['url', 'fileRef'], + }, +}) diff --git a/services/web/app/src/Features/HealthCheck/HealthCheckController.js b/services/web/app/src/Features/HealthCheck/HealthCheckController.js new file mode 100644 index 0000000000..3e7b84d0e7 --- /dev/null +++ b/services/web/app/src/Features/HealthCheck/HealthCheckController.js @@ -0,0 +1,125 @@ +const RedisWrapper = require('../../infrastructure/RedisWrapper') +const rclient = RedisWrapper.client('health_check') +const settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const UserGetter = require('../User/UserGetter') +const { + SmokeTestFailure, + runSmokeTests, +} = require('./../../../../test/smoke/src/SmokeTests') + +module.exports = { + check(req, res, next) { + if (!settings.siteIsOpen || !settings.editorIsOpen) { + // always return successful health checks when site is closed + res.contentType('application/json') + res.sendStatus(200) + } else { + // detach from express for cleaner stack traces + setTimeout(() => runSmokeTestsDetached(req, res).catch(next)) + } + }, + + checkActiveHandles(req, res, next) { + if (!(settings.maxActiveHandles > 0) || !process._getActiveHandles) { + return next() + } + const activeHandlesCount = (process._getActiveHandles() || []).length + if (activeHandlesCount > settings.maxActiveHandles) { + logger.err( + { activeHandlesCount, maxActiveHandles: settings.maxActiveHandles }, + 'exceeded max active handles, failing health check' + ) + return res.sendStatus(500) + } else { + logger.debug( + { activeHandlesCount, maxActiveHandles: settings.maxActiveHandles }, + 'active handles are below maximum' + ) + next() + } + }, + + checkApi(req, res, next) { + rclient.healthCheck(err => { + if (err) { + logger.err({ err }, 'failed api redis health check') + return res.sendStatus(500) + } + UserGetter.getUserEmail(settings.smokeTest.userId, (err, email) => { + if (err) { + logger.err({ err }, 'failed api mongo health check') + return res.sendStatus(500) + } + if (email == null) { + logger.err({ err }, 'failed api mongo health check (no email)') + return res.sendStatus(500) + } + res.sendStatus(200) + }) + }) + }, + + checkRedis(req, res, next) { + return rclient.healthCheck(function (error) { + if (error != null) { + logger.err({ err: error }, 'failed redis health check') + return res.sendStatus(500) + } else { + return res.sendStatus(200) + } + }) + }, + + checkMongo(req, res, next) { + return UserGetter.getUserEmail( + settings.smokeTest.userId, + function (err, email) { + if (err != null) { + logger.err({ err }, 'mongo health check failed, error present') + return res.sendStatus(500) + } else if (email == null) { + logger.err( + { err }, + 'mongo health check failed, no emai present in find result' + ) + return res.sendStatus(500) + } else { + return res.sendStatus(200) + } + } + ) + }, +} + +function prettyJSON(blob) { + return JSON.stringify(blob, null, 2) + '\n' +} +async function runSmokeTestsDetached(req, res) { + function isAborted() { + return req.aborted + } + const stats = { start: new Date(), steps: [] } + let status, response + try { + try { + await runSmokeTests({ isAborted, stats }) + } finally { + stats.end = new Date() + stats.duration = stats.end - stats.start + } + status = 200 + response = { stats } + } catch (e) { + let err = e + if (!(e instanceof SmokeTestFailure)) { + err = new SmokeTestFailure('low level error', {}, e) + } + logger.err({ err, stats }, 'health check failed') + status = 500 + response = { stats, error: err.message } + } + if (isAborted()) return + res.contentType('application/json') + res.status(status).send(prettyJSON(response)) +} diff --git a/services/web/app/src/Features/Helpers/AsyncFormHelper.js b/services/web/app/src/Features/Helpers/AsyncFormHelper.js new file mode 100644 index 0000000000..8b8a3b6875 --- /dev/null +++ b/services/web/app/src/Features/Helpers/AsyncFormHelper.js @@ -0,0 +1,17 @@ +const { + acceptsJson, +} = require('../../infrastructure/RequestContentTypeDetection') + +module.exports = { + redirect, +} + +// redirect the request via headers or JSON response depending on the request +// format +function redirect(req, res, redir) { + if (acceptsJson(req)) { + res.json({ redir }) + } else { + res.redirect(redir) + } +} diff --git a/services/web/app/src/Features/Helpers/AuthorizationHelper.js b/services/web/app/src/Features/Helpers/AuthorizationHelper.js new file mode 100644 index 0000000000..793d7d3db5 --- /dev/null +++ b/services/web/app/src/Features/Helpers/AuthorizationHelper.js @@ -0,0 +1,19 @@ +const { UserSchema } = require('../../models/User') + +module.exports = { + hasAnyStaffAccess, +} + +function hasAnyStaffAccess(user) { + if (user.isAdmin) { + return true + } + if (!user.staffAccess) { + return false + } + + for (const key of Object.keys(UserSchema.obj.staffAccess)) { + if (user.staffAccess[key]) return true + } + return false +} diff --git a/services/web/app/src/Features/Helpers/EmailHelper.js b/services/web/app/src/Features/Helpers/EmailHelper.js new file mode 100644 index 0000000000..99f944739b --- /dev/null +++ b/services/web/app/src/Features/Helpers/EmailHelper.js @@ -0,0 +1,29 @@ +// eslint-disable-next-line no-useless-escape +const EMAIL_REGEXP = /^([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + +function getDomain(email) { + email = parseEmail(email) + return email ? email.split('@').pop() : null +} + +function parseEmail(email) { + if (email == null) { + return null + } + if (email.length > 254) { + return null + } + email = email.trim().toLowerCase() + + const matched = email.match(EMAIL_REGEXP) + if (matched == null || matched[0] == null) { + return null + } + + return matched[0] +} + +module.exports = { + getDomain, + parseEmail, +} diff --git a/services/web/app/src/Features/Helpers/FeatureFlag.js b/services/web/app/src/Features/Helpers/FeatureFlag.js new file mode 100644 index 0000000000..44f0b812aa --- /dev/null +++ b/services/web/app/src/Features/Helpers/FeatureFlag.js @@ -0,0 +1,9 @@ +function shouldDisplayFeature(req, name, variantFlag) { + if (req.query && req.query[name]) { + return req.query[name] === 'true' + } else { + return variantFlag === true + } +} + +module.exports = { shouldDisplayFeature } diff --git a/services/web/app/src/Features/Helpers/Mongo.js b/services/web/app/src/Features/Helpers/Mongo.js new file mode 100644 index 0000000000..a6c102d989 --- /dev/null +++ b/services/web/app/src/Features/Helpers/Mongo.js @@ -0,0 +1,51 @@ +const OError = require('@overleaf/o-error') +const { ObjectId } = require('mongodb') +const { ObjectId: MongooseObjectId } = require('mongoose').mongo + +function _getObjectIdInstance(id) { + if (typeof id === 'string') { + return ObjectId(id) + } else if (id instanceof ObjectId) { + return id + } else if (id instanceof MongooseObjectId) { + return ObjectId(id.toString()) + } else { + throw new OError('unexpected object id', { id }) + } +} + +function normalizeQuery(query) { + if (!query) { + throw new Error('no query provided') + } + if ( + typeof query === 'string' || + query instanceof ObjectId || + query instanceof MongooseObjectId + ) { + return { _id: _getObjectIdInstance(query) } + } else if (typeof query._id === 'string') { + query._id = ObjectId(query._id) + return query + } else { + return query + } +} + +function normalizeMultiQuery(query) { + if (Array.isArray(query)) { + return { _id: { $in: query.map(id => _getObjectIdInstance(id)) } } + } else { + return normalizeQuery(query) + } +} + +function isObjectIdInstance(id) { + return id instanceof ObjectId || id instanceof MongooseObjectId +} + +module.exports = { + isObjectIdInstance, + normalizeQuery, + normalizeMultiQuery, +} diff --git a/services/web/app/src/Features/Helpers/NewLogsUI.js b/services/web/app/src/Features/Helpers/NewLogsUI.js new file mode 100644 index 0000000000..62b91e53f2 --- /dev/null +++ b/services/web/app/src/Features/Helpers/NewLogsUI.js @@ -0,0 +1,60 @@ +const { ObjectId } = require('mongodb') +const Settings = require('@overleaf/settings') + +const EXISTING_UI = { newLogsUI: false, subvariant: null } +const NEW_UI_WITH_POPUP = { + newLogsUI: true, + subvariant: 'new-logs-ui-with-popup', +} +const NEW_UI_WITHOUT_POPUP = { + newLogsUI: true, + subvariant: 'new-logs-ui-without-popup', +} + +function _getVariantForPercentile(percentile) { + // The current percentages are: + // - 33% New UI with pop-up (originally, 5%) + // - 33% New UI without pop-up (originally, 5%) + // - 34% Existing UI + // To ensure group stability, the implementation below respects the original partitions + // for the new UI variants: [0, 5[ and [5,10[. + // Two new partitions are added: [10, 38[ and [38, 66[. These represent an extra 28p.p. + // which, with to the original 5%, add up to 33%. + + if (percentile < 5) { + // This partition represents the "New UI with pop-up" group in the original roll-out (5%) + return NEW_UI_WITH_POPUP + } else if (percentile >= 5 && percentile < 10) { + // This partition represents the "New UI without pop-up" group in the original roll-out (5%) + return NEW_UI_WITHOUT_POPUP + } else if (percentile >= 10 && percentile < 38) { + // This partition represents an extra 28% of users getting the "New UI with pop-up" + return NEW_UI_WITH_POPUP + } else if (percentile >= 38 && percentile < 66) { + // This partition represents an extra 28% of users getting the "New UI without pop-up" + return NEW_UI_WITHOUT_POPUP + } else { + return EXISTING_UI + } +} + +function getNewLogsUIVariantForUser(user) { + const { _id: userId, alphaProgram: isAlphaUser } = user + const isSaaS = Boolean(Settings.overleaf) + + if (!userId || !isSaaS) { + return EXISTING_UI + } + + const userIdAsPercentile = (ObjectId(userId).getTimestamp() / 1000) % 100 + + if (isAlphaUser) { + return NEW_UI_WITH_POPUP + } else { + return _getVariantForPercentile(userIdAsPercentile) + } +} + +module.exports = { + getNewLogsUIVariantForUser, +} diff --git a/services/web/app/src/Features/Helpers/SafeHTMLSubstitution.js b/services/web/app/src/Features/Helpers/SafeHTMLSubstitution.js new file mode 100644 index 0000000000..4ae40d80f0 --- /dev/null +++ b/services/web/app/src/Features/Helpers/SafeHTMLSubstitution.js @@ -0,0 +1,46 @@ +const pug = require('pug-runtime') + +const SPLIT_REGEX = /<(\d+)>(.*?)<\/\1>/g + +function render(locale, components) { + const output = [] + function addPlainText(text) { + if (!text) return + output.push(pug.escape(text)) + } + + // 'PRE<0>INNERPOST' -> ['PRE', '0', 'INNER', 'POST'] + // '<0>INNER' -> ['', '0', 'INNER', ''] + // '<0>' -> ['', '0', '', ''] + // '<0>INNER<0>INNER2' -> ['', '0', 'INNER', '', '0', 'INNER2', ''] + // '<0><1>INNER' -> ['', '0', '<1>INNER', ''] + // 'PLAIN TEXT' -> ['PLAIN TEXT'] + // NOTE: a test suite is verifying these cases: SafeHTMLSubstituteTests + const chunks = locale.split(SPLIT_REGEX) + + // extract the 'PRE' chunk + addPlainText(chunks.shift()) + + while (chunks.length) { + // each batch consists of three chunks: ['0', 'INNER', 'POST'] + const [idx, innerChunk, intermediateChunk] = chunks.splice(0, 3) + + const component = components[idx] + const componentName = + typeof component === 'string' ? component : component.name + // pug is doing any necessary escaping on attribute values + const attributes = (component.attrs && pug.attrs(component.attrs)) || '' + output.push( + `<${componentName + attributes}>`, + ...render(innerChunk, components), + `` + ) + addPlainText(intermediateChunk) + } + return output.join('') +} + +module.exports = { + SPLIT_REGEX, + render, +} diff --git a/services/web/app/src/Features/Helpers/StringHelper.js b/services/web/app/src/Features/Helpers/StringHelper.js new file mode 100644 index 0000000000..47f71df89f --- /dev/null +++ b/services/web/app/src/Features/Helpers/StringHelper.js @@ -0,0 +1,30 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +let StringHelper +const JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/g + +const JSON_ESCAPE = { + '&': '\\u0026', + '>': '\\u003e', + '<': '\\u003c', + '\u2028': '\\u2028', + '\u2029': '\\u2029', +} + +module.exports = StringHelper = { + // stringifies and escapes a json object for use in a script. This ensures that &, < and > characters are escaped, + // along with quotes. This ensures that the string can be safely rendered into HTML. See rationale at: + // https://api.rubyonrails.org/classes/ERB/Util.html#method-c-json_escape + // and implementation lifted from: + // https://github.com/ember-fastboot/fastboot/blob/cafd96c48564d8384eb83dc908303dba8ece10fd/src/ember-app.js#L496-L510 + stringifyJsonForScript(object) { + return JSON.stringify(object).replace( + JSON_ESCAPE_REGEXP, + match => JSON_ESCAPE[match] + ) + }, +} diff --git a/services/web/app/src/Features/Helpers/UrlHelper.js b/services/web/app/src/Features/Helpers/UrlHelper.js new file mode 100644 index 0000000000..d75eaccfc6 --- /dev/null +++ b/services/web/app/src/Features/Helpers/UrlHelper.js @@ -0,0 +1,32 @@ +const Settings = require('@overleaf/settings') +const { URL } = require('url') + +function getSafeRedirectPath(value) { + const baseURL = Settings.siteUrl // base URL is required to construct URL from path + const url = new URL(value, baseURL) + let safePath = `${url.pathname}${url.search}${url.hash}`.replace(/^\/+/, '/') + if (safePath === '/') { + safePath = undefined + } + return safePath +} + +const UrlHelper = { + getSafeRedirectPath, + wrapUrlWithProxy(url) { + // TODO: Consider what to do for Community and Enterprise edition? + if (!Settings.apis.linkedUrlProxy.url) { + throw new Error('no linked url proxy configured') + } + return `${Settings.apis.linkedUrlProxy.url}?url=${encodeURIComponent(url)}` + }, + + prependHttpIfNeeded(url) { + if (!url.match('://')) { + url = `http://${url}` + } + return url + }, +} + +module.exports = UrlHelper diff --git a/services/web/app/src/Features/History/HistoryController.js b/services/web/app/src/Features/History/HistoryController.js new file mode 100644 index 0000000000..00f97e33d8 --- /dev/null +++ b/services/web/app/src/Features/History/HistoryController.js @@ -0,0 +1,432 @@ +let HistoryController +const OError = require('@overleaf/o-error') +const async = require('async') +const logger = require('logger-sharelatex') +const request = require('request') +const settings = require('@overleaf/settings') +const SessionManager = require('../Authentication/SessionManager') +const UserGetter = require('../User/UserGetter') +const Errors = require('../Errors/Errors') +const HistoryManager = require('./HistoryManager') +const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler') +const ProjectEntityUpdateHandler = require('../Project/ProjectEntityUpdateHandler') +const RestoreManager = require('./RestoreManager') +const { pipeline } = require('stream') + +module.exports = HistoryController = { + selectHistoryApi(req, res, next) { + const { Project_id: projectId } = req.params + // find out which type of history service this project uses + ProjectDetailsHandler.getDetails(projectId, function (err, project) { + if (err) { + return next(err) + } + const history = project.overleaf && project.overleaf.history + if (history && history.id && history.display) { + req.useProjectHistory = true + } else { + req.useProjectHistory = false + } + next() + }) + }, + + ensureProjectHistoryEnabled(req, res, next) { + if (req.useProjectHistory) { + next() + } else { + res.sendStatus(404) + } + }, + + proxyToHistoryApi(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + const url = + HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url + + const getReq = request({ + url, + method: req.method, + headers: { + 'X-User-Id': userId, + }, + }) + getReq.pipe(res) + getReq.on('error', function (err) { + logger.warn({ url, err }, 'history API error') + next(err) + }) + }, + + proxyToHistoryApiAndInjectUserDetails(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + const url = + HistoryController.buildHistoryServiceUrl(req.useProjectHistory) + req.url + HistoryController._makeRequest( + { + url, + method: req.method, + json: true, + headers: { + 'X-User-Id': userId, + }, + }, + function (err, body) { + if (err) { + return next(err) + } + HistoryManager.injectUserDetails(body, function (err, data) { + if (err) { + return next(err) + } + res.json(data) + }) + } + ) + }, + + buildHistoryServiceUrl(useProjectHistory) { + // choose a history service, either document-level (trackchanges) + // or project-level (project_history) + if (useProjectHistory) { + return settings.apis.project_history.url + } else { + return settings.apis.trackchanges.url + } + }, + + resyncProjectHistory(req, res, next) { + const projectId = req.params.Project_id + ProjectEntityUpdateHandler.resyncProjectHistory(projectId, function (err) { + if (err instanceof Errors.ProjectHistoryDisabledError) { + return res.sendStatus(404) + } + if (err) { + return next(err) + } + res.sendStatus(204) + }) + }, + + restoreFileFromV2(req, res, next) { + const { project_id: projectId } = req.params + const { version, pathname } = req.body + const userId = SessionManager.getLoggedInUserId(req.session) + RestoreManager.restoreFileFromV2( + userId, + projectId, + version, + pathname, + function (err, entity) { + if (err) { + return next(err) + } + res.json({ + type: entity.type, + id: entity._id, + }) + } + ) + }, + + restoreDocFromDeletedDoc(req, res, next) { + const { project_id: projectId, doc_id: docId } = req.params + const { name } = req.body + const userId = SessionManager.getLoggedInUserId(req.session) + if (name == null) { + return res.sendStatus(400) // Malformed request + } + RestoreManager.restoreDocFromDeletedDoc( + userId, + projectId, + docId, + name, + (err, doc) => { + if (err) return next(err) + res.json({ + doc_id: doc._id, + }) + } + ) + }, + + getLabels(req, res, next) { + const projectId = req.params.Project_id + HistoryController._makeRequest( + { + method: 'GET', + url: `${settings.apis.project_history.url}/project/${projectId}/labels`, + json: true, + }, + function (err, labels) { + if (err) { + return next(err) + } + HistoryController._enrichLabels(labels, (err, labels) => { + if (err) { + return next(err) + } + res.json(labels) + }) + } + ) + }, + + createLabel(req, res, next) { + const projectId = req.params.Project_id + const { comment, version } = req.body + const userId = SessionManager.getLoggedInUserId(req.session) + HistoryController._makeRequest( + { + method: 'POST', + url: `${settings.apis.project_history.url}/project/${projectId}/user/${userId}/labels`, + json: { comment, version }, + }, + function (err, label) { + if (err) { + return next(err) + } + HistoryController._enrichLabel(label, (err, label) => { + if (err) { + return next(err) + } + res.json(label) + }) + } + ) + }, + + _enrichLabel(label, callback) { + if (!label.user_id) { + return callback(null, label) + } + UserGetter.getUser( + label.user_id, + { first_name: 1, last_name: 1, email: 1 }, + (err, user) => { + if (err) { + return callback(err) + } + const newLabel = Object.assign({}, label) + newLabel.user_display_name = HistoryController._displayNameForUser(user) + callback(null, newLabel) + } + ) + }, + + _enrichLabels(labels, callback) { + if (!labels || !labels.length) { + return callback(null, []) + } + const uniqueUsers = new Set(labels.map(label => label.user_id)) + + // For backwards compatibility expect missing user_id fields + uniqueUsers.delete(undefined) + + if (!uniqueUsers.size) { + return callback(null, labels) + } + + UserGetter.getUsers( + Array.from(uniqueUsers), + { first_name: 1, last_name: 1, email: 1 }, + function (err, rawUsers) { + if (err) { + return callback(err) + } + const users = new Map(rawUsers.map(user => [String(user._id), user])) + + labels.forEach(label => { + const user = users.get(label.user_id) + if (!user) return + label.user_display_name = HistoryController._displayNameForUser(user) + }) + callback(null, labels) + } + ) + }, + + _displayNameForUser(user) { + if (user == null) { + return 'Anonymous' + } + if (user.name) { + return user.name + } + let name = [user.first_name, user.last_name] + .filter(n => n != null) + .join(' ') + .trim() + if (name === '') { + name = user.email.split('@')[0] + } + if (!name) { + return '?' + } + return name + }, + + deleteLabel(req, res, next) { + const { Project_id: projectId, label_id: labelId } = req.params + const userId = SessionManager.getLoggedInUserId(req.session) + HistoryController._makeRequest( + { + method: 'DELETE', + url: `${settings.apis.project_history.url}/project/${projectId}/user/${userId}/labels/${labelId}`, + }, + function (err) { + if (err) { + return next(err) + } + res.sendStatus(204) + } + ) + }, + + _makeRequest(options, callback) { + return request(options, function (err, response, body) { + if (err) { + return callback(err) + } + if (response.statusCode >= 200 && response.statusCode < 300) { + callback(null, body) + } else { + err = new Error( + `history api responded with non-success code: ${response.statusCode}` + ) + callback(err) + } + }) + }, + + downloadZipOfVersion(req, res, next) { + const { project_id: projectId, version } = req.params + ProjectDetailsHandler.getDetails(projectId, function (err, project) { + if (err) { + return next(err) + } + const v1Id = + project.overleaf && + project.overleaf.history && + project.overleaf.history.id + if (v1Id == null) { + logger.error( + { projectId, version }, + 'got request for zip version of non-v1 history project' + ) + return res.sendStatus(402) + } + HistoryController._pipeHistoryZipToResponse( + v1Id, + version, + `${project.name} (Version ${version})`, + req, + res, + next + ) + }) + }, + + _pipeHistoryZipToResponse(v1ProjectId, version, name, req, res, next) { + if (req.aborted) { + // client has disconnected -- skip project history api call and download + return + } + // increase timeout to 6 minutes + res.setTimeout(6 * 60 * 1000) + const url = `${settings.apis.v1_history.url}/projects/${v1ProjectId}/version/${version}/zip` + const options = { + auth: { + user: settings.apis.v1_history.user, + pass: settings.apis.v1_history.pass, + }, + json: true, + method: 'post', + url, + } + request(options, function (err, response, body) { + if (err) { + OError.tag(err, 'history API error', { + v1ProjectId, + version, + }) + return next(err) + } + if (req.aborted) { + // client has disconnected -- skip delayed s3 download + return + } + let retryAttempt = 0 + let retryDelay = 2000 + // retry for about 6 minutes starting with short delay + async.retry( + 40, + callback => + setTimeout(function () { + if (req.aborted) { + // client has disconnected -- skip s3 download + return callback() // stop async.retry loop + } + + // increase delay by 1 second up to 10 + if (retryDelay < 10000) { + retryDelay += 1000 + } + retryAttempt++ + const getReq = request({ + url: body.zipUrl, + sendImmediately: true, + }) + const abortS3Request = () => getReq.abort() + req.on('aborted', abortS3Request) + res.on('timeout', abortS3Request) + function cleanupAbortTrigger() { + req.off('aborted', abortS3Request) + res.off('timeout', abortS3Request) + } + getReq.on('response', function (response) { + if (response.statusCode !== 200) { + cleanupAbortTrigger() + return callback(new Error('invalid response')) + } + // pipe also proxies the headers, but we want to customize these ones + delete response.headers['content-disposition'] + delete response.headers['content-type'] + res.status(response.statusCode) + res.setContentDisposition('attachment', { + filename: `${name}.zip`, + }) + res.contentType('application/zip') + pipeline(response, res, err => { + if (err) { + logger.warn( + { err, v1ProjectId, version, retryAttempt }, + 'history s3 proxying error' + ) + } + }) + callback() + }) + getReq.on('error', function (err) { + logger.warn( + { err, v1ProjectId, version, retryAttempt }, + 'history s3 download error' + ) + cleanupAbortTrigger() + callback(err) + }) + }, retryDelay), + function (err) { + if (err) { + OError.tag(err, 'history s3 download failed', { + v1ProjectId, + version, + retryAttempt, + }) + next(err) + } + } + ) + }) + }, +} diff --git a/services/web/app/src/Features/History/HistoryManager.js b/services/web/app/src/Features/History/HistoryManager.js new file mode 100644 index 0000000000..4ac4a09390 --- /dev/null +++ b/services/web/app/src/Features/History/HistoryManager.js @@ -0,0 +1,171 @@ +const { callbackify } = require('util') +const request = require('request-promise-native') +const settings = require('@overleaf/settings') +const OError = require('@overleaf/o-error') +const UserGetter = require('../User/UserGetter') + +module.exports = { + initializeProject: callbackify(initializeProject), + flushProject: callbackify(flushProject), + resyncProject: callbackify(resyncProject), + deleteProject: callbackify(deleteProject), + injectUserDetails: callbackify(injectUserDetails), + promises: { + initializeProject, + flushProject, + resyncProject, + deleteProject, + injectUserDetails, + }, +} + +async function initializeProject() { + if ( + !( + settings.apis.project_history && + settings.apis.project_history.initializeHistoryForNewProjects + ) + ) { + return + } + try { + const body = await request.post({ + url: `${settings.apis.project_history.url}/project`, + }) + const project = JSON.parse(body) + const overleafId = project && project.project && project.project.id + if (!overleafId) { + throw new Error('project-history did not provide an id', project) + } + return { overleaf_id: overleafId } + } catch (err) { + throw OError.tag(err, 'failed to initialize project history') + } +} + +async function flushProject(projectId) { + try { + await request.post({ + url: `${settings.apis.project_history.url}/project/${projectId}/flush`, + }) + } catch (err) { + throw OError.tag(err, 'failed to flush project to project history', { + projectId, + }) + } +} + +async function resyncProject(projectId) { + try { + await request.post({ + url: `${settings.apis.project_history.url}/project/${projectId}/resync`, + }) + } catch (err) { + throw OError.tag(err, 'failed to resync project history', { projectId }) + } +} + +async function deleteProject(projectId, historyId) { + try { + const tasks = [ + request.delete( + `${settings.apis.project_history.url}/project/${projectId}` + ), + ] + if (historyId != null) { + tasks.push( + request.delete({ + url: `${settings.apis.v1_history.url}/projects/${historyId}`, + auth: { + user: settings.apis.v1_history.user, + pass: settings.apis.v1_history.pass, + }, + }) + ) + } + await Promise.all(tasks) + } catch (err) { + throw OError.tag(err, 'failed to clear project history', { + projectId, + historyId, + }) + } +} + +async function injectUserDetails(data) { + // data can be either: + // { + // diff: [{ + // i: "foo", + // meta: { + // users: ["user_id", v1_user_id, ...] + // ... + // } + // }, ...] + // } + // or + // { + // updates: [{ + // pathnames: ["main.tex"] + // meta: { + // users: ["user_id", v1_user_id, ...] + // ... + // }, + // ... + // }, ...] + // } + // Either way, the top level key points to an array of objects with a meta.users property + // that we need to replace user_ids with populated user objects. + // Note that some entries in the users arrays may be v1 ids returned by the v1 history + // service. v1 ids will be `numbers` + let userIds = new Set() + let v1UserIds = new Set() + const entries = Array.isArray(data.diff) + ? data.diff + : Array.isArray(data.updates) + ? data.updates + : [] + for (const entry of entries) { + for (const user of (entry.meta && entry.meta.users) || []) { + if (typeof user === 'string') { + userIds.add(user) + } else if (typeof user === 'number') { + v1UserIds.add(user) + } + } + } + + userIds = Array.from(userIds) + v1UserIds = Array.from(v1UserIds) + const projection = { first_name: 1, last_name: 1, email: 1 } + const usersArray = await UserGetter.promises.getUsers(userIds, projection) + const users = {} + for (const user of usersArray) { + users[user._id.toString()] = _userView(user) + } + projection.overleaf = 1 + const v1IdentifiedUsersArray = await UserGetter.promises.getUsersByV1Ids( + v1UserIds, + projection + ) + for (const user of v1IdentifiedUsersArray) { + users[user.overleaf.id] = _userView(user) + } + for (const entry of entries) { + if (entry.meta != null) { + entry.meta.users = ((entry.meta && entry.meta.users) || []).map(user => { + if (typeof user === 'string' || typeof user === 'number') { + return users[user] + } else { + return user + } + }) + } + } + return data +} + +function _userView(user) { + const { _id, first_name: firstName, last_name: lastName, email } = user + return { first_name: firstName, last_name: lastName, email, id: _id } +} diff --git a/services/web/app/src/Features/History/RestoreManager.js b/services/web/app/src/Features/History/RestoreManager.js new file mode 100644 index 0000000000..4d4e229c10 --- /dev/null +++ b/services/web/app/src/Features/History/RestoreManager.js @@ -0,0 +1,157 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let RestoreManager +const Settings = require('@overleaf/settings') +const Path = require('path') +const FileWriter = require('../../infrastructure/FileWriter') +const FileSystemImportManager = require('../Uploads/FileSystemImportManager') +const ProjectEntityHandler = require('../Project/ProjectEntityHandler') +const EditorController = require('../Editor/EditorController') +const Errors = require('../Errors/Errors') +const moment = require('moment') + +module.exports = RestoreManager = { + restoreDocFromDeletedDoc(user_id, project_id, doc_id, name, callback) { + // This is the legacy method for restoring a doc from the SL track-changes/deletedDocs system. + // It looks up the deleted doc's contents, and then creates a new doc with the same content. + // We don't actually remove the deleted doc entry, just create a new one from its lines. + if (callback == null) { + callback = function (error, doc, folder_id) {} + } + return ProjectEntityHandler.getDoc( + project_id, + doc_id, + { include_deleted: true }, + function (error, lines) { + if (error != null) { + return callback(error) + } + const addDocWithName = (name, callback) => + EditorController.addDoc( + project_id, + null, + name, + lines, + 'restore', + user_id, + callback + ) + return RestoreManager._addEntityWithUniqueName( + addDocWithName, + name, + callback + ) + } + ) + }, + + restoreFileFromV2(user_id, project_id, version, pathname, callback) { + if (callback == null) { + callback = function (error, entity) {} + } + return RestoreManager._writeFileVersionToDisk( + project_id, + version, + pathname, + function (error, fsPath) { + if (error != null) { + return callback(error) + } + const basename = Path.basename(pathname) + let dirname = Path.dirname(pathname) + if (dirname === '.') { + // no directory + dirname = '' + } + return RestoreManager._findOrCreateFolder( + project_id, + dirname, + function (error, parent_folder_id) { + if (error != null) { + return callback(error) + } + const addEntityWithName = (name, callback) => + FileSystemImportManager.addEntity( + user_id, + project_id, + parent_folder_id, + name, + fsPath, + false, + callback + ) + return RestoreManager._addEntityWithUniqueName( + addEntityWithName, + basename, + callback + ) + } + ) + } + ) + }, + + _findOrCreateFolder(project_id, dirname, callback) { + if (callback == null) { + callback = function (error, folder_id) {} + } + return EditorController.mkdirp( + project_id, + dirname, + function (error, newFolders, lastFolder) { + if (error != null) { + return callback(error) + } + return callback(null, lastFolder != null ? lastFolder._id : undefined) + } + ) + }, + + _addEntityWithUniqueName(addEntityWithName, basename, callback) { + if (callback == null) { + callback = function (error) {} + } + return addEntityWithName(basename, function (error, entity) { + if (error != null) { + if (error instanceof Errors.InvalidNameError) { + // likely a duplicate name, so try with a prefix + const date = moment(new Date()).format('Do MMM YY H:mm:ss') + // Move extension to the end so the file type is preserved + const extension = Path.extname(basename) + basename = Path.basename(basename, extension) + basename = `${basename} (Restored on ${date})` + if (extension !== '') { + basename = `${basename}${extension}` + } + return addEntityWithName(basename, callback) + } else { + return callback(error) + } + } else { + return callback(null, entity) + } + }) + }, + + _writeFileVersionToDisk(project_id, version, pathname, callback) { + if (callback == null) { + callback = function (error, fsPath) {} + } + const url = `${ + Settings.apis.project_history.url + }/project/${project_id}/version/${version}/${encodeURIComponent(pathname)}` + return FileWriter.writeUrlToDisk(project_id, url, callback) + }, +} diff --git a/services/web/app/src/Features/InactiveData/InactiveProjectController.js b/services/web/app/src/Features/InactiveData/InactiveProjectController.js new file mode 100644 index 0000000000..c3f913e6b9 --- /dev/null +++ b/services/web/app/src/Features/InactiveData/InactiveProjectController.js @@ -0,0 +1,45 @@ +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const InactiveProjectManager = require('./InactiveProjectManager') + +module.exports = { + deactivateOldProjects(req, res) { + const numberOfProjectsToArchive = parseInt( + req.body.numberOfProjectsToArchive, + 10 + ) + const { ageOfProjects } = req.body + return InactiveProjectManager.deactivateOldProjects( + numberOfProjectsToArchive, + ageOfProjects, + function (err, projectsDeactivated) { + if (err != null) { + return res.sendStatus(500) + } else { + return res.send(projectsDeactivated) + } + } + ) + }, + + deactivateProject(req, res) { + const { project_id } = req.params + return InactiveProjectManager.deactivateProject(project_id, function (err) { + if (err != null) { + return res.sendStatus(500) + } else { + return res.sendStatus(200) + } + }) + }, +} diff --git a/services/web/app/src/Features/InactiveData/InactiveProjectManager.js b/services/web/app/src/Features/InactiveData/InactiveProjectManager.js new file mode 100644 index 0000000000..2c8acc23ac --- /dev/null +++ b/services/web/app/src/Features/InactiveData/InactiveProjectManager.js @@ -0,0 +1,118 @@ +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let InactiveProjectManager +const OError = require('@overleaf/o-error') +const async = require('async') +const _ = require('underscore') +const logger = require('logger-sharelatex') +const DocstoreManager = require('../Docstore/DocstoreManager') +const ProjectGetter = require('../Project/ProjectGetter') +const ProjectUpdateHandler = require('../Project/ProjectUpdateHandler') +const { Project } = require('../../models/Project') +const { ObjectId } = require('mongodb') + +const MILISECONDS_IN_DAY = 86400000 +module.exports = InactiveProjectManager = { + reactivateProjectIfRequired(project_id, callback) { + return ProjectGetter.getProject( + project_id, + { active: true }, + function (err, project) { + if (err != null) { + OError.tag(err, 'error getting project', { + project_id, + }) + return callback(err) + } + logger.log( + { project_id, active: project.active }, + 'seeing if need to reactivate project' + ) + + if (project.active) { + return callback() + } + + return DocstoreManager.unarchiveProject(project_id, function (err) { + if (err != null) { + OError.tag(err, 'error reactivating project in docstore', { + project_id, + }) + return callback(err) + } + return ProjectUpdateHandler.markAsActive(project_id, callback) + }) + } + ) + }, + + deactivateOldProjects(limit, daysOld, callback) { + if (limit == null) { + limit = 10 + } + if (daysOld == null) { + daysOld = 360 + } + const oldProjectDate = new Date() - MILISECONDS_IN_DAY * daysOld + // use $not $gt to catch non-opened projects where lastOpened is null + Project.find({ lastOpened: { $not: { $gt: oldProjectDate } } }) + .where('_id') + .lt(ObjectId.createFromTime(oldProjectDate / 1000)) + .where('active') + .equals(true) + .select('_id') + .sort({ _id: 1 }) + .limit(limit) + .read('secondary') + .exec(function (err, projects) { + if (err != null) { + logger.err({ err }, 'could not get projects for deactivating') + } + const jobs = _.map(projects, project => cb => + InactiveProjectManager.deactivateProject(project._id, function (err) { + if (err) { + logger.err( + { project_id: project._id, err: err }, + 'unable to deactivate project' + ) + } + cb() + }) + ) + logger.log( + { numberOfProjects: projects && projects.length }, + 'deactivating projects' + ) + async.series(jobs, function (err) { + if (err != null) { + logger.warn({ err }, 'error deactivating projects') + } + callback(err, projects) + }) + }) + }, + + deactivateProject(project_id, callback) { + logger.log({ project_id }, 'deactivating inactive project') + const jobs = [ + cb => DocstoreManager.archiveProject(project_id, cb), + cb => ProjectUpdateHandler.markAsInactive(project_id, cb), + ] + return async.series(jobs, function (err) { + if (err != null) { + logger.warn({ err, project_id }, 'error deactivating project') + } + return callback(err) + }) + }, +} diff --git a/services/web/app/src/Features/Institutions/InstitutionsAPI.js b/services/web/app/src/Features/Institutions/InstitutionsAPI.js new file mode 100644 index 0000000000..81fc5e3906 --- /dev/null +++ b/services/web/app/src/Features/Institutions/InstitutionsAPI.js @@ -0,0 +1,308 @@ +const OError = require('@overleaf/o-error') +const logger = require('logger-sharelatex') +const metrics = require('@overleaf/metrics') +const settings = require('@overleaf/settings') +const request = require('request') +const { promisifyAll } = require('../../util/promises') +const NotificationsBuilder = require('../Notifications/NotificationsBuilder') +const { + V1ConnectionError, + InvalidInstitutionalEmailError, +} = require('../Errors/Errors') + +const InstitutionsAPI = { + getInstitutionAffiliations(institutionId, callback) { + makeAffiliationRequest( + { + method: 'GET', + path: `/api/v2/institutions/${institutionId.toString()}/affiliations`, + defaultErrorMessage: "Couldn't get institution affiliations", + }, + (error, body) => callback(error, body || []) + ) + }, + + getInstitutionAffiliationsCounts(institutionId, callback) { + makeAffiliationRequest( + { + method: 'GET', + path: `/api/v2/institutions/${institutionId.toString()}/affiliations_counts`, + defaultErrorMessage: "Couldn't get institution counts", + }, + (error, body) => callback(error, body || []) + ) + }, + + getLicencesForAnalytics(lag, queryDate, callback) { + makeAffiliationRequest( + { + method: 'GET', + path: `/api/v2/institutions/institutions_licences`, + body: { query_date: queryDate, lag }, + defaultErrorMessage: 'Could not get institutions licences', + }, + callback + ) + }, + + getInstitutionLicences(institutionId, startDate, endDate, lag, callback) { + makeAffiliationRequest( + { + method: 'GET', + path: `/api/v2/institutions/${institutionId.toString()}/institution_licences`, + body: { start_date: startDate, end_date: endDate, lag }, + defaultErrorMessage: "Couldn't get institution licences", + }, + callback + ) + }, + + getInstitutionNewLicences(institutionId, startDate, endDate, lag, callback) { + makeAffiliationRequest( + { + method: 'GET', + path: `/api/v2/institutions/${institutionId.toString()}/new_institution_licences`, + body: { start_date: startDate, end_date: endDate, lag }, + defaultErrorMessage: "Couldn't get institution new licences", + }, + callback + ) + }, + + getUserAffiliations(userId, callback) { + makeAffiliationRequest( + { + method: 'GET', + path: `/api/v2/users/${userId.toString()}/affiliations`, + defaultErrorMessage: "Couldn't get user affiliations", + }, + (error, body) => callback(error, body || []) + ) + }, + + getUsersNeedingReconfirmationsLapsedProcessed(callback) { + makeAffiliationRequest( + { + method: 'GET', + path: '/api/v2/institutions/need_reconfirmation_lapsed_processed', + defaultErrorMessage: + 'Could not get users that need reconfirmations lapsed processed', + }, + (error, body) => callback(error, body || []) + ) + }, + + addAffiliation(userId, email, affiliationOptions, callback) { + if (!callback) { + // affiliationOptions is optional + callback = affiliationOptions + affiliationOptions = {} + } + + const { + university, + department, + role, + confirmedAt, + entitlement, + rejectIfBlocklisted, + } = affiliationOptions + makeAffiliationRequest( + { + method: 'POST', + path: `/api/v2/users/${userId.toString()}/affiliations`, + body: { + email, + university, + department, + role, + confirmedAt, + entitlement, + rejectIfBlocklisted, + }, + defaultErrorMessage: "Couldn't create affiliation", + }, + function (error, body) { + if (error) { + if (error.info && error.info.statusCode === 422) { + return callback( + new InvalidInstitutionalEmailError(error.message).withCause(error) + ) + } + return callback(error) + } + if (!university) { + return callback(null, body) + } + + // have notifications delete any ip matcher notifications for this university + NotificationsBuilder.ipMatcherAffiliation(userId).read( + university.id, + function (err) { + if (err) { + // log and ignore error + logger.err( + { err }, + 'Something went wrong marking ip notifications read' + ) + } + callback(null, body) + } + ) + } + ) + }, + + removeAffiliation(userId, email, callback) { + makeAffiliationRequest( + { + method: 'POST', + path: `/api/v2/users/${userId.toString()}/affiliations/remove`, + body: { email }, + extraSuccessStatusCodes: [404], // `Not Found` responses are considered successful + defaultErrorMessage: "Couldn't remove affiliation", + }, + callback + ) + }, + + endorseAffiliation(userId, email, role, department, callback) { + makeAffiliationRequest( + { + method: 'POST', + path: `/api/v2/users/${userId.toString()}/affiliations/endorse`, + body: { email, role, department }, + defaultErrorMessage: "Couldn't endorse affiliation", + }, + callback + ) + }, + + deleteAffiliations(userId, callback) { + makeAffiliationRequest( + { + method: 'DELETE', + path: `/api/v2/users/${userId.toString()}/affiliations`, + defaultErrorMessage: "Couldn't delete affiliations", + }, + callback + ) + }, + + addEntitlement(userId, email, callback) { + makeAffiliationRequest( + { + method: 'POST', + path: `/api/v2/users/${userId}/affiliations/add_entitlement`, + body: { email }, + defaultErrorMessage: "Couldn't add entitlement", + }, + callback + ) + }, + + removeEntitlement(userId, email, callback) { + makeAffiliationRequest( + { + method: 'POST', + path: `/api/v2/users/${userId}/affiliations/remove_entitlement`, + body: { email }, + defaultErrorMessage: "Couldn't remove entitlement", + extraSuccessStatusCodes: [404], + }, + callback + ) + }, + + sendUsersWithReconfirmationsLapsedProcessed(users, callback) { + makeAffiliationRequest( + { + method: 'POST', + path: '/api/v2/institutions/reconfirmation_lapsed_processed', + body: { users }, + defaultErrorMessage: + 'Could not update reconfirmation_lapsed_processed_at', + }, + (error, body) => callback(error, body || []) + ) + }, +} + +var makeAffiliationRequest = function (requestOptions, callback) { + if (!settings.apis.v1.url) { + return callback(null) + } // service is not configured + if (!requestOptions.extraSuccessStatusCodes) { + requestOptions.extraSuccessStatusCodes = [] + } + request( + { + method: requestOptions.method, + url: `${settings.apis.v1.url}${requestOptions.path}`, + body: requestOptions.body, + auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }, + json: true, + timeout: 20 * 1000, + }, + function (error, response, body) { + if (error) { + return callback( + new V1ConnectionError('error getting affiliations from v1').withCause( + error + ) + ) + } + if (response && response.statusCode >= 500) { + return callback( + new V1ConnectionError({ + message: 'error getting affiliations from v1', + info: { + status: response.statusCode, + body: body, + }, + }) + ) + } + let isSuccess = response.statusCode >= 200 && response.statusCode < 300 + if (!isSuccess) { + isSuccess = requestOptions.extraSuccessStatusCodes.includes( + response.statusCode + ) + } + if (!isSuccess) { + let errorMessage + if (body && body.errors) { + errorMessage = `${response.statusCode}: ${body.errors}` + } else { + errorMessage = `${requestOptions.defaultErrorMessage}: ${response.statusCode}` + } + + logger.warn( + { path: requestOptions.path, body: requestOptions.body }, + errorMessage + ) + return callback( + new OError(errorMessage, { statusCode: response.statusCode }) + ) + } + + callback(null, body) + } + ) +} +;[ + 'getInstitutionAffiliations', + 'getUserAffiliations', + 'addAffiliation', + 'removeAffiliation', +].map(method => + metrics.timeAsyncMethod( + InstitutionsAPI, + method, + 'mongo.InstitutionsAPI', + logger + ) +) + +InstitutionsAPI.promises = promisifyAll(InstitutionsAPI) +module.exports = InstitutionsAPI diff --git a/services/web/app/src/Features/Institutions/InstitutionsController.js b/services/web/app/src/Features/Institutions/InstitutionsController.js new file mode 100644 index 0000000000..3c26d3a296 --- /dev/null +++ b/services/web/app/src/Features/Institutions/InstitutionsController.js @@ -0,0 +1,80 @@ +const OError = require('@overleaf/o-error') +const UserGetter = require('../User/UserGetter') +const { addAffiliation } = require('../Institutions/InstitutionsAPI') +const FeaturesUpdater = require('../Subscription/FeaturesUpdater') +const async = require('async') +const ASYNC_AFFILIATIONS_LIMIT = 10 + +module.exports = { + confirmDomain(req, res, next) { + const { hostname } = req.body + affiliateUsers(hostname, function (error) { + if (error) { + return next(error) + } + res.sendStatus(200) + }) + }, +} + +var affiliateUsers = function (hostname, callback) { + const reversedHostname = hostname.trim().split('').reverse().join('') + UserGetter.getUsersByHostname(hostname, { _id: 1 }, function (error, users) { + if (error) { + OError.tag(error, 'problem fetching users by hostname') + return callback(error) + } + + async.mapLimit( + users, + ASYNC_AFFILIATIONS_LIMIT, + (user, innerCallback) => { + UserGetter.getUserFullEmails(user._id, (error, emails) => { + if (error) return innerCallback(error) + user.emails = emails + affiliateUserByReversedHostname(user, reversedHostname, innerCallback) + }) + }, + callback + ) + }) +} + +var affiliateUserByReversedHostname = function ( + user, + reversedHostname, + callback +) { + const matchingEmails = user.emails.filter( + email => email.reversedHostname === reversedHostname + ) + async.mapSeries( + matchingEmails, + (email, innerCallback) => { + addAffiliation( + user._id, + email.email, + { + confirmedAt: email.confirmedAt, + entitlement: + email.samlIdentifier && email.samlIdentifier.hasEntitlement, + }, + error => { + if (error) { + OError.tag( + error, + 'problem adding affiliation while confirming hostname' + ) + return innerCallback(error) + } + FeaturesUpdater.refreshFeatures( + user._id, + 'affiliate-user-by-reversed-hostname', + innerCallback + ) + } + ) + }, + callback + ) +} diff --git a/services/web/app/src/Features/Institutions/InstitutionsFeatures.js b/services/web/app/src/Features/Institutions/InstitutionsFeatures.js new file mode 100644 index 0000000000..dc8bcba353 --- /dev/null +++ b/services/web/app/src/Features/Institutions/InstitutionsFeatures.js @@ -0,0 +1,46 @@ +let InstitutionsFeatures +const UserGetter = require('../User/UserGetter') +const PlansLocator = require('../Subscription/PlansLocator') +const Settings = require('@overleaf/settings') + +module.exports = InstitutionsFeatures = { + getInstitutionsFeatures(userId, callback) { + InstitutionsFeatures.getInstitutionsPlan( + userId, + function (error, planCode) { + if (error) { + return callback(error) + } + const plan = planCode && PlansLocator.findLocalPlanInSettings(planCode) + const features = plan && plan.features + callback(null, features || {}) + } + ) + }, + + getInstitutionsPlan(userId, callback) { + InstitutionsFeatures.hasLicence(userId, function (error, hasLicence) { + if (error) { + return callback(error) + } + if (!hasLicence) { + return callback(null, null) + } + callback(null, Settings.institutionPlanCode) + }) + }, + + hasLicence(userId, callback) { + UserGetter.getUserFullEmails(userId, function (error, emailsData) { + if (error) { + return callback(error) + } + + const hasLicence = emailsData.some( + emailData => emailData.emailHasInstitutionLicence + ) + + callback(null, hasLicence) + }) + }, +} diff --git a/services/web/app/src/Features/Institutions/InstitutionsGetter.js b/services/web/app/src/Features/Institutions/InstitutionsGetter.js new file mode 100644 index 0000000000..312e2df3f7 --- /dev/null +++ b/services/web/app/src/Features/Institutions/InstitutionsGetter.js @@ -0,0 +1,68 @@ +const { callbackify } = require('util') +const UserGetter = require('../User/UserGetter') +const UserMembershipsHandler = require('../UserMembership/UserMembershipsHandler') +const UserMembershipEntityConfigs = require('../UserMembership/UserMembershipEntityConfigs') + +async function _getCurrentAffiliations(userId) { + const fullEmails = await UserGetter.promises.getUserFullEmails(userId) + // current are those confirmed and not with lapsed reconfirmations + return fullEmails + .filter( + emailData => + emailData.confirmedAt && + emailData.affiliation && + emailData.affiliation.institution && + emailData.affiliation.institution.confirmed && + !emailData.affiliation.pastReconfirmDate + ) + .map(emailData => emailData.affiliation) +} + +async function getCurrentInstitutionIds(userId) { + // current are those confirmed and not with lapsed reconfirmations + // only 1 record returned per current institutionId + const institutionIds = new Set() + const currentAffiliations = await _getCurrentAffiliations(userId) + currentAffiliations.forEach(affiliation => { + institutionIds.add(affiliation.institution.id) + }) + return [...institutionIds] +} + +const InstitutionsGetter = { + getConfirmedAffiliations(userId, callback) { + UserGetter.getUserFullEmails(userId, function (error, emailsData) { + if (error) { + return callback(error) + } + + const confirmedAffiliations = emailsData + .filter( + emailData => + emailData.confirmedAt && + emailData.affiliation && + emailData.affiliation.institution && + emailData.affiliation.institution.confirmed + ) + .map(emailData => emailData.affiliation) + + callback(null, confirmedAffiliations) + }) + }, + + getCurrentInstitutionIds: callbackify(getCurrentInstitutionIds), + + getManagedInstitutions(userId, callback) { + UserMembershipsHandler.getEntitiesByUser( + UserMembershipEntityConfigs.institution, + userId, + callback + ) + }, +} + +InstitutionsGetter.promises = { + getCurrentInstitutionIds, +} + +module.exports = InstitutionsGetter diff --git a/services/web/app/src/Features/Institutions/InstitutionsHelper.js b/services/web/app/src/Features/Institutions/InstitutionsHelper.js new file mode 100644 index 0000000000..c4cb486264 --- /dev/null +++ b/services/web/app/src/Features/Institutions/InstitutionsHelper.js @@ -0,0 +1,28 @@ +function emailHasLicence(emailData) { + if (!emailData.confirmedAt) { + return false + } + if (!emailData.affiliation) { + return false + } + const affiliation = emailData.affiliation + const institution = affiliation.institution + if (!institution) { + return false + } + if (!institution.confirmed) { + return false + } + if (!affiliation.licence) { + return false + } + if (affiliation.pastReconfirmDate) { + return false + } + + return affiliation.licence !== 'free' +} + +module.exports = { + emailHasLicence, +} diff --git a/services/web/app/src/Features/Institutions/InstitutionsManager.js b/services/web/app/src/Features/Institutions/InstitutionsManager.js new file mode 100644 index 0000000000..4318394a74 --- /dev/null +++ b/services/web/app/src/Features/Institutions/InstitutionsManager.js @@ -0,0 +1,326 @@ +const async = require('async') +const _ = require('underscore') +const { callbackify } = require('util') +const { ObjectId } = require('mongodb') +const Settings = require('@overleaf/settings') +const { + getInstitutionAffiliations, + promises: InstitutionsAPIPromises, +} = require('./InstitutionsAPI') +const FeaturesUpdater = require('../Subscription/FeaturesUpdater') +const UserGetter = require('../User/UserGetter') +const NotificationsBuilder = require('../Notifications/NotificationsBuilder') +const SubscriptionLocator = require('../Subscription/SubscriptionLocator') +const { Institution } = require('../../models/Institution') +const { Subscription } = require('../../models/Subscription') + +const ASYNC_LIMIT = parseInt(process.env.ASYNC_LIMIT, 10) || 5 + +async function _getSsoUsers(institutionId, lapsedUserIds) { + let currentNotEntitledCount = 0 + const ssoNonEntitledUsersIds = [] + const allSsoUsersByIds = {} + + const allSsoUsers = await UserGetter.promises.getSsoUsersAtInstitution( + institutionId, + { samlIdentifiers: 1 } + ) + allSsoUsers.forEach(user => { + allSsoUsersByIds[user._id] = user.samlIdentifiers.find( + identifer => identifer.providerId === institutionId.toString() + ) + }) + for (const userId in allSsoUsersByIds) { + if (!allSsoUsersByIds[userId].hasEntitlement) { + ssoNonEntitledUsersIds.push(userId) + } + } + if (ssoNonEntitledUsersIds.length > 0) { + currentNotEntitledCount = ssoNonEntitledUsersIds.filter( + id => !lapsedUserIds.includes(id) + ).length + } + + return { + allSsoUsers, + allSsoUsersByIds, + currentNotEntitledCount, + } +} + +async function _checkUsersFeatures(userIds) { + const users = await UserGetter.promises.getUsers(userIds, { features: 1 }) + const result = { + proUserIds: [], + nonProUserIds: [], + } + + await new Promise((resolve, reject) => { + async.eachLimit( + users, + ASYNC_LIMIT, + (user, callback) => { + const hasProFeaturesOrBetter = FeaturesUpdater.isFeatureSetBetter( + user.features, + Settings.features.professional + ) + + if (hasProFeaturesOrBetter) { + result.proUserIds.push(user._id) + } else { + result.nonProUserIds.push(user._id) + } + callback() + }, + error => { + if (error) return reject(error) + resolve() + } + ) + }) + return result +} + +async function checkInstitutionUsers(institutionId) { + /* + v1 has affiliation data. Via getInstitutionAffiliationsCounts, v1 will send + lapsed_user_ids, which includes all user types + (not linked, linked and entitled, linked not entitled). + However, for SSO institutions, it does not know which email is linked + to SSO when the license is non-trivial. Here we need to split that + lapsed count into SSO (entitled and not) or just email users + */ + + const result = { + emailUsers: { + total: 0, // v1 all users - v2 all SSO users + current: 0, // v1 current - v1 SSO entitled - (v2 calculated not entitled current) + lapsed: 0, // v1 lapsed user IDs that are not in v2 SSO users + pro: { + current: 0, + lapsed: 0, + }, + nonPro: { + current: 0, + lapsed: 0, + }, + }, + ssoUsers: { + total: 0, // only v2 + current: { + entitled: 0, // only v1 + notEntitled: 0, // v2 non-entitled SSO users - v1 lapsed user IDs + }, + lapsed: 0, // v2 SSO users that are in v1 lapsed user IDs + pro: { + current: 0, + lapsed: 0, + }, + nonPro: { + current: 0, + lapsed: 0, + }, + }, + } + + const { + user_ids: userIds, // confirmed and not removed users. Includes users with lapsed reconfirmations + current_users_count: currentUsersCount, // all users not with lapsed reconfirmations + lapsed_user_ids: lapsedUserIds, // includes all user types that did not reconfirm (sso entitled, sso not entitled, email only) + with_confirmed_email: withConfirmedEmail, // same count as affiliation metrics + entitled_via_sso: entitled, // same count as affiliation metrics + } = await InstitutionsAPIPromises.getInstitutionAffiliationsCounts( + institutionId + ) + result.ssoUsers.current.entitled = entitled + + const { + allSsoUsers, + allSsoUsersByIds, + currentNotEntitledCount, + } = await _getSsoUsers(institutionId, lapsedUserIds) + result.ssoUsers.total = allSsoUsers.length + result.ssoUsers.current.notEntitled = currentNotEntitledCount + + // check if lapsed user ID an SSO user + const lapsedUsersByIds = {} + lapsedUserIds.forEach(id => { + lapsedUsersByIds[id] = true // create a map for more performant lookups + if (allSsoUsersByIds[id]) { + ++result.ssoUsers.lapsed + } else { + ++result.emailUsers.lapsed + } + }) + + result.emailUsers.current = + currentUsersCount - entitled - result.ssoUsers.current.notEntitled + result.emailUsers.total = userIds.length - allSsoUsers.length + + // compare v1 and v2 counts. + if ( + result.ssoUsers.current.notEntitled + result.emailUsers.current !== + withConfirmedEmail + ) { + result.databaseMismatch = { + withConfirmedEmail: { + v1: withConfirmedEmail, + v2: result.ssoUsers.current.notEntitled + result.emailUsers.current, + }, + } + } + + // Add Pro/NonPro status for users + // NOTE: Users not entitled via institution could have Pro via another method + const { proUserIds, nonProUserIds } = await _checkUsersFeatures(userIds) + proUserIds.forEach(id => { + const userType = lapsedUsersByIds[id] ? 'lapsed' : 'current' + if (allSsoUsersByIds[id]) { + result.ssoUsers.pro[userType]++ + } else { + result.emailUsers.pro[userType]++ + } + }) + nonProUserIds.forEach(id => { + const userType = lapsedUsersByIds[id] ? 'lapsed' : 'current' + if (allSsoUsersByIds[id]) { + result.ssoUsers.nonPro[userType]++ + } else { + result.emailUsers.nonPro[userType]++ + } + }) + return result +} + +const InstitutionsManager = { + refreshInstitutionUsers(institutionId, notify, callback) { + const refreshFunction = notify ? refreshFeaturesAndNotify : refreshFeatures + async.waterfall( + [ + cb => fetchInstitutionAndAffiliations(institutionId, cb), + function (institution, affiliations, cb) { + affiliations = _.map(affiliations, function (affiliation) { + affiliation.institutionName = institution.name + affiliation.institutionId = institutionId + return affiliation + }) + async.eachLimit(affiliations, ASYNC_LIMIT, refreshFunction, err => + cb(err) + ) + }, + ], + callback + ) + }, + + checkInstitutionUsers: callbackify(checkInstitutionUsers), + + getInstitutionUsersSubscriptions(institutionId, callback) { + getInstitutionAffiliations(institutionId, function (error, affiliations) { + if (error) { + return callback(error) + } + const userIds = affiliations.map(affiliation => + ObjectId(affiliation.user_id) + ) + Subscription.find({ + admin_id: userIds, + planCode: { $not: /trial/ }, + }) + .populate('admin_id', 'email') + .exec(callback) + }) + }, +} + +var fetchInstitutionAndAffiliations = (institutionId, callback) => + async.waterfall( + [ + cb => + Institution.findOne({ v1Id: institutionId }, (err, institution) => + cb(err, institution) + ), + (institution, cb) => + institution.fetchV1Data((err, institution) => cb(err, institution)), + (institution, cb) => + getInstitutionAffiliations(institutionId, (err, affiliations) => + cb(err, institution, affiliations) + ), + ], + callback + ) + +var refreshFeatures = function (affiliation, callback) { + const userId = ObjectId(affiliation.user_id) + FeaturesUpdater.refreshFeatures(userId, 'refresh-institution-users', callback) +} + +var refreshFeaturesAndNotify = function (affiliation, callback) { + const userId = ObjectId(affiliation.user_id) + async.waterfall( + [ + cb => + FeaturesUpdater.refreshFeatures( + userId, + 'refresh-institution-users', + (err, features, featuresChanged) => cb(err, featuresChanged) + ), + (featuresChanged, cb) => + getUserInfo(userId, (error, user, subscription) => + cb(error, user, subscription, featuresChanged) + ), + (user, subscription, featuresChanged, cb) => + notifyUser(user, affiliation, subscription, featuresChanged, cb), + ], + callback + ) +} + +var getUserInfo = (userId, callback) => + async.waterfall( + [ + cb => UserGetter.getUser(userId, cb), + (user, cb) => + SubscriptionLocator.getUsersSubscription(user, (err, subscription) => + cb(err, user, subscription) + ), + ], + callback + ) + +var notifyUser = (user, affiliation, subscription, featuresChanged, callback) => + async.parallel( + [ + function (cb) { + if (featuresChanged) { + NotificationsBuilder.featuresUpgradedByAffiliation( + affiliation, + user + ).create(cb) + } else { + cb() + } + }, + function (cb) { + if ( + subscription && + !subscription.planCode.match(/(free|trial)/) && + !subscription.groupPlan + ) { + NotificationsBuilder.redundantPersonalSubscription( + affiliation, + user + ).create(cb) + } else { + cb() + } + }, + ], + callback + ) + +InstitutionsManager.promises = { + checkInstitutionUsers, +} + +module.exports = InstitutionsManager diff --git a/services/web/app/src/Features/LinkedFiles/LinkedFilesController.js b/services/web/app/src/Features/LinkedFiles/LinkedFilesController.js new file mode 100644 index 0000000000..b6f06ee1e9 --- /dev/null +++ b/services/web/app/src/Features/LinkedFiles/LinkedFilesController.js @@ -0,0 +1,192 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let LinkedFilesController +const SessionManager = require('../Authentication/SessionManager') +const EditorController = require('../Editor/EditorController') +const ProjectLocator = require('../Project/ProjectLocator') +const Settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const _ = require('underscore') +const LinkedFilesHandler = require('./LinkedFilesHandler') +const { + CompileFailedError, + UrlFetchFailedError, + InvalidUrlError, + OutputFileFetchFailedError, + AccessDeniedError, + BadEntityTypeError, + BadDataError, + ProjectNotFoundError, + V1ProjectNotFoundError, + SourceFileNotFoundError, + NotOriginalImporterError, + FeatureNotAvailableError, + RemoteServiceError, + FileCannotRefreshError, +} = require('./LinkedFilesErrors') +const Modules = require('../../infrastructure/Modules') + +module.exports = LinkedFilesController = { + Agents: _.extend( + { + url: require('./UrlAgent'), + project_file: require('./ProjectFileAgent'), + project_output_file: require('./ProjectOutputFileAgent'), + }, + Modules.linkedFileAgentsIncludes() + ), + + _getAgent(provider) { + if ( + !Object.prototype.hasOwnProperty.call( + LinkedFilesController.Agents, + provider + ) + ) { + return null + } + if (!Array.from(Settings.enabledLinkedFileTypes).includes(provider)) { + return null + } + return LinkedFilesController.Agents[provider] + }, + + createLinkedFile(req, res, next) { + const { project_id } = req.params + const { name, provider, data, parent_folder_id } = req.body + const user_id = SessionManager.getLoggedInUserId(req.session) + + const Agent = LinkedFilesController._getAgent(provider) + if (Agent == null) { + return res.sendStatus(400) + } + + data.provider = provider + + return Agent.createLinkedFile( + project_id, + data, + name, + parent_folder_id, + user_id, + function (err, newFileId) { + if (err != null) { + return LinkedFilesController.handleError(err, req, res, next) + } + return res.json({ new_file_id: newFileId }) + } + ) + }, + + refreshLinkedFile(req, res, next) { + const { project_id, file_id } = req.params + const user_id = SessionManager.getLoggedInUserId(req.session) + + return LinkedFilesHandler.getFileById( + project_id, + file_id, + function (err, file, path, parentFolder) { + if (err != null) { + return next(err) + } + if (file == null) { + return res.sendStatus(404) + } + const { name } = file + const { linkedFileData } = file + if ( + linkedFileData == null || + (linkedFileData != null ? linkedFileData.provider : undefined) == null + ) { + return res.sendStatus(409) + } + const { provider } = linkedFileData + const parent_folder_id = parentFolder._id + const Agent = LinkedFilesController._getAgent(provider) + if (Agent == null) { + return res.sendStatus(400) + } + + return Agent.refreshLinkedFile( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + function (err, newFileId) { + if (err != null) { + return LinkedFilesController.handleError(err, req, res, next) + } + return res.json({ new_file_id: newFileId }) + } + ) + } + ) + }, + + handleError(error, req, res, next) { + if (error instanceof AccessDeniedError) { + return res.status(403).send('You do not have access to this project') + } else if (error instanceof BadDataError) { + return res.status(400).send('The submitted data is not valid') + } else if (error instanceof BadEntityTypeError) { + return res.status(400).send('The file is the wrong type') + } else if (error instanceof SourceFileNotFoundError) { + return res.status(404).send('Source file not found') + } else if (error instanceof ProjectNotFoundError) { + return res.status(404).send('Project not found') + } else if (error instanceof V1ProjectNotFoundError) { + return res + .status(409) + .send( + 'Sorry, the source project is not yet imported to Overleaf v2. Please import it to Overleaf v2 to refresh this file' + ) + } else if (error instanceof CompileFailedError) { + return res + .status(422) + .send(res.locals.translate('generic_linked_file_compile_error')) + } else if (error instanceof OutputFileFetchFailedError) { + return res.status(404).send('Could not get output file') + } else if (error instanceof UrlFetchFailedError) { + return res + .status(422) + .send( + `Your URL could not be reached (${error.statusCode} status code). Please check it and try again.` + ) + } else if (error instanceof InvalidUrlError) { + return res + .status(422) + .send('Your URL is not valid. Please check it and try again.') + } else if (error instanceof NotOriginalImporterError) { + return res + .status(400) + .send('You are not the user who originally imported this file') + } else if (error instanceof FeatureNotAvailableError) { + return res.status(400).send('This feature is not enabled on your account') + } else if (error instanceof RemoteServiceError) { + return res.status(502).send('The remote service produced an error') + } else if (error instanceof FileCannotRefreshError) { + return res.status(400).send('This file cannot be refreshed') + } else if (error.message === 'project_has_too_many_files') { + return res.status(400).send('too many files') + } else if (/\bECONNREFUSED\b/.test(error.message)) { + return res + .status(500) + .send('Importing references is not currently available') + } else { + return next(error) + } + }, +} diff --git a/services/web/app/src/Features/LinkedFiles/LinkedFilesErrors.js b/services/web/app/src/Features/LinkedFiles/LinkedFilesErrors.js new file mode 100644 index 0000000000..5a1457d51b --- /dev/null +++ b/services/web/app/src/Features/LinkedFiles/LinkedFilesErrors.js @@ -0,0 +1,45 @@ +const { BackwardCompatibleError } = require('../Errors/Errors.js') + +class UrlFetchFailedError extends BackwardCompatibleError {} + +class InvalidUrlError extends BackwardCompatibleError {} + +class CompileFailedError extends BackwardCompatibleError {} +class OutputFileFetchFailedError extends BackwardCompatibleError {} + +class AccessDeniedError extends BackwardCompatibleError {} + +class BadEntityTypeError extends BackwardCompatibleError {} + +class BadDataError extends BackwardCompatibleError {} + +class ProjectNotFoundError extends BackwardCompatibleError {} + +class V1ProjectNotFoundError extends BackwardCompatibleError {} + +class SourceFileNotFoundError extends BackwardCompatibleError {} + +class NotOriginalImporterError extends BackwardCompatibleError {} + +class FeatureNotAvailableError extends BackwardCompatibleError {} + +class RemoteServiceError extends BackwardCompatibleError {} + +class FileCannotRefreshError extends BackwardCompatibleError {} + +module.exports = { + CompileFailedError, + UrlFetchFailedError, + InvalidUrlError, + OutputFileFetchFailedError, + AccessDeniedError, + BadEntityTypeError, + BadDataError, + ProjectNotFoundError, + V1ProjectNotFoundError, + SourceFileNotFoundError, + NotOriginalImporterError, + FeatureNotAvailableError, + RemoteServiceError, + FileCannotRefreshError, +} diff --git a/services/web/app/src/Features/LinkedFiles/LinkedFilesHandler.js b/services/web/app/src/Features/LinkedFiles/LinkedFilesHandler.js new file mode 100644 index 0000000000..1df9af65c3 --- /dev/null +++ b/services/web/app/src/Features/LinkedFiles/LinkedFilesHandler.js @@ -0,0 +1,163 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let LinkedFilesHandler +const FileWriter = require('../../infrastructure/FileWriter') +const EditorController = require('../Editor/EditorController') +const ProjectLocator = require('../Project/ProjectLocator') +const { Project } = require('../../models/Project') +const ProjectGetter = require('../Project/ProjectGetter') +const _ = require('underscore') +const { + ProjectNotFoundError, + V1ProjectNotFoundError, + BadDataError, +} = require('./LinkedFilesErrors') + +module.exports = LinkedFilesHandler = { + getFileById(project_id, file_id, callback) { + if (callback == null) { + callback = function (err, file) {} + } + return ProjectLocator.findElement( + { + project_id, + element_id: file_id, + type: 'file', + }, + function (err, file, path, parentFolder) { + if (err != null) { + return callback(err) + } + return callback(null, file, path, parentFolder) + } + ) + }, + + getSourceProject(data, callback) { + if (callback == null) { + callback = function (err, project) {} + } + const projection = { _id: 1, name: 1 } + if (data.v1_source_doc_id != null) { + return Project.findOne( + { 'overleaf.id': data.v1_source_doc_id }, + projection, + function (err, project) { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(new V1ProjectNotFoundError()) + } + return callback(null, project) + } + ) + } else if (data.source_project_id != null) { + return ProjectGetter.getProject( + data.source_project_id, + projection, + function (err, project) { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(new ProjectNotFoundError()) + } + return callback(null, project) + } + ) + } else { + return callback(new BadDataError('neither v1 nor v2 id present')) + } + }, + + importFromStream( + project_id, + readStream, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) { + if (callback == null) { + callback = function (err, file) {} + } + callback = _.once(callback) + return FileWriter.writeStreamToDisk( + project_id, + readStream, + function (err, fsPath) { + if (err != null) { + return callback(err) + } + return EditorController.upsertFile( + project_id, + parent_folder_id, + name, + fsPath, + linkedFileData, + 'upload', + user_id, + (err, file) => { + if (err != null) { + return callback(err) + } + return callback(null, file) + } + ) + } + ) + }, + + importContent( + project_id, + content, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) { + if (callback == null) { + callback = function (err, file) {} + } + callback = _.once(callback) + return FileWriter.writeContentToDisk( + project_id, + content, + function (err, fsPath) { + if (err != null) { + return callback(err) + } + return EditorController.upsertFile( + project_id, + parent_folder_id, + name, + fsPath, + linkedFileData, + 'upload', + user_id, + (err, file) => { + if (err != null) { + return callback(err) + } + return callback(null, file) + } + ) + } + ) + }, +} diff --git a/services/web/app/src/Features/LinkedFiles/LinkedFilesRouter.js b/services/web/app/src/Features/LinkedFiles/LinkedFilesRouter.js new file mode 100644 index 0000000000..d66825be61 --- /dev/null +++ b/services/web/app/src/Features/LinkedFiles/LinkedFilesRouter.js @@ -0,0 +1,44 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware') +const AuthenticationController = require('../Authentication/AuthenticationController') +const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') +const LinkedFilesController = require('./LinkedFilesController') + +module.exports = { + apply(webRouter) { + webRouter.post( + '/project/:project_id/linked_file', + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + RateLimiterMiddleware.rateLimit({ + endpointName: 'create-linked-file', + params: ['project_id'], + maxRequests: 100, + timeInterval: 60, + }), + LinkedFilesController.createLinkedFile + ) + + return webRouter.post( + '/project/:project_id/linked_file/:file_id/refresh', + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + RateLimiterMiddleware.rateLimit({ + endpointName: 'refresh-linked-file', + params: ['project_id'], + maxRequests: 100, + timeInterval: 60, + }), + LinkedFilesController.refreshLinkedFile + ) + }, +} diff --git a/services/web/app/src/Features/LinkedFiles/ProjectFileAgent.js b/services/web/app/src/Features/LinkedFiles/ProjectFileAgent.js new file mode 100644 index 0000000000..0941cbda6a --- /dev/null +++ b/services/web/app/src/Features/LinkedFiles/ProjectFileAgent.js @@ -0,0 +1,262 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectFileAgent +const AuthorizationManager = require('../Authorization/AuthorizationManager') +const ProjectLocator = require('../Project/ProjectLocator') +const ProjectGetter = require('../Project/ProjectGetter') +const DocstoreManager = require('../Docstore/DocstoreManager') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const FileStoreHandler = require('../FileStore/FileStoreHandler') +const _ = require('underscore') +const Settings = require('@overleaf/settings') +const LinkedFilesHandler = require('./LinkedFilesHandler') +const { + BadDataError, + AccessDeniedError, + BadEntityTypeError, + SourceFileNotFoundError, + ProjectNotFoundError, + V1ProjectNotFoundError, +} = require('./LinkedFilesErrors') + +module.exports = ProjectFileAgent = { + createLinkedFile( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) { + if (!this._canCreate(linkedFileData)) { + return callback(new AccessDeniedError()) + } + return this._go( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) + }, + + refreshLinkedFile( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) { + return this._go( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) + }, + + _prepare(project_id, linkedFileData, user_id, callback) { + if (callback == null) { + callback = function (err, linkedFileData) {} + } + return this._checkAuth( + project_id, + linkedFileData, + user_id, + (err, allowed) => { + if (err != null) { + return callback(err) + } + if (!allowed) { + return callback(new AccessDeniedError()) + } + if (!this._validate(linkedFileData)) { + return callback(new BadDataError()) + } + return callback(null, linkedFileData) + } + ) + }, + + _go(project_id, linkedFileData, name, parent_folder_id, user_id, callback) { + linkedFileData = this._sanitizeData(linkedFileData) + return this._prepare( + project_id, + linkedFileData, + user_id, + (err, linkedFileData) => { + if (err != null) { + return callback(err) + } + if (!this._validate(linkedFileData)) { + return callback(new BadDataError()) + } + return this._getEntity( + linkedFileData, + user_id, + (err, source_project, entity, type) => { + if (err != null) { + return callback(err) + } + if (type === 'doc') { + return DocstoreManager.getDoc( + source_project._id, + entity._id, + function (err, lines) { + if (err != null) { + return callback(err) + } + return LinkedFilesHandler.importContent( + project_id, + lines.join('\n'), + linkedFileData, + name, + parent_folder_id, + user_id, + function (err, file) { + if (err != null) { + return callback(err) + } + return callback(null, file._id) + } + ) + } + ) // Created + } else if (type === 'file') { + return FileStoreHandler.getFileStream( + source_project._id, + entity._id, + null, + function (err, fileStream) { + if (err != null) { + return callback(err) + } + return LinkedFilesHandler.importFromStream( + project_id, + fileStream, + linkedFileData, + name, + parent_folder_id, + user_id, + function (err, file) { + if (err != null) { + return callback(err) + } + return callback(null, file._id) + } + ) + } + ) // Created + } else { + return callback(new BadEntityTypeError()) + } + } + ) + } + ) + }, + + _getEntity(linkedFileData, current_user_id, callback) { + if (callback == null) { + callback = function (err, entity, type) {} + } + callback = _.once(callback) + const { source_entity_path } = linkedFileData + return this._getSourceProject(linkedFileData, function (err, project) { + if (err != null) { + return callback(err) + } + const source_project_id = project._id + return DocumentUpdaterHandler.flushProjectToMongo( + source_project_id, + function (err) { + if (err != null) { + return callback(err) + } + return ProjectLocator.findElementByPath( + { + project_id: source_project_id, + path: source_entity_path, + exactCaseMatch: true, + }, + function (err, entity, type) { + if (err != null) { + if (/^not found.*/.test(err.toString())) { + err = new SourceFileNotFoundError() + } + return callback(err) + } + return callback(null, project, entity, type) + } + ) + } + ) + }) + }, + + _sanitizeData(data) { + return _.pick( + data, + 'provider', + 'source_project_id', + 'v1_source_doc_id', + 'source_entity_path' + ) + }, + + _validate(data) { + return ( + (data.source_project_id != null || data.v1_source_doc_id != null) && + data.source_entity_path != null + ) + }, + + _canCreate(data) { + // Don't allow creation of linked-files with v1 doc ids + return data.v1_source_doc_id == null + }, + + _getSourceProject: LinkedFilesHandler.getSourceProject, + + _checkAuth(project_id, data, current_user_id, callback) { + if (callback == null) { + callback = function (error, allowed) {} + } + callback = _.once(callback) + if (!ProjectFileAgent._validate(data)) { + return callback(new BadDataError()) + } + return this._getSourceProject(data, function (err, project) { + if (err != null) { + return callback(err) + } + return AuthorizationManager.canUserReadProject( + current_user_id, + project._id, + null, + function (err, canRead) { + if (err != null) { + return callback(err) + } + return callback(null, canRead) + } + ) + }) + }, +} diff --git a/services/web/app/src/Features/LinkedFiles/ProjectOutputFileAgent.js b/services/web/app/src/Features/LinkedFiles/ProjectOutputFileAgent.js new file mode 100644 index 0000000000..21979e7924 --- /dev/null +++ b/services/web/app/src/Features/LinkedFiles/ProjectOutputFileAgent.js @@ -0,0 +1,299 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectOutputFileAgent +const AuthorizationManager = require('../Authorization/AuthorizationManager') +const ProjectGetter = require('../Project/ProjectGetter') +const Settings = require('@overleaf/settings') +const CompileManager = require('../Compile/CompileManager') +const ClsiManager = require('../Compile/ClsiManager') +const ProjectFileAgent = require('./ProjectFileAgent') +const _ = require('underscore') +const { + CompileFailedError, + BadDataError, + AccessDeniedError, + BadEntityTypeError, + OutputFileFetchFailedError, +} = require('./LinkedFilesErrors') +const LinkedFilesHandler = require('./LinkedFilesHandler') +const logger = require('logger-sharelatex') + +module.exports = ProjectOutputFileAgent = { + _prepare(project_id, linkedFileData, user_id, callback) { + if (callback == null) { + callback = function (err, linkedFileData) {} + } + return this._checkAuth( + project_id, + linkedFileData, + user_id, + (err, allowed) => { + if (err != null) { + return callback(err) + } + if (!allowed) { + return callback(new AccessDeniedError()) + } + if (!this._validate(linkedFileData)) { + return callback(new BadDataError()) + } + return callback(null, linkedFileData) + } + ) + }, + + createLinkedFile( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) { + if (!this._canCreate(linkedFileData)) { + return callback(new AccessDeniedError()) + } + linkedFileData = this._sanitizeData(linkedFileData) + return this._prepare( + project_id, + linkedFileData, + user_id, + (err, linkedFileData) => { + if (err != null) { + return callback(err) + } + return this._getFileStream( + linkedFileData, + user_id, + (err, readStream) => { + if (err != null) { + return callback(err) + } + readStream.on('error', callback) + return readStream.on('response', response => { + if (response.statusCode >= 200 && response.statusCode < 300) { + readStream.resume() + return LinkedFilesHandler.importFromStream( + project_id, + readStream, + linkedFileData, + name, + parent_folder_id, + user_id, + function (err, file) { + if (err != null) { + return callback(err) + } + return callback(null, file._id) + } + ) // Created + } else { + err = new OutputFileFetchFailedError( + `Output file fetch failed: ${linkedFileData.build_id}, ${linkedFileData.source_output_file_path}` + ) + err.statusCode = response.statusCode + return callback(err) + } + }) + } + ) + } + ) + }, + + refreshLinkedFile( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) { + return this._prepare( + project_id, + linkedFileData, + user_id, + (err, linkedFileData) => { + if (err != null) { + return callback(err) + } + return this._compileAndGetFileStream( + linkedFileData, + user_id, + (err, readStream, new_build_id) => { + if (err != null) { + return callback(err) + } + readStream.on('error', callback) + return readStream.on('response', response => { + if (response.statusCode >= 200 && response.statusCode < 300) { + readStream.resume() + linkedFileData.build_id = new_build_id + return LinkedFilesHandler.importFromStream( + project_id, + readStream, + linkedFileData, + name, + parent_folder_id, + user_id, + function (err, file) { + if (err != null) { + return callback(err) + } + return callback(null, file._id) + } + ) // Created + } else { + err = new OutputFileFetchFailedError( + `Output file fetch failed: ${linkedFileData.build_id}, ${linkedFileData.source_output_file_path}` + ) + err.statusCode = response.statusCode + return callback(err) + } + }) + } + ) + } + ) + }, + + _sanitizeData(data) { + return { + provider: data.provider, + source_project_id: data.source_project_id, + source_output_file_path: data.source_output_file_path, + build_id: data.build_id, + } + }, + + _canCreate: ProjectFileAgent._canCreate, + + _getSourceProject: LinkedFilesHandler.getSourceProject, + + _validate(data) { + if (data.v1_source_doc_id != null) { + return ( + data.v1_source_doc_id != null && data.source_output_file_path != null + ) + } else { + return ( + data.source_project_id != null && + data.source_output_file_path != null && + data.build_id != null + ) + } + }, + + _checkAuth(project_id, data, current_user_id, callback) { + if (callback == null) { + callback = function (err, allowed) {} + } + callback = _.once(callback) + if (!this._validate(data)) { + return callback(new BadDataError()) + } + return this._getSourceProject(data, function (err, project) { + if (err != null) { + return callback(err) + } + return AuthorizationManager.canUserReadProject( + current_user_id, + project._id, + null, + function (err, canRead) { + if (err != null) { + return callback(err) + } + return callback(null, canRead) + } + ) + }) + }, + + _getFileStream(linkedFileData, user_id, callback) { + if (callback == null) { + callback = function (err, fileStream) {} + } + callback = _.once(callback) + const { source_output_file_path, build_id } = linkedFileData + return this._getSourceProject(linkedFileData, function (err, project) { + if (err != null) { + return callback(err) + } + const source_project_id = project._id + return ClsiManager.getOutputFileStream( + source_project_id, + user_id, + build_id, + source_output_file_path, + function (err, readStream) { + if (err != null) { + return callback(err) + } + readStream.pause() + return callback(null, readStream) + } + ) + }) + }, + + _compileAndGetFileStream(linkedFileData, user_id, callback) { + if (callback == null) { + callback = function (err, stream, build_id) {} + } + callback = _.once(callback) + const { source_output_file_path } = linkedFileData + return this._getSourceProject(linkedFileData, function (err, project) { + if (err != null) { + return callback(err) + } + const source_project_id = project._id + return CompileManager.compile( + source_project_id, + user_id, + {}, + function (err, status, outputFiles) { + if (err != null) { + return callback(err) + } + if (status !== 'success') { + return callback(new CompileFailedError()) + } + const outputFile = _.find( + outputFiles, + o => o.path === source_output_file_path + ) + if (outputFile == null) { + return callback(new OutputFileFetchFailedError()) + } + const build_id = outputFile.build + return ClsiManager.getOutputFileStream( + source_project_id, + user_id, + build_id, + source_output_file_path, + function (err, readStream) { + if (err != null) { + return callback(err) + } + readStream.pause() + return callback(null, readStream, build_id) + } + ) + } + ) + }) + }, +} diff --git a/services/web/app/src/Features/LinkedFiles/UrlAgent.js b/services/web/app/src/Features/LinkedFiles/UrlAgent.js new file mode 100644 index 0000000000..2e25447723 --- /dev/null +++ b/services/web/app/src/Features/LinkedFiles/UrlAgent.js @@ -0,0 +1,110 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UrlAgent +const request = require('request') +const _ = require('underscore') +const urlValidator = require('valid-url') +const { InvalidUrlError, UrlFetchFailedError } = require('./LinkedFilesErrors') +const LinkedFilesHandler = require('./LinkedFilesHandler') +const UrlHelper = require('../Helpers/UrlHelper') + +module.exports = UrlAgent = { + createLinkedFile( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) { + linkedFileData = this._sanitizeData(linkedFileData) + return this._getUrlStream( + project_id, + linkedFileData, + user_id, + function (err, readStream) { + if (err != null) { + return callback(err) + } + readStream.on('error', callback) + return readStream.on('response', function (response) { + if (response.statusCode >= 200 && response.statusCode < 300) { + readStream.resume() + return LinkedFilesHandler.importFromStream( + project_id, + readStream, + linkedFileData, + name, + parent_folder_id, + user_id, + function (err, file) { + if (err != null) { + return callback(err) + } + return callback(null, file._id) + } + ) // Created + } else { + const error = new UrlFetchFailedError( + `url fetch failed: ${linkedFileData.url}` + ) + error.statusCode = response.statusCode + return callback(error) + } + }) + } + ) + }, + + refreshLinkedFile( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) { + return this.createLinkedFile( + project_id, + linkedFileData, + name, + parent_folder_id, + user_id, + callback + ) + }, + + _sanitizeData(data) { + return { + provider: data.provider, + url: UrlHelper.prependHttpIfNeeded(data.url), + } + }, + + _getUrlStream(project_id, data, current_user_id, callback) { + if (callback == null) { + callback = function (error, fsPath) {} + } + callback = _.once(callback) + let { url } = data + if (!urlValidator.isWebUri(url)) { + return callback(new InvalidUrlError(`invalid url: ${url}`)) + } + url = UrlHelper.wrapUrlWithProxy(url) + const readStream = request.get(url) + readStream.pause() + return callback(null, readStream) + }, +} diff --git a/services/web/app/src/Features/Metadata/MetaController.js b/services/web/app/src/Features/Metadata/MetaController.js new file mode 100644 index 0000000000..070219c5b1 --- /dev/null +++ b/services/web/app/src/Features/Metadata/MetaController.js @@ -0,0 +1,71 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let MetaController +const OError = require('@overleaf/o-error') +const EditorRealTimeController = require('../Editor/EditorRealTimeController') +const MetaHandler = require('./MetaHandler') +const logger = require('logger-sharelatex') + +module.exports = MetaController = { + getMetadata(req, res, next) { + const { project_id } = req.params + logger.log({ project_id }, 'getting all labels for project') + return MetaHandler.getAllMetaForProject( + project_id, + function (err, projectMeta) { + if (err != null) { + OError.tag( + err, + '[MetaController] error getting all labels from project', + { + project_id, + } + ) + return next(err) + } + return res.json({ projectId: project_id, projectMeta }) + } + ) + }, + + broadcastMetadataForDoc(req, res, next) { + const { project_id } = req.params + const { doc_id } = req.params + const { broadcast } = req.body + logger.log({ project_id, doc_id, broadcast }, 'getting labels for doc') + return MetaHandler.getMetaForDoc( + project_id, + doc_id, + function (err, docMeta) { + if (err != null) { + OError.tag(err, '[MetaController] error getting labels from doc', { + project_id, + doc_id, + }) + return next(err) + } + // default to broadcasting, unless explicitly disabled (for backwards compatibility) + if (broadcast !== false) { + EditorRealTimeController.emitToRoom(project_id, 'broadcastDocMeta', { + docId: doc_id, + meta: docMeta, + }) + return res.sendStatus(200) + } else { + return res.json({ docId: doc_id, meta: docMeta }) + } + } + ) + }, +} diff --git a/services/web/app/src/Features/Metadata/MetaHandler.js b/services/web/app/src/Features/Metadata/MetaHandler.js new file mode 100644 index 0000000000..c65e30051e --- /dev/null +++ b/services/web/app/src/Features/Metadata/MetaHandler.js @@ -0,0 +1,132 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-cond-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let MetaHandler +const ProjectEntityHandler = require('../Project/ProjectEntityHandler') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const packageMapping = require('./packageMapping') + +module.exports = MetaHandler = { + labelRegex() { + return /\\label{(.{0,80}?)}/g + }, + + usepackageRegex() { + return /^\\usepackage(?:\[.{0,80}?])?{(.{0,80}?)}/g + }, + + ReqPackageRegex() { + return /^\\RequirePackage(?:\[.{0,80}?])?{(.{0,80}?)}/g + }, + + getAllMetaForProject(projectId, callback) { + if (callback == null) { + callback = function (err, projectMeta) {} + } + return DocumentUpdaterHandler.flushProjectToMongo( + projectId, + function (err) { + if (err != null) { + return callback(err) + } + return ProjectEntityHandler.getAllDocs(projectId, function (err, docs) { + if (err != null) { + return callback(err) + } + const projectMeta = MetaHandler.extractMetaFromProjectDocs(docs) + return callback(null, projectMeta) + }) + } + ) + }, + + getMetaForDoc(projectId, docId, callback) { + if (callback == null) { + callback = function (err, docMeta) {} + } + return DocumentUpdaterHandler.flushDocToMongo( + projectId, + docId, + function (err) { + if (err != null) { + return callback(err) + } + return ProjectEntityHandler.getDoc( + projectId, + docId, + function (err, lines) { + if (err != null) { + return callback(err) + } + const docMeta = MetaHandler.extractMetaFromDoc(lines) + return callback(null, docMeta) + } + ) + } + ) + }, + + extractMetaFromDoc(lines) { + let pkg + const docMeta = { labels: [], packages: {} } + const packages = [] + const label_re = MetaHandler.labelRegex() + const package_re = MetaHandler.usepackageRegex() + const req_package_re = MetaHandler.ReqPackageRegex() + for (const line of Array.from(lines)) { + var labelMatch + var clean, messy, packageMatch + while ((labelMatch = label_re.exec(line))) { + var label + if ((label = labelMatch[1])) { + docMeta.labels.push(label) + } + } + while ((packageMatch = package_re.exec(line))) { + if ((messy = packageMatch[1])) { + for (pkg of Array.from(messy.split(','))) { + if ((clean = pkg.trim())) { + packages.push(clean) + } + } + } + } + while ((packageMatch = req_package_re.exec(line))) { + if ((messy = packageMatch[1])) { + for (pkg of Array.from(messy.split(','))) { + if ((clean = pkg.trim())) { + packages.push(clean) + } + } + } + } + } + for (pkg of Array.from(packages)) { + if (packageMapping[pkg] != null) { + docMeta.packages[pkg] = packageMapping[pkg] + } + } + return docMeta + }, + + extractMetaFromProjectDocs(projectDocs) { + const projectMeta = {} + for (const _path in projectDocs) { + const doc = projectDocs[_path] + projectMeta[doc._id] = MetaHandler.extractMetaFromDoc(doc.lines) + } + return projectMeta + }, +} diff --git a/services/web/app/src/Features/Metadata/packageMapping.js b/services/web/app/src/Features/Metadata/packageMapping.js new file mode 100644 index 0000000000..e268902305 --- /dev/null +++ b/services/web/app/src/Features/Metadata/packageMapping.js @@ -0,0 +1,71595 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +module.exports = { + inputenc: [ + { + caption: '\\inputencoding{}', + snippet: '\\inputencoding{$1}', + meta: 'inputenc-cmd', + score: 0.0002447047447770061, + }, + ], + graphicx: [ + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'graphicx-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'graphicx-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'graphicx-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'graphicx-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'graphicx-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'graphicx-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'graphicx-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'graphicx-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'graphicx-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'graphicx-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'graphicx-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'graphicx-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'graphicx-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'graphicx-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'graphicx-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'graphicx-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'graphicx-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'graphicx-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'graphicx-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'graphicx-cmd', + score: 0.008565354665444157, + }, + ], + amsmath: [ + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'amsmath-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'amsmath-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'amsmath-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'amsmath-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'amsmath-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'amsmath-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'amsmath-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'amsmath-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'amsmath-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'amsmath-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'amsmath-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'amsmath-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'amsmath-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'amsmath-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'amsmath-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'amsmath-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'amsmath-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'amsmath-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'amsmath-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'amsmath-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'amsmath-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'amsmath-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'amsmath-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'amsmath-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'amsmath-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'amsmath-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'amsmath-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'amsmath-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'amsmath-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'amsmath-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'amsmath-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'amsmath-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'amsmath-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'amsmath-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'amsmath-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'amsmath-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'amsmath-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'amsmath-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'amsmath-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'amsmath-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'amsmath-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'amsmath-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'amsmath-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'amsmath-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'amsmath-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'amsmath-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'amsmath-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'amsmath-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'amsmath-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'amsmath-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'amsmath-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'amsmath-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'amsmath-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'amsmath-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'amsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'amsmath-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'amsmath-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'amsmath-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'amsmath-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'amsmath-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'amsmath-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'amsmath-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'amsmath-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'amsmath-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'amsmath-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'amsmath-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'amsmath-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'amsmath-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'amsmath-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'amsmath-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'amsmath-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'amsmath-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'amsmath-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'amsmath-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'amsmath-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'amsmath-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'amsmath-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'amsmath-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'amsmath-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'amsmath-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'amsmath-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'amsmath-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'amsmath-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'amsmath-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'amsmath-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'amsmath-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'amsmath-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'amsmath-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'amsmath-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'amsmath-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'amsmath-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'amsmath-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'amsmath-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'amsmath-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'amsmath-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'amsmath-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'amsmath-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'amsmath-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'amsmath-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'amsmath-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'amsmath-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'amsmath-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'amsmath-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'amsmath-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'amsmath-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'amsmath-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'amsmath-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'amsmath-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'amsmath-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'amsmath-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'amsmath-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'amsmath-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'amsmath-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'amsmath-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'amsmath-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'amsmath-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'amsmath-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'amsmath-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'amsmath-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'amsmath-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'amsmath-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'amsmath-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'amsmath-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'amsmath-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'amsmath-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'amsmath-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'amsmath-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'amsmath-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'amsmath-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'amsmath-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'amsmath-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'amsmath-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'amsmath-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'amsmath-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'amsmath-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'amsmath-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'amsmath-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'amsmath-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'amsmath-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'amsmath-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'amsmath-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'amsmath-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'amsmath-cmd', + score: 0.0063276692758974925, + }, + ], + geometry: [ + { + caption: '\\savegeometry{}', + snippet: '\\savegeometry{$1}', + meta: 'geometry-cmd', + score: 6.461638865465447e-5, + }, + { + caption: '\\loadgeometry{}', + snippet: '\\loadgeometry{$1}', + meta: 'geometry-cmd', + score: 6.461638865465447e-5, + }, + { + caption: '\\newgeometry{}', + snippet: '\\newgeometry{$1}', + meta: 'geometry-cmd', + score: 0.0025977479207639352, + }, + { + caption: '\\geometry{}', + snippet: '\\geometry{$1}', + meta: 'geometry-cmd', + score: 0.046218420429973615, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'geometry-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\restoregeometry', + snippet: '\\restoregeometry', + meta: 'geometry-cmd', + score: 0.0007546303842143648, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'geometry-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'geometry-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'geometry-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'geometry-cmd', + score: 0.00021116765384691477, + }, + ], + amssymb: [ + { + caption: '\\frak{}', + snippet: '\\frak{$1}', + meta: 'amssymb-cmd', + score: 0.0017966000518546787, + }, + { + caption: '\\checkmark', + snippet: '\\checkmark', + meta: 'amssymb-cmd', + score: 0.025060530944368123, + }, + { + caption: '\\bold', + snippet: '\\bold', + meta: 'amssymb-cmd', + score: 0.0014358547624941567, + }, + { + caption: '\\bold{}', + snippet: '\\bold{$1}', + meta: 'amssymb-cmd', + score: 0.0014358547624941567, + }, + { + caption: '\\Bbb{}', + snippet: '\\Bbb{$1}', + meta: 'amssymb-cmd', + score: 0.0006671850995492977, + }, + { + caption: '\\Bbb', + snippet: '\\Bbb', + meta: 'amssymb-cmd', + score: 0.0006671850995492977, + }, + ], + hyperref: [ + { + caption: '\\nameref{}', + snippet: '\\nameref{$1}', + meta: 'hyperref-cmd', + score: 0.009472569279662113, + }, + { + caption: '\\pdfbookmark[]{}{}', + snippet: '\\pdfbookmark[$1]{$2}{$3}', + meta: 'hyperref-cmd', + score: 0.006492248863367502, + }, + { + caption: '\\figureautorefname', + snippet: '\\figureautorefname', + meta: 'hyperref-cmd', + score: 0.00014582556188448738, + }, + { + caption: '\\figureautorefname{}', + snippet: '\\figureautorefname{$1}', + meta: 'hyperref-cmd', + score: 0.00014582556188448738, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'hyperref-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hyperref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hyperref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\footnoteautorefname', + snippet: '\\footnoteautorefname', + meta: 'hyperref-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\roman{}', + snippet: '\\roman{$1}', + meta: 'hyperref-cmd', + score: 0.005553384455935491, + }, + { + caption: '\\roman', + snippet: '\\roman', + meta: 'hyperref-cmd', + score: 0.005553384455935491, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'hyperref-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\MakeLowercase{}', + snippet: '\\MakeLowercase{$1}', + meta: 'hyperref-cmd', + score: 0.017289599800633146, + }, + { + caption: '\\textunderscore', + snippet: '\\textunderscore', + meta: 'hyperref-cmd', + score: 0.001509072212764015, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'hyperref-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\begin{}', + snippet: '\\begin{$1}', + meta: 'hyperref-cmd', + score: 7.849662248028187, + }, + { + caption: '\\begin{}[]', + snippet: '\\begin{$1}[$2]', + meta: 'hyperref-cmd', + score: 7.849662248028187, + }, + { + caption: '\\begin{}{}', + snippet: '\\begin{$1}{$2}', + meta: 'hyperref-cmd', + score: 7.849662248028187, + }, + { + caption: '\\FancyVerbLineautorefname', + snippet: '\\FancyVerbLineautorefname', + meta: 'hyperref-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\hyperlink{}{}', + snippet: '\\hyperlink{$1}{$2}', + meta: 'hyperref-cmd', + score: 0.00978652043902115, + }, + { + caption: '\\tableautorefname', + snippet: '\\tableautorefname', + meta: 'hyperref-cmd', + score: 0.00012704528567339081, + }, + { + caption: '\\tableautorefname{}', + snippet: '\\tableautorefname{$1}', + meta: 'hyperref-cmd', + score: 0.00012704528567339081, + }, + { + caption: '\\equationautorefname', + snippet: '\\equationautorefname', + meta: 'hyperref-cmd', + score: 0.00018777198999871106, + }, + { + caption: '\\equationautorefname{}', + snippet: '\\equationautorefname{$1}', + meta: 'hyperref-cmd', + score: 0.00018777198999871106, + }, + { + caption: '\\chapterautorefname', + snippet: '\\chapterautorefname', + meta: 'hyperref-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\TeX', + snippet: '\\TeX', + meta: 'hyperref-cmd', + score: 0.02873756018238537, + }, + { + caption: '\\TeX{}', + snippet: '\\TeX{$1}', + meta: 'hyperref-cmd', + score: 0.02873756018238537, + }, + { + caption: '\\protect', + snippet: '\\protect', + meta: 'hyperref-cmd', + score: 0.0200686676229443, + }, + { + caption: '\\appendixautorefname', + snippet: '\\appendixautorefname', + meta: 'hyperref-cmd', + score: 7.950698053641679e-5, + }, + { + caption: '\\appendixautorefname{}', + snippet: '\\appendixautorefname{$1}', + meta: 'hyperref-cmd', + score: 7.950698053641679e-5, + }, + { + caption: '\\newlabel{}{}', + snippet: '\\newlabel{$1}{$2}', + meta: 'hyperref-cmd', + score: 0.00029737672328168955, + }, + { + caption: '\\texorpdfstring{}{}', + snippet: '\\texorpdfstring{$1}{$2}', + meta: 'hyperref-cmd', + score: 0.0073781967296121, + }, + { + caption: '\\refstepcounter{}', + snippet: '\\refstepcounter{$1}', + meta: 'hyperref-cmd', + score: 0.002140559856649122, + }, + { + caption: '\\alph', + snippet: '\\alph', + meta: 'hyperref-cmd', + score: 0.01034327266194849, + }, + { + caption: '\\alph{}', + snippet: '\\alph{$1}', + meta: 'hyperref-cmd', + score: 0.01034327266194849, + }, + { + caption: '\\pageref{}', + snippet: '\\pageref{$1}', + meta: 'hyperref-cmd', + score: 0.019788865471151957, + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'hyperref-cmd', + score: 3.800886892251021, + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'hyperref-cmd', + score: 3.800886892251021, + }, + { + caption: '\\LaTeX', + snippet: '\\LaTeX', + meta: 'hyperref-cmd', + score: 0.2334089308452787, + }, + { + caption: '\\LaTeX{}', + snippet: '\\LaTeX{$1}', + meta: 'hyperref-cmd', + score: 0.2334089308452787, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\itemautorefname', + snippet: '\\itemautorefname', + meta: 'hyperref-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'hyperref-cmd', + score: 1.2569477427490174, + }, + { + caption: '\\sectionautorefname', + snippet: '\\sectionautorefname', + meta: 'hyperref-cmd', + score: 0.0019832324299155183, + }, + { + caption: '\\sectionautorefname{}', + snippet: '\\sectionautorefname{$1}', + meta: 'hyperref-cmd', + score: 0.0019832324299155183, + }, + { + caption: '\\LaTeXe', + snippet: '\\LaTeXe', + meta: 'hyperref-cmd', + score: 0.007928096378157487, + }, + { + caption: '\\LaTeXe{}', + snippet: '\\LaTeXe{$1}', + meta: 'hyperref-cmd', + score: 0.007928096378157487, + }, + { + caption: '\\footref{}', + snippet: '\\footref{$1}', + meta: 'hyperref-cmd', + score: 0.0003680857021151614, + }, + { + caption: '\\footref', + snippet: '\\footref', + meta: 'hyperref-cmd', + score: 0.0003680857021151614, + }, + { + caption: '\\hypertarget{}{}', + snippet: '\\hypertarget{$1}{$2}', + meta: 'hyperref-cmd', + score: 0.009652820108904094, + }, + { + caption: '\\theoremautorefname', + snippet: '\\theoremautorefname', + meta: 'hyperref-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'hyperref-cmd', + score: 0.7504160124360846, + }, + { + caption: '\\subparagraphautorefname', + snippet: '\\subparagraphautorefname', + meta: 'hyperref-cmd', + score: 0.0005446476945175932, + }, + { + caption: '\\url{}', + snippet: '\\url{$1}', + meta: 'hyperref-cmd', + score: 0.13586474005868793, + }, + { + caption: '\\author{}', + snippet: '\\author{$1}', + meta: 'hyperref-cmd', + score: 0.8973590434087177, + }, + { + caption: '\\author[]{}', + snippet: '\\author[$1]{$2}', + meta: 'hyperref-cmd', + score: 0.8973590434087177, + }, + { + caption: '\\href{}{}', + snippet: '\\href{$1}{$2}', + meta: 'hyperref-cmd', + score: 0.27111130260612365, + }, + { + caption: '\\Roman{}', + snippet: '\\Roman{$1}', + meta: 'hyperref-cmd', + score: 0.0038703587462843594, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hyperref-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\autoref{}', + snippet: '\\autoref{$1}', + meta: 'hyperref-cmd', + score: 0.03741172773691362, + }, + { + caption: '\\nolinkurl{}', + snippet: '\\nolinkurl{$1}', + meta: 'hyperref-cmd', + score: 0.0004995635515943437, + }, + { + caption: '\\end{}', + snippet: '\\end{$1}', + meta: 'hyperref-cmd', + score: 7.847906405228455, + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'hyperref-cmd', + score: 0.0174633138331273, + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'hyperref-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'hyperref-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\partautorefname', + snippet: '\\partautorefname', + meta: 'hyperref-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\Itemautorefname{}', + snippet: '\\Itemautorefname{$1}', + meta: 'hyperref-cmd', + score: 6.006262128895586e-5, + }, + { + caption: '\\halign{}', + snippet: '\\halign{$1}', + meta: 'hyperref-cmd', + score: 0.00017906650306643613, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'hyperref-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\ref{}', + snippet: '\\ref{$1}', + meta: 'hyperref-cmd', + score: 1.4380093454211778, + }, + { + caption: '\\Alph{}', + snippet: '\\Alph{$1}', + meta: 'hyperref-cmd', + score: 0.002233258780143355, + }, + { + caption: '\\Alph', + snippet: '\\Alph', + meta: 'hyperref-cmd', + score: 0.002233258780143355, + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'hyperref-cmd', + score: 0.047007158741781095, + }, + { + caption: '\\MP', + snippet: '\\MP', + meta: 'hyperref-cmd', + score: 0.00018344383742255004, + }, + { + caption: '\\MP{}', + snippet: '\\MP{$1}', + meta: 'hyperref-cmd', + score: 0.00018344383742255004, + }, + { + caption: '\\paragraphautorefname', + snippet: '\\paragraphautorefname', + meta: 'hyperref-cmd', + score: 0.0005446476945175932, + }, + { + caption: '\\citeN{}', + snippet: '\\citeN{$1}', + meta: 'hyperref-cmd', + score: 0.0018503938529945614, + }, + { + caption: '\\citeN', + snippet: '\\citeN', + meta: 'hyperref-cmd', + score: 0.0018503938529945614, + }, + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'hyperref-cmd', + score: 0.07503475348393239, + }, + { + caption: '\\subsectionautorefname', + snippet: '\\subsectionautorefname', + meta: 'hyperref-cmd', + score: 0.0012546605780895737, + }, + { + caption: '\\subsectionautorefname{}', + snippet: '\\subsectionautorefname{$1}', + meta: 'hyperref-cmd', + score: 0.0012546605780895737, + }, + { + caption: '\\hyperref[]{}', + snippet: '\\hyperref[$1]{$2}', + meta: 'hyperref-cmd', + score: 0.004515152477030062, + }, + { + caption: '\\arabic{}', + snippet: '\\arabic{$1}', + meta: 'hyperref-cmd', + score: 0.02445837629741638, + }, + { + caption: '\\arabic', + snippet: '\\arabic', + meta: 'hyperref-cmd', + score: 0.02445837629741638, + }, + { + caption: '\\newline', + snippet: '\\newline', + meta: 'hyperref-cmd', + score: 0.3311721696201715, + }, + { + caption: '\\hypersetup{}', + snippet: '\\hypersetup{$1}', + meta: 'hyperref-cmd', + score: 0.06967310843464661, + }, + { + caption: '\\subsubsectionautorefname', + snippet: '\\subsubsectionautorefname', + meta: 'hyperref-cmd', + score: 0.0012064581899162352, + }, + { + caption: '\\subsubsectionautorefname{}', + snippet: '\\subsubsectionautorefname{$1}', + meta: 'hyperref-cmd', + score: 0.0012064581899162352, + }, + { + caption: '\\title{}', + snippet: '\\title{$1}', + meta: 'hyperref-cmd', + score: 0.9202908262245683, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'hyperref-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hyperref-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'hyperref-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'hyperref-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'hyperref-cmd', + score: 0.0002854206807593436, + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'hyperref-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'hyperref-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'hyperref-cmd', + score: 0.010515056688180681, + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'hyperref-cmd', + score: 0.008041789461944983, + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'hyperref-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'hyperref-cmd', + score: 0.0032990580087398644, + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'hyperref-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'hyperref-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hyperref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hyperref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'hyperref-cmd', + score: 0.00047530324346933345, + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'hyperref-cmd', + score: 0.0005277905480209891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hyperref-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hyperref-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hyperref-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hyperref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hyperref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'hyperref-cmd', + score: 0.0058342578961340175, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'hyperref-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hyperref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hyperref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hyperref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'hyperref-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hyperref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hyperref-cmd', + score: 0.00530510025314411, + }, + ], + babel: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'babel-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'babel-cmd', + score: 0.021170869458413965, + }, + ], + color: [ + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'color-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'color-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'color-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'color-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'color-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'color-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'color-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'color-cmd', + score: 0.2864294797053033, + }, + ], + xcolor: [ + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'xcolor-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xcolor-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xcolor-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'xcolor-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'xcolor-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'xcolor-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'xcolor-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'xcolor-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xcolor-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'xcolor-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'xcolor-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xcolor-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'xcolor-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'xcolor-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xcolor-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xcolor-cmd', + score: 0.2864294797053033, + }, + ], + natbib: [ + { + caption: '\\citealt{}', + snippet: '\\citealt{$1}', + meta: 'natbib-cmd', + score: 0.007302105441724955, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'natbib-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'natbib-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\textsuperscript{}', + snippet: '\\textsuperscript{$1}', + meta: 'natbib-cmd', + score: 0.05216393882408519, + }, + { + caption: '\\nocite{}', + snippet: '\\nocite{$1}', + meta: 'natbib-cmd', + score: 0.04990693820960752, + }, + { + caption: '\\bibname', + snippet: '\\bibname', + meta: 'natbib-cmd', + score: 0.007599529252128519, + }, + { + caption: '\\bibname{}', + snippet: '\\bibname{$1}', + meta: 'natbib-cmd', + score: 0.007599529252128519, + }, + { + caption: '\\bibpunct', + snippet: '\\bibpunct', + meta: 'natbib-cmd', + score: 0.001148574749873469, + }, + { + caption: '\\bibpunct{}{}{}{}{}{}', + snippet: '\\bibpunct{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'natbib-cmd', + score: 0.001148574749873469, + }, + { + caption: '\\bibpunct[]{}{}{}{}{}{}', + snippet: '\\bibpunct[$1]{$2}{$3}{$4}{$5}{$6}{$7}', + meta: 'natbib-cmd', + score: 0.001148574749873469, + }, + { + caption: '\\citepalias{}', + snippet: '\\citepalias{$1}', + meta: 'natbib-cmd', + score: 0.00032712684909035603, + }, + { + caption: '\\citepalias[][]{}', + snippet: '\\citepalias[$1][$2]{$3}', + meta: 'natbib-cmd', + score: 0.00032712684909035603, + }, + { + caption: '\\makeindex', + snippet: '\\makeindex', + meta: 'natbib-cmd', + score: 0.010304996748556729, + }, + { + caption: '\\citep{}', + snippet: '\\citep{$1}', + meta: 'natbib-cmd', + score: 0.2941882834697057, + }, + { + caption: '\\bibsection', + snippet: '\\bibsection', + meta: 'natbib-cmd', + score: 0.00038872734530908233, + }, + { + caption: '\\bibsection{}', + snippet: '\\bibsection{$1}', + meta: 'natbib-cmd', + score: 0.00038872734530908233, + }, + { + caption: '\\refname', + snippet: '\\refname', + meta: 'natbib-cmd', + score: 0.006490238196722249, + }, + { + caption: '\\refname{}', + snippet: '\\refname{$1}', + meta: 'natbib-cmd', + score: 0.006490238196722249, + }, + { + caption: '\\citealp{}', + snippet: '\\citealp{$1}', + meta: 'natbib-cmd', + score: 0.005275912376595364, + }, + { + caption: '\\citealp[]{}', + snippet: '\\citealp[$1]{$2}', + meta: 'natbib-cmd', + score: 0.005275912376595364, + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'natbib-cmd', + score: 2.341195220791228, + }, + { + caption: '\\citetalias{}', + snippet: '\\citetalias{$1}', + meta: 'natbib-cmd', + score: 0.001419571355756266, + }, + { + caption: '\\bibitem{}', + snippet: '\\bibitem{$1}', + meta: 'natbib-cmd', + score: 0.3689547570562042, + }, + { + caption: '\\bibitem[]{}', + snippet: '\\bibitem[$1]{$2}', + meta: 'natbib-cmd', + score: 0.3689547570562042, + }, + { + caption: '\\citet{}', + snippet: '\\citet{$1}', + meta: 'natbib-cmd', + score: 0.09046048561361801, + }, + { + caption: '\\defcitealias{}{}', + snippet: '\\defcitealias{$1}{$2}', + meta: 'natbib-cmd', + score: 0.00042021825647418025, + }, + { + caption: '\\aftergroup', + snippet: '\\aftergroup', + meta: 'natbib-cmd', + score: 0.002020423627422133, + }, + { + caption: '\\setcitestyle{}', + snippet: '\\setcitestyle{$1}', + meta: 'natbib-cmd', + score: 0.0015840652870152204, + }, + { + caption: '\\citeyearpar{}', + snippet: '\\citeyearpar{$1}', + meta: 'natbib-cmd', + score: 0.001877888310324327, + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'natbib-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'natbib-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\newblock', + snippet: '\\newblock', + meta: 'natbib-cmd', + score: 0.03684301726876973, + }, + { + caption: '\\newblock{}', + snippet: '\\newblock{$1}', + meta: 'natbib-cmd', + score: 0.03684301726876973, + }, + { + caption: '\\bibnumfmt', + snippet: '\\bibnumfmt', + meta: 'natbib-cmd', + score: 0.000353353600267394, + }, + { + caption: '\\citeyear{}', + snippet: '\\citeyear{$1}', + meta: 'natbib-cmd', + score: 0.01091041305836494, + }, + { + caption: '\\citeauthor{}', + snippet: '\\citeauthor{$1}', + meta: 'natbib-cmd', + score: 0.01359248786373484, + }, + ], + url: [ + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'url-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'url-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'url-cmd', + score: 0.0002854206807593436, + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'url-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'url-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'url-cmd', + score: 0.010515056688180681, + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'url-cmd', + score: 0.008041789461944983, + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'url-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'url-cmd', + score: 0.0032990580087398644, + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'url-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'url-cmd', + score: 3.7048287721105874e-5, + }, + ], + fontenc: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'fontenc-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'fontenc-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'fontenc-cmd', + score: 0.021170869458413965, + }, + ], + tikz: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikz-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikz-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikz-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikz-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikz-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikz-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikz-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikz-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikz-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tikz-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tikz-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tikz-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tikz-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tikz-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tikz-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tikz-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tikz-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tikz-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tikz-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikz-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikz-cmd', + score: 0.2864294797053033, + }, + ], + fancyhdr: [ + { + caption: '\\lhead{}', + snippet: '\\lhead{$1}', + meta: 'fancyhdr-cmd', + score: 0.05268978171228714, + }, + { + caption: '\\chaptermark', + snippet: '\\chaptermark', + meta: 'fancyhdr-cmd', + score: 0.005924520024686584, + }, + { + caption: '\\chaptermark{}', + snippet: '\\chaptermark{$1}', + meta: 'fancyhdr-cmd', + score: 0.005924520024686584, + }, + { + caption: '\\fancypagestyle{}{}', + snippet: '\\fancypagestyle{$1}{$2}', + meta: 'fancyhdr-cmd', + score: 0.009430919590937878, + }, + { + caption: '\\footrule', + snippet: '\\footrule', + meta: 'fancyhdr-cmd', + score: 0.0010032754348913366, + }, + { + caption: '\\footrule{}', + snippet: '\\footrule{$1}', + meta: 'fancyhdr-cmd', + score: 0.0010032754348913366, + }, + { + caption: '\\fancyfoot[]{}', + snippet: '\\fancyfoot[$1]{$2}', + meta: 'fancyhdr-cmd', + score: 0.024973618823189894, + }, + { + caption: '\\fancyfoot{}', + snippet: '\\fancyfoot{$1}', + meta: 'fancyhdr-cmd', + score: 0.024973618823189894, + }, + { + caption: '\\fancyfootoffset[]{}', + snippet: '\\fancyfootoffset[$1]{$2}', + meta: 'fancyhdr-cmd', + score: 0.0015373246231684555, + }, + { + caption: '\\fancyfootoffset{}', + snippet: '\\fancyfootoffset{$1}', + meta: 'fancyhdr-cmd', + score: 0.0015373246231684555, + }, + { + caption: '\\footruleskip', + snippet: '\\footruleskip', + meta: 'fancyhdr-cmd', + score: 0.000830117957327721, + }, + { + caption: '\\fancyheadoffset[]{}', + snippet: '\\fancyheadoffset[$1]{$2}', + meta: 'fancyhdr-cmd', + score: 0.0016786568695309166, + }, + { + caption: '\\fancyheadoffset{}', + snippet: '\\fancyheadoffset{$1}', + meta: 'fancyhdr-cmd', + score: 0.0016786568695309166, + }, + { + caption: '\\iffloatpage{}{}', + snippet: '\\iffloatpage{$1}{$2}', + meta: 'fancyhdr-cmd', + score: 6.606286310833368e-5, + }, + { + caption: '\\cfoot{}', + snippet: '\\cfoot{$1}', + meta: 'fancyhdr-cmd', + score: 0.013411641301057813, + }, + { + caption: '\\subsectionmark', + snippet: '\\subsectionmark', + meta: 'fancyhdr-cmd', + score: 3.1153423008593836e-5, + }, + { + caption: '\\footrulewidth', + snippet: '\\footrulewidth', + meta: 'fancyhdr-cmd', + score: 0.011424740897486949, + }, + { + caption: '\\fancyhfoffset[]{}', + snippet: '\\fancyhfoffset[$1]{$2}', + meta: 'fancyhdr-cmd', + score: 3.741978601121172e-5, + }, + { + caption: '\\rhead{}', + snippet: '\\rhead{$1}', + meta: 'fancyhdr-cmd', + score: 0.022782817416731292, + }, + { + caption: '\\fancyplain{}{}', + snippet: '\\fancyplain{$1}{$2}', + meta: 'fancyhdr-cmd', + score: 0.007402339896386138, + }, + { + caption: '\\rfoot{}', + snippet: '\\rfoot{$1}', + meta: 'fancyhdr-cmd', + score: 0.013393817825547868, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'fancyhdr-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\plainheadrulewidth', + snippet: '\\plainheadrulewidth', + meta: 'fancyhdr-cmd', + score: 6.2350576842596716e-6, + }, + { + caption: '\\baselinestretch', + snippet: '\\baselinestretch', + meta: 'fancyhdr-cmd', + score: 0.03225350148161425, + }, + { + caption: '\\lfoot{}', + snippet: '\\lfoot{$1}', + meta: 'fancyhdr-cmd', + score: 0.00789399846642229, + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'fancyhdr-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'fancyhdr-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\fancyhf{}', + snippet: '\\fancyhf{$1}', + meta: 'fancyhdr-cmd', + score: 0.02314618933449356, + }, + { + caption: '\\sectionmark', + snippet: '\\sectionmark', + meta: 'fancyhdr-cmd', + score: 0.005008938879210868, + }, + { + caption: '\\fancyhead[]{}', + snippet: '\\fancyhead[$1]{$2}', + meta: 'fancyhdr-cmd', + score: 0.039101068064744296, + }, + { + caption: '\\fancyhead{}', + snippet: '\\fancyhead{$1}', + meta: 'fancyhdr-cmd', + score: 0.039101068064744296, + }, + { + caption: '\\nouppercase{}', + snippet: '\\nouppercase{$1}', + meta: 'fancyhdr-cmd', + score: 0.006416387071584083, + }, + { + caption: '\\nouppercase', + snippet: '\\nouppercase', + meta: 'fancyhdr-cmd', + score: 0.006416387071584083, + }, + { + caption: '\\headrule', + snippet: '\\headrule', + meta: 'fancyhdr-cmd', + score: 0.0008327432627715623, + }, + { + caption: '\\headrule{}', + snippet: '\\headrule{$1}', + meta: 'fancyhdr-cmd', + score: 0.0008327432627715623, + }, + { + caption: '\\chead{}', + snippet: '\\chead{$1}', + meta: 'fancyhdr-cmd', + score: 0.00755042164734884, + }, + { + caption: '\\headrulewidth', + snippet: '\\headrulewidth', + meta: 'fancyhdr-cmd', + score: 0.02268137935335823, + }, + ], + booktabs: [ + { + caption: '\\specialrule{}{}{}', + snippet: '\\specialrule{$1}{$2}{$3}', + meta: 'booktabs-cmd', + score: 0.004974385202605165, + }, + { + caption: '\\cmidrule', + snippet: '\\cmidrule', + meta: 'booktabs-cmd', + score: 0.01894952272365088, + }, + { + caption: '\\cmidrule{}', + snippet: '\\cmidrule{$1}', + meta: 'booktabs-cmd', + score: 0.01894952272365088, + }, + { + caption: '\\bottomrule', + snippet: '\\bottomrule', + meta: 'booktabs-cmd', + score: 0.04533364657852219, + }, + { + caption: '\\midrule', + snippet: '\\midrule', + meta: 'booktabs-cmd', + score: 0.07098077735912875, + }, + { + caption: '\\addlinespace', + snippet: '\\addlinespace', + meta: 'booktabs-cmd', + score: 0.005865460617491447, + }, + { + caption: '\\addlinespace[]', + snippet: '\\addlinespace[$1]', + meta: 'booktabs-cmd', + score: 0.005865460617491447, + }, + { + caption: '\\toprule', + snippet: '\\toprule', + meta: 'booktabs-cmd', + score: 0.059857788139528495, + }, + ], + amsfonts: [ + { + caption: '\\frak{}', + snippet: '\\frak{$1}', + meta: 'amsfonts-cmd', + score: 0.0017966000518546787, + }, + { + caption: '\\checkmark', + snippet: '\\checkmark', + meta: 'amsfonts-cmd', + score: 0.025060530944368123, + }, + { + caption: '\\bold', + snippet: '\\bold', + meta: 'amsfonts-cmd', + score: 0.0014358547624941567, + }, + { + caption: '\\bold{}', + snippet: '\\bold{$1}', + meta: 'amsfonts-cmd', + score: 0.0014358547624941567, + }, + { + caption: '\\Bbb{}', + snippet: '\\Bbb{$1}', + meta: 'amsfonts-cmd', + score: 0.0006671850995492977, + }, + { + caption: '\\Bbb', + snippet: '\\Bbb', + meta: 'amsfonts-cmd', + score: 0.0006671850995492977, + }, + ], + float: [ + { + caption: '\\listof{}{}', + snippet: '\\listof{$1}{$2}', + meta: 'float-cmd', + score: 0.0009837365348002915, + }, + { + caption: '\\floatplacement{}{}', + snippet: '\\floatplacement{$1}{$2}', + meta: 'float-cmd', + score: 0.0005815474978918903, + }, + { + caption: '\\restylefloat{}', + snippet: '\\restylefloat{$1}', + meta: 'float-cmd', + score: 0.0008866338267686714, + }, + { + caption: '\\floatstyle{}', + snippet: '\\floatstyle{$1}', + meta: 'float-cmd', + score: 0.0015470917047414941, + }, + { + caption: '\\floatname{}{}', + snippet: '\\floatname{$1}{$2}', + meta: 'float-cmd', + score: 0.0011934321931750752, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'float-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'float-cmd', + score: 1.2569477427490174, + }, + { + caption: '\\newfloat{}{}{}', + snippet: '\\newfloat{$1}{$2}{$3}', + meta: 'float-cmd', + score: 0.0012745874472536625, + }, + { + caption: '\\newfloat', + snippet: '\\newfloat', + meta: 'float-cmd', + score: 0.0012745874472536625, + }, + { + caption: '\\newfloat{}', + snippet: '\\newfloat{$1}', + meta: 'float-cmd', + score: 0.0012745874472536625, + }, + ], + amsthm: [ + { + caption: '\\swapnumbers', + snippet: '\\swapnumbers', + meta: 'amsthm-cmd', + score: 0.0002908376412221364, + }, + { + caption: '\\qedhere', + snippet: '\\qedhere', + meta: 'amsthm-cmd', + score: 0.0001608548097938035, + }, + { + caption: '\\qed', + snippet: '\\qed', + meta: 'amsthm-cmd', + score: 0.0014240748825867814, + }, + { + caption: '\\qed{}', + snippet: '\\qed{$1}', + meta: 'amsthm-cmd', + score: 0.0014240748825867814, + }, + { + caption: '\\newtheoremstyle{}', + snippet: '\\newtheoremstyle{$1}', + meta: 'amsthm-cmd', + score: 0.004259886909451789, + }, + { + caption: '\\newtheoremstyle{}{}{}', + snippet: '\\newtheoremstyle{$1}{$2}{$3}', + meta: 'amsthm-cmd', + score: 0.004259886909451789, + }, + { + caption: '\\newtheoremstyle{}{}{}{}', + snippet: '\\newtheoremstyle{$1}{$2}{$3}{$4}', + meta: 'amsthm-cmd', + score: 0.004259886909451789, + }, + { + caption: '\\theoremstyle{}', + snippet: '\\theoremstyle{$1}', + meta: 'amsthm-cmd', + score: 0.02533412165007986, + }, + { + caption: '\\proofname', + snippet: '\\proofname', + meta: 'amsthm-cmd', + score: 0.00021208362094925234, + }, + { + caption: '\\pushQED{}', + snippet: '\\pushQED{$1}', + meta: 'amsthm-cmd', + score: 0.00019346981338869148, + }, + { + caption: '\\qedsymbol', + snippet: '\\qedsymbol', + meta: 'amsthm-cmd', + score: 0.0022671784428571723, + }, + { + caption: '\\qedsymbol{}', + snippet: '\\qedsymbol{$1}', + meta: 'amsthm-cmd', + score: 0.0022671784428571723, + }, + { + caption: '\\popQED', + snippet: '\\popQED', + meta: 'amsthm-cmd', + score: 9.673490669434574e-5, + }, + { + caption: '\\newtheorem{}[]{}', + snippet: '\\newtheorem{$1}[$2]{$3}', + meta: 'amsthm-cmd', + score: 0.215689795055434, + }, + { + caption: '\\newtheorem{}{}', + snippet: '\\newtheorem{$1}{$2}', + meta: 'amsthm-cmd', + score: 0.215689795055434, + }, + { + caption: '\\newtheorem{}{}[]', + snippet: '\\newtheorem{$1}{$2}[$3]', + meta: 'amsthm-cmd', + score: 0.215689795055434, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'amsthm-cmd', + score: 0.0063276692758974925, + }, + ], + caption: [ + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'caption-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'caption-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\captionof{}{}', + snippet: '\\captionof{$1}{$2}', + meta: 'caption-cmd', + score: 0.018348594199161503, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'caption-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'caption-cmd', + score: 0.047007158741781095, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'caption-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'caption-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\chapter{}', + snippet: '\\chapter{$1}', + meta: 'caption-cmd', + score: 0.422097569591803, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'caption-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\hspace{}', + snippet: '\\hspace{$1}', + meta: 'caption-cmd', + score: 0.3147206476372336, + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'caption-cmd', + score: 1.2569477427490174, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'caption-cmd', + score: 1.897791904799601, + }, + { + caption: '\\ContinuedFloat', + snippet: '\\ContinuedFloat', + meta: 'caption-cmd', + score: 5.806935368083486e-5, + }, + { + caption: '\\noindent', + snippet: '\\noindent', + meta: 'caption-cmd', + score: 0.42355747798114207, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'caption-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\DeclareCaptionJustification{}{}', + snippet: '\\DeclareCaptionJustification{$1}{$2}', + meta: 'caption-cmd', + score: 0.0001872850414971473, + }, + { + caption: '\\DeclareCaptionLabelSeparator{}{}', + snippet: '\\DeclareCaptionLabelSeparator{$1}{$2}', + meta: 'caption-cmd', + score: 0.0003890810058478364, + }, + { + caption: '\\DeclareCaptionFormat{}{}', + snippet: '\\DeclareCaptionFormat{$1}{$2}', + meta: 'caption-cmd', + score: 0.0004717618449370015, + }, + { + caption: '\\DeclareCaptionFont{}{}', + snippet: '\\DeclareCaptionFont{$1}{$2}', + meta: 'caption-cmd', + score: 5.0133404990680195e-5, + }, + { + caption: '\\DeclareCaptionSubType[]{}', + snippet: '\\DeclareCaptionSubType[$1]{$2}', + meta: 'caption-cmd', + score: 0.0001872850414971473, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'caption-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'caption-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'caption-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'caption-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'caption-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\DeclareCaptionType{}[][]', + snippet: '\\DeclareCaptionType{$1}[$2][$3]', + meta: 'caption-cmd', + score: 0.00015256647321237863, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'caption-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'caption-cmd', + score: 0.2253056071787701, + }, + { + caption: '\\footnotemark[]', + snippet: '\\footnotemark[$1]', + meta: 'caption-cmd', + score: 0.021473212893597875, + }, + { + caption: '\\footnotemark', + snippet: '\\footnotemark', + meta: 'caption-cmd', + score: 0.021473212893597875, + }, + ], + ifthen: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'ifthen-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'ifthen-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'ifthen-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'ifthen-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'ifthen-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'ifthen-cmd', + score: 0.0018957469739775527, + }, + ], + setspace: [ + { + caption: '\\setstretch{}', + snippet: '\\setstretch{$1}', + meta: 'setspace-cmd', + score: 0.019634763572332112, + }, + { + caption: '\\onehalfspacing', + snippet: '\\onehalfspacing', + meta: 'setspace-cmd', + score: 0.010655415521079565, + }, + { + caption: '\\singlespacing', + snippet: '\\singlespacing', + meta: 'setspace-cmd', + score: 0.008351544612280968, + }, + { + caption: '\\doublespacing', + snippet: '\\doublespacing', + meta: 'setspace-cmd', + score: 0.007835428951987135, + }, + { + caption: '\\baselinestretch', + snippet: '\\baselinestretch', + meta: 'setspace-cmd', + score: 0.03225350148161425, + }, + ], + multirow: [ + { + caption: '\\multirow{}{}{}', + snippet: '\\multirow{$1}{$2}{$3}', + meta: 'multirow-cmd', + score: 0.07525389638751734, + }, + { + caption: '\\multirow{}[]{}{}', + snippet: '\\multirow{$1}[$2]{$3}{$4}', + meta: 'multirow-cmd', + score: 0.07525389638751734, + }, + ], + array: [ + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'array-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'array-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'array-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'array-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'array-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'array-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'array-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'array-cmd', + score: 0.018615449342361392, + }, + ], + titlesec: [ + { + caption: '\\titleclass{}{}[]', + snippet: '\\titleclass{$1}{$2}[$3]', + meta: 'titlesec-cmd', + score: 0.00028979763314974667, + }, + { + caption: '\\titlelabel{}', + snippet: '\\titlelabel{$1}', + meta: 'titlesec-cmd', + score: 6.40387839367932e-6, + }, + { + caption: '\\thetitle', + snippet: '\\thetitle', + meta: 'titlesec-cmd', + score: 0.0015531478302713473, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'titlesec-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'titlesec-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\titleformat{}{}{}{}{}[]', + snippet: '\\titleformat{$1}{$2}{$3}{$4}{$5}[$6]', + meta: 'titlesec-cmd', + score: 0.03475519439740096, + }, + { + caption: '\\titleformat{}[]{}{}{}{}', + snippet: '\\titleformat{$1}[$2]{$3}{$4}{$5}{$6}', + meta: 'titlesec-cmd', + score: 0.03475519439740096, + }, + { + caption: '\\titleformat{}{}', + snippet: '\\titleformat{$1}{$2}', + meta: 'titlesec-cmd', + score: 0.03475519439740096, + }, + { + caption: '\\titleformat{}{}{}{}{}', + snippet: '\\titleformat{$1}{$2}{$3}{$4}{$5}', + meta: 'titlesec-cmd', + score: 0.03475519439740096, + }, + { + caption: '\\titlespacing{}{}{}{}', + snippet: '\\titlespacing{$1}{$2}{$3}{$4}', + meta: 'titlesec-cmd', + score: 0.023062744385192156, + }, + { + caption: '\\markboth{}{}', + snippet: '\\markboth{$1}{$2}', + meta: 'titlesec-cmd', + score: 0.038323601301945065, + }, + { + caption: '\\markboth{}', + snippet: '\\markboth{$1}', + meta: 'titlesec-cmd', + score: 0.038323601301945065, + }, + { + caption: '\\markright{}', + snippet: '\\markright{$1}', + meta: 'titlesec-cmd', + score: 0.007138622674767024, + }, + { + caption: '\\markright{}{}', + snippet: '\\markright{$1}{$2}', + meta: 'titlesec-cmd', + score: 0.007138622674767024, + }, + { + caption: '\\filleft', + snippet: '\\filleft', + meta: 'titlesec-cmd', + score: 7.959989906732799e-5, + }, + { + caption: '\\filcenter', + snippet: '\\filcenter', + meta: 'titlesec-cmd', + score: 0.0004835660211260246, + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'titlesec-cmd', + score: 0.2253056071787701, + }, + { + caption: '\\cleardoublepage', + snippet: '\\cleardoublepage', + meta: 'titlesec-cmd', + score: 0.044016804142963585, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'titlesec-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\chaptertitlename', + snippet: '\\chaptertitlename', + meta: 'titlesec-cmd', + score: 0.0016985007766926272, + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'titlesec-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\filright', + snippet: '\\filright', + meta: 'titlesec-cmd', + score: 7.959989906732799e-5, + }, + { + caption: '\\titlerule', + snippet: '\\titlerule', + meta: 'titlesec-cmd', + score: 0.019273712561461216, + }, + { + caption: '\\titlerule[]{}', + snippet: '\\titlerule[$1]{$2}', + meta: 'titlesec-cmd', + score: 0.019273712561461216, + }, + ], + multicol: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'multicol-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'multicol-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\raggedcolumns', + snippet: '\\raggedcolumns', + meta: 'multicol-cmd', + score: 0.00027461965178228156, + }, + { + caption: '\\columnbreak', + snippet: '\\columnbreak', + meta: 'multicol-cmd', + score: 0.002609610141555795, + }, + { + caption: '\\columnseprulecolor{}', + snippet: '\\columnseprulecolor{$1}', + meta: 'multicol-cmd', + score: 1.3314892207625771e-5, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'multicol-cmd', + score: 0.1789117552185788, + }, + ], + listings: [ + { + caption: '\\vskip', + snippet: '\\vskip', + meta: 'listings-cmd', + score: 0.05143052892347224, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'listings-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'listings-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'listings-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\thelstlisting', + snippet: '\\thelstlisting', + meta: 'listings-cmd', + score: 0.00012774128088872144, + }, + { + caption: '\\lstinputlisting[]{}', + snippet: '\\lstinputlisting[$1]{$2}', + meta: 'listings-cmd', + score: 0.011660477607086044, + }, + { + caption: '\\lstinputlisting{}', + snippet: '\\lstinputlisting{$1}', + meta: 'listings-cmd', + score: 0.011660477607086044, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'listings-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'listings-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\lstinline', + snippet: '\\lstinline', + meta: 'listings-cmd', + score: 0.005972262850694285, + }, + { + caption: '\\lstinline{}', + snippet: '\\lstinline{$1}', + meta: 'listings-cmd', + score: 0.005972262850694285, + }, + { + caption: '\\lstlistoflistings', + snippet: '\\lstlistoflistings', + meta: 'listings-cmd', + score: 0.005279080363360602, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'listings-cmd', + score: 0.00037306820619479756, + }, + ], + blindtext: [ + { + caption: '\\glqq', + snippet: '\\glqq', + meta: 'blindtext-cmd', + score: 0.0039133256714254504, + }, + { + caption: '\\glqq{}', + snippet: '\\glqq{$1}', + meta: 'blindtext-cmd', + score: 0.0039133256714254504, + }, + { + caption: '\\blindtext', + snippet: '\\blindtext', + meta: 'blindtext-cmd', + score: 0.05782040856823667, + }, + { + caption: '\\blindtext[]', + snippet: '\\blindtext[$1]', + meta: 'blindtext-cmd', + score: 0.05782040856823667, + }, + { + caption: '\\Blindtext', + snippet: '\\Blindtext', + meta: 'blindtext-cmd', + score: 0.006384906903938044, + }, + { + caption: '\\grqq', + snippet: '\\grqq', + meta: 'blindtext-cmd', + score: 0.006659522189248266, + }, + { + caption: '\\grqq{}', + snippet: '\\grqq{$1}', + meta: 'blindtext-cmd', + score: 0.006659522189248266, + }, + { + caption: '\\blinddocument', + snippet: '\\blinddocument', + meta: 'blindtext-cmd', + score: 0.00011480988129172825, + }, + { + caption: '\\xspace', + snippet: '\\xspace', + meta: 'blindtext-cmd', + score: 0.07560370351316588, + }, + ], + enumitem: [ + { + caption: '\\newlist{}{}{}', + snippet: '\\newlist{$1}{$2}{$3}', + meta: 'enumitem-cmd', + score: 0.0007266225924074459, + }, + { + caption: '\\setlist[]{}', + snippet: '\\setlist[$1]{$2}', + meta: 'enumitem-cmd', + score: 0.010895384475728338, + }, + { + caption: '\\setlist{}', + snippet: '\\setlist{$1}', + meta: 'enumitem-cmd', + score: 0.010895384475728338, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'enumitem-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'enumitem-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\setlistdepth{}', + snippet: '\\setlistdepth{$1}', + meta: 'enumitem-cmd', + score: 0.0001113322912630871, + }, + { + caption: '\\setenumerate[]{}', + snippet: '\\setenumerate[$1]{$2}', + meta: 'enumitem-cmd', + score: 7.437178301071255e-5, + }, + { + caption: '\\setenumerate{}', + snippet: '\\setenumerate{$1}', + meta: 'enumitem-cmd', + score: 7.437178301071255e-5, + }, + { + caption: '\\renewlist{}{}{}', + snippet: '\\renewlist{$1}{$2}{$3}', + meta: 'enumitem-cmd', + score: 0.0001113322912630871, + }, + { + caption: '\\descriptionlabel{}', + snippet: '\\descriptionlabel{$1}', + meta: 'enumitem-cmd', + score: 7.678089052626698e-6, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'enumitem-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\setitemize[]{}', + snippet: '\\setitemize[$1]{$2}', + meta: 'enumitem-cmd', + score: 0.0019580640711971786, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'enumitem-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'enumitem-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\makelabel', + snippet: '\\makelabel', + meta: 'enumitem-cmd', + score: 5.739925426740175e-5, + }, + { + caption: '\\makelabel{}', + snippet: '\\makelabel{$1}', + meta: 'enumitem-cmd', + score: 5.739925426740175e-5, + }, + { + caption: '\\makelabel[]{}', + snippet: '\\makelabel[$1]{$2}', + meta: 'enumitem-cmd', + score: 5.739925426740175e-5, + }, + ], + times: [ + { + caption: '\\rmdefault', + snippet: '\\rmdefault', + meta: 'times-cmd', + score: 0.0012870877747432935, + }, + { + caption: '\\sfdefault', + snippet: '\\sfdefault', + meta: 'times-cmd', + score: 0.008427383388519996, + }, + { + caption: '\\sfdefault{}', + snippet: '\\sfdefault{$1}', + meta: 'times-cmd', + score: 0.008427383388519996, + }, + { + caption: '\\ttdefault', + snippet: '\\ttdefault', + meta: 'times-cmd', + score: 0.0011733254149332488, + }, + { + caption: '\\ttdefault{}', + snippet: '\\ttdefault{$1}', + meta: 'times-cmd', + score: 0.0011733254149332488, + }, + ], + subcaption: [ + { + caption: '\\subref{}', + snippet: '\\subref{$1}', + meta: 'subcaption-cmd', + score: 0.007192033516871399, + }, + { + caption: '\\subcaptionbox{}{}', + snippet: '\\subcaptionbox{$1}{$2}', + meta: 'subcaption-cmd', + score: 0.0008634329663023698, + }, + { + caption: '\\newsubfloat{}', + snippet: '\\newsubfloat{$1}', + meta: 'subcaption-cmd', + score: 0.000615805121082521, + }, + { + caption: '\\subcaption{}', + snippet: '\\subcaption{$1}', + meta: 'subcaption-cmd', + score: 0.006820005741581297, + }, + { + caption: '\\subcaption[]{}', + snippet: '\\subcaption[$1]{$2}', + meta: 'subcaption-cmd', + score: 0.006820005741581297, + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'subcaption-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'subcaption-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\captionof{}{}', + snippet: '\\captionof{$1}{$2}', + meta: 'subcaption-cmd', + score: 0.018348594199161503, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'subcaption-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'subcaption-cmd', + score: 0.047007158741781095, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'subcaption-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'subcaption-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\chapter{}', + snippet: '\\chapter{$1}', + meta: 'subcaption-cmd', + score: 0.422097569591803, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'subcaption-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\hspace{}', + snippet: '\\hspace{$1}', + meta: 'subcaption-cmd', + score: 0.3147206476372336, + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'subcaption-cmd', + score: 1.2569477427490174, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'subcaption-cmd', + score: 1.897791904799601, + }, + { + caption: '\\ContinuedFloat', + snippet: '\\ContinuedFloat', + meta: 'subcaption-cmd', + score: 5.806935368083486e-5, + }, + { + caption: '\\noindent', + snippet: '\\noindent', + meta: 'subcaption-cmd', + score: 0.42355747798114207, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'subcaption-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\DeclareCaptionJustification{}{}', + snippet: '\\DeclareCaptionJustification{$1}{$2}', + meta: 'subcaption-cmd', + score: 0.0001872850414971473, + }, + { + caption: '\\DeclareCaptionLabelSeparator{}{}', + snippet: '\\DeclareCaptionLabelSeparator{$1}{$2}', + meta: 'subcaption-cmd', + score: 0.0003890810058478364, + }, + { + caption: '\\DeclareCaptionFormat{}{}', + snippet: '\\DeclareCaptionFormat{$1}{$2}', + meta: 'subcaption-cmd', + score: 0.0004717618449370015, + }, + { + caption: '\\DeclareCaptionFont{}{}', + snippet: '\\DeclareCaptionFont{$1}{$2}', + meta: 'subcaption-cmd', + score: 5.0133404990680195e-5, + }, + { + caption: '\\DeclareCaptionSubType[]{}', + snippet: '\\DeclareCaptionSubType[$1]{$2}', + meta: 'subcaption-cmd', + score: 0.0001872850414971473, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'subcaption-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'subcaption-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'subcaption-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'subcaption-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'subcaption-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\DeclareCaptionType{}[][]', + snippet: '\\DeclareCaptionType{$1}[$2][$3]', + meta: 'subcaption-cmd', + score: 0.00015256647321237863, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'subcaption-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'subcaption-cmd', + score: 0.2253056071787701, + }, + { + caption: '\\footnotemark[]', + snippet: '\\footnotemark[$1]', + meta: 'subcaption-cmd', + score: 0.021473212893597875, + }, + { + caption: '\\footnotemark', + snippet: '\\footnotemark', + meta: 'subcaption-cmd', + score: 0.021473212893597875, + }, + ], + bm: [ + { + caption: '\\bm{}', + snippet: '\\bm{$1}', + meta: 'bm-cmd', + score: 0.14733018077819282, + }, + { + caption: '\\bm', + snippet: '\\bm', + meta: 'bm-cmd', + score: 0.14733018077819282, + }, + ], + fontspec: [ + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'fontspec-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'fontspec-cmd', + score: 0.2864294797053033, + }, + ], + subfigure: [ + { + caption: '\\subref{}', + snippet: '\\subref{$1}', + meta: 'subfigure-cmd', + score: 0.007192033516871399, + }, + { + caption: '\\subfigure[]{}', + snippet: '\\subfigure[$1]{$2}', + meta: 'subfigure-cmd', + score: 0.037856842641104005, + }, + ], + calc: [ + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'calc-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'calc-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'calc-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'calc-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'calc-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'calc-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'calc-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'calc-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'calc-cmd', + score: 0.028955796305270766, + }, + ], + tabularx: [ + { + caption: '\\let', + snippet: '\\let', + meta: 'tabularx-cmd', + score: 0.03789745970461662, + }, + { + caption: '\\write', + snippet: '\\write', + meta: 'tabularx-cmd', + score: 0.0008038857295393196, + }, + { + caption: '\\tabularxcolumn[]{}', + snippet: '\\tabularxcolumn[$1]{$2}', + meta: 'tabularx-cmd', + score: 0.00048507499766588637, + }, + { + caption: '\\tabularxcolumn', + snippet: '\\tabularxcolumn', + meta: 'tabularx-cmd', + score: 0.00048507499766588637, + }, + { + caption: '\\tabularx{}{}', + snippet: '\\tabularx{$1}{$2}', + meta: 'tabularx-cmd', + score: 0.0005861357565780464, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'tabularx-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'tabularx-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'tabularx-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'tabularx-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'tabularx-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'tabularx-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tabularx-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'tabularx-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'tabularx-cmd', + score: 0.018615449342361392, + }, + ], + algorithm: [ + { + caption: '\\listalgorithmname', + snippet: '\\listalgorithmname', + meta: 'algorithm-cmd', + score: 0.00022490402516652368, + }, + { + caption: '\\listofalgorithms', + snippet: '\\listofalgorithms', + meta: 'algorithm-cmd', + score: 0.0012576983422794912, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'algorithm-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'algorithm-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'algorithm-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'algorithm-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'algorithm-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'algorithm-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\listof{}{}', + snippet: '\\listof{$1}{$2}', + meta: 'algorithm-cmd', + score: 0.0009837365348002915, + }, + { + caption: '\\floatplacement{}{}', + snippet: '\\floatplacement{$1}{$2}', + meta: 'algorithm-cmd', + score: 0.0005815474978918903, + }, + { + caption: '\\restylefloat{}', + snippet: '\\restylefloat{$1}', + meta: 'algorithm-cmd', + score: 0.0008866338267686714, + }, + { + caption: '\\floatstyle{}', + snippet: '\\floatstyle{$1}', + meta: 'algorithm-cmd', + score: 0.0015470917047414941, + }, + { + caption: '\\floatname{}{}', + snippet: '\\floatname{$1}{$2}', + meta: 'algorithm-cmd', + score: 0.0011934321931750752, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'algorithm-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'algorithm-cmd', + score: 1.2569477427490174, + }, + { + caption: '\\newfloat{}{}{}', + snippet: '\\newfloat{$1}{$2}{$3}', + meta: 'algorithm-cmd', + score: 0.0012745874472536625, + }, + { + caption: '\\newfloat', + snippet: '\\newfloat', + meta: 'algorithm-cmd', + score: 0.0012745874472536625, + }, + { + caption: '\\newfloat{}', + snippet: '\\newfloat{$1}', + meta: 'algorithm-cmd', + score: 0.0012745874472536625, + }, + ], + biblatex: [ + { + caption: '\\textcite{}', + snippet: '\\textcite{$1}', + meta: 'biblatex-cmd', + score: 0.0071363824748767206, + }, + { + caption: '\\iffieldundef{}{}{}', + snippet: '\\iffieldundef{$1}{$2}{$3}', + meta: 'biblatex-cmd', + score: 4.841482597532878e-5, + }, + { + caption: '\\list{}{}', + snippet: '\\list{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.00046570666700199663, + }, + { + caption: '\\list{}', + snippet: '\\list{$1}', + meta: 'biblatex-cmd', + score: 0.00046570666700199663, + }, + { + caption: '\\list', + snippet: '\\list', + meta: 'biblatex-cmd', + score: 0.00046570666700199663, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'biblatex-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'biblatex-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\printbibliography', + snippet: '\\printbibliography', + meta: 'biblatex-cmd', + score: 0.028923378512954446, + }, + { + caption: '\\printbibliography[]', + snippet: '\\printbibliography[$1]', + meta: 'biblatex-cmd', + score: 0.028923378512954446, + }, + { + caption: '\\keyword{}', + snippet: '\\keyword{$1}', + meta: 'biblatex-cmd', + score: 0.0056978719547823445, + }, + { + caption: '\\nocite{}', + snippet: '\\nocite{$1}', + meta: 'biblatex-cmd', + score: 0.04990693820960752, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'biblatex-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\mkbibquote{}', + snippet: '\\mkbibquote{$1}', + meta: 'biblatex-cmd', + score: 4.841482597532878e-5, + }, + { + caption: '\\addabbrvspace', + snippet: '\\addabbrvspace', + meta: 'biblatex-cmd', + score: 4.841482597532878e-5, + }, + { + caption: '\\AtEveryBibitem{}', + snippet: '\\AtEveryBibitem{$1}', + meta: 'biblatex-cmd', + score: 0.0006862523808353773, + }, + { + caption: '\\mkbibemph{}', + snippet: '\\mkbibemph{$1}', + meta: 'biblatex-cmd', + score: 4.841482597532878e-5, + }, + { + caption: '\\DeclareFieldFormat{}{}', + snippet: '\\DeclareFieldFormat{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.00028207109055618685, + }, + { + caption: '\\bibliography{}', + snippet: '\\bibliography{$1}', + meta: 'biblatex-cmd', + score: 0.2659628337907604, + }, + { + caption: '\\enquote{}', + snippet: '\\enquote{$1}', + meta: 'biblatex-cmd', + score: 0.0077432730806830915, + }, + { + caption: '\\bibopenbracket', + snippet: '\\bibopenbracket', + meta: 'biblatex-cmd', + score: 0.0005125772067631753, + }, + { + caption: '\\newbibmacro{}[]{}', + snippet: '\\newbibmacro{$1}[$2]{$3}', + meta: 'biblatex-cmd', + score: 4.841482597532878e-5, + }, + { + caption: '\\addbibresource{}', + snippet: '\\addbibresource{$1}', + meta: 'biblatex-cmd', + score: 0.033545778388159704, + }, + { + caption: '\\defbibheading{}{}', + snippet: '\\defbibheading{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.00013423526504458629, + }, + { + caption: '\\DeclareNameAlias{}{}', + snippet: '\\DeclareNameAlias{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.0003596306478652252, + }, + { + caption: '\\bibcloseparen', + snippet: '\\bibcloseparen', + meta: 'biblatex-cmd', + score: 0.0005125772067631753, + }, + { + caption: '\\renewbibmacro{}{}', + snippet: '\\renewbibmacro{$1}{$2}', + meta: 'biblatex-cmd', + score: 9.70299207241043e-5, + }, + { + caption: '\\bibclosebracket', + snippet: '\\bibclosebracket', + meta: 'biblatex-cmd', + score: 0.0005125772067631753, + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'biblatex-cmd', + score: 3.800886892251021, + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'biblatex-cmd', + score: 3.800886892251021, + }, + { + caption: '\\parentext', + snippet: '\\parentext', + meta: 'biblatex-cmd', + score: 0.0005125772067631753, + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'biblatex-cmd', + score: 2.341195220791228, + }, + { + caption: '\\addspace', + snippet: '\\addspace', + meta: 'biblatex-cmd', + score: 0.0002657609533376918, + }, + { + caption: '\\ifentrytype{}{}{}', + snippet: '\\ifentrytype{$1}{$2}{$3}', + meta: 'biblatex-cmd', + score: 8.342875497183237e-5, + }, + { + caption: '\\addslash', + snippet: '\\addslash', + meta: 'biblatex-cmd', + score: 0.0002657609533376918, + }, + { + caption: '\\DefineBibliographyStrings{}{}', + snippet: '\\DefineBibliographyStrings{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.001537977148659816, + }, + { + caption: '\\section{}', + snippet: '\\section{$1}', + meta: 'biblatex-cmd', + score: 3.0952612541683835, + }, + { + caption: '\\newblockpunct', + snippet: '\\newblockpunct', + meta: 'biblatex-cmd', + score: 0.0001328804766688459, + }, + { + caption: '\\defbibfilter{}{}', + snippet: '\\defbibfilter{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.0005203319717980072, + }, + { + caption: '\\parencite{}', + snippet: '\\parencite{$1}', + meta: 'biblatex-cmd', + score: 0.0447747090014577, + }, + { + caption: '\\parencite[]{}', + snippet: '\\parencite[$1]{$2}', + meta: 'biblatex-cmd', + score: 0.0447747090014577, + }, + { + caption: '\\midsentence', + snippet: '\\midsentence', + meta: 'biblatex-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\nolinkurl{}', + snippet: '\\nolinkurl{$1}', + meta: 'biblatex-cmd', + score: 0.0004995635515943437, + }, + { + caption: '\\DeclareSourcemap{}', + snippet: '\\DeclareSourcemap{$1}', + meta: 'biblatex-cmd', + score: 0.0005203319717980072, + }, + { + caption: '\\AtBeginBibliography{}', + snippet: '\\AtBeginBibliography{$1}', + meta: 'biblatex-cmd', + score: 0.0004668773504581073, + }, + { + caption: '\\AtEveryCite{}', + snippet: '\\AtEveryCite{$1}', + meta: 'biblatex-cmd', + score: 0.0005125772067631753, + }, + { + caption: '\\DeclareLanguageMapping{}{}', + snippet: '\\DeclareLanguageMapping{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.000703956971675325, + }, + { + caption: '\\addtocategory{}{}', + snippet: '\\addtocategory{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.008238589553468446, + }, + { + caption: '\\DeclareBibliographyCategory{}', + snippet: '\\DeclareBibliographyCategory{$1}', + meta: 'biblatex-cmd', + score: 0.0010298236941835557, + }, + { + caption: '\\break', + snippet: '\\break', + meta: 'biblatex-cmd', + score: 0.016352452390960115, + }, + { + caption: '\\break{}', + snippet: '\\break{$1}', + meta: 'biblatex-cmd', + score: 0.016352452390960115, + }, + { + caption: '\\break{}{}', + snippet: '\\break{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.016352452390960115, + }, + { + caption: '\\bibopenparen', + snippet: '\\bibopenparen', + meta: 'biblatex-cmd', + score: 0.0005125772067631753, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'biblatex-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\name{}{}', + snippet: '\\name{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.1236289144754329, + }, + { + caption: '\\name', + snippet: '\\name', + meta: 'biblatex-cmd', + score: 0.1236289144754329, + }, + { + caption: '\\name{}', + snippet: '\\name{$1}', + meta: 'biblatex-cmd', + score: 0.1236289144754329, + }, + { + caption: '\\ExecuteBibliographyOptions{}', + snippet: '\\ExecuteBibliographyOptions{$1}', + meta: 'biblatex-cmd', + score: 4.841482597532878e-5, + }, + { + caption: '\\usebibmacro{}{}', + snippet: '\\usebibmacro{$1}{$2}', + meta: 'biblatex-cmd', + score: 9.682965195065755e-5, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'biblatex-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'biblatex-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'biblatex-cmd', + score: 0.0002854206807593436, + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'biblatex-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'biblatex-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'biblatex-cmd', + score: 0.010515056688180681, + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'biblatex-cmd', + score: 0.008041789461944983, + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'biblatex-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'biblatex-cmd', + score: 0.0032990580087398644, + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'biblatex-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'biblatex-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'biblatex-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'biblatex-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'biblatex-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'biblatex-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'biblatex-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'biblatex-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'biblatex-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'biblatex-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'biblatex-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'biblatex-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'biblatex-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'biblatex-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'biblatex-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'biblatex-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'biblatex-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'biblatex-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'biblatex-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'biblatex-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'biblatex-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'biblatex-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'biblatex-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'biblatex-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'biblatex-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'biblatex-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'biblatex-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'biblatex-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'biblatex-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'biblatex-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'biblatex-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'biblatex-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'biblatex-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'biblatex-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'biblatex-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'biblatex-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'biblatex-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'biblatex-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'biblatex-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'biblatex-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'biblatex-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'biblatex-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'biblatex-cmd', + score: 0.008565354665444157, + }, + ], + microtype: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'microtype-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'microtype-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'microtype-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\lsstyle', + snippet: '\\lsstyle', + meta: 'microtype-cmd', + score: 0.0023367519914345774, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'microtype-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\DisableLigatures[]{}', + snippet: '\\DisableLigatures[$1]{$2}', + meta: 'microtype-cmd', + score: 0.0009805246614299932, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'microtype-cmd', + score: 0.00037306820619479756, + }, + ], + etoolbox: [ + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'etoolbox-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'etoolbox-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'etoolbox-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'etoolbox-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'etoolbox-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'etoolbox-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'etoolbox-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'etoolbox-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'etoolbox-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'etoolbox-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'etoolbox-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'etoolbox-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'etoolbox-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'etoolbox-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'etoolbox-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'etoolbox-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'etoolbox-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'etoolbox-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'etoolbox-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'etoolbox-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'etoolbox-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'etoolbox-cmd', + score: 0.008565354665444157, + }, + ], + longtable: [ + { + caption: '\\endhead', + snippet: '\\endhead', + meta: 'longtable-cmd', + score: 0.0023853501147448834, + }, + { + caption: '\\endfoot', + snippet: '\\endfoot', + meta: 'longtable-cmd', + score: 0.00044045261916551967, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'longtable-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'longtable-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\nopagebreak', + snippet: '\\nopagebreak', + meta: 'longtable-cmd', + score: 9.952664522415981e-5, + }, + { + caption: '\\endfirsthead', + snippet: '\\endfirsthead', + meta: 'longtable-cmd', + score: 0.0016148498709822416, + }, + { + caption: '\\endlastfoot', + snippet: '\\endlastfoot', + meta: 'longtable-cmd', + score: 0.00044045261916551967, + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'longtable-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\tablename', + snippet: '\\tablename', + meta: 'longtable-cmd', + score: 0.0029238994233674776, + }, + { + caption: '\\pagebreak', + snippet: '\\pagebreak', + meta: 'longtable-cmd', + score: 0.0313525090421608, + }, + ], + mathtools: [ + { + caption: '\\xleftrightarrow[][]{}', + snippet: '\\xleftrightarrow[$1][$2]{$3}', + meta: 'mathtools-cmd', + score: 4.015559489911509e-5, + }, + { + caption: '\\vcentcolon', + snippet: '\\vcentcolon', + meta: 'mathtools-cmd', + score: 0.00021361943526711615, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'mathtools-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\coloneqq', + snippet: '\\coloneqq', + meta: 'mathtools-cmd', + score: 0.0014407293323958122, + }, + { + caption: '\\mathclap{}', + snippet: '\\mathclap{$1}', + meta: 'mathtools-cmd', + score: 7.84378567451772e-5, + }, + { + caption: '\\adjustlimits', + snippet: '\\adjustlimits', + meta: 'mathtools-cmd', + score: 0.0005307066890271085, + }, + { + caption: '\\MoveEqLeft', + snippet: '\\MoveEqLeft', + meta: 'mathtools-cmd', + score: 5.343949980628182e-5, + }, + { + caption: '\\mathrlap{}', + snippet: '\\mathrlap{$1}', + meta: 'mathtools-cmd', + score: 0.0003112817211637952, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'mathtools-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\xhookrightarrow{}', + snippet: '\\xhookrightarrow{$1}', + meta: 'mathtools-cmd', + score: 5.444260823474129e-5, + }, + { + caption: '\\DeclarePairedDelimiter{}{}{}', + snippet: '\\DeclarePairedDelimiter{$1}{$2}{$3}', + meta: 'mathtools-cmd', + score: 0.0033916678416372487, + }, + { + caption: '\\DeclarePairedDelimiter', + snippet: '\\DeclarePairedDelimiter', + meta: 'mathtools-cmd', + score: 0.0033916678416372487, + }, + { + caption: '\\prescript{}{}{}', + snippet: '\\prescript{$1}{$2}{$3}', + meta: 'mathtools-cmd', + score: 8.833369785705982e-6, + }, + { + caption: '\\underbrace{}', + snippet: '\\underbrace{$1}', + meta: 'mathtools-cmd', + score: 0.010373780436850907, + }, + { + caption: '\\mathllap{}', + snippet: '\\mathllap{$1}', + meta: 'mathtools-cmd', + score: 3.140504277052775e-5, + }, + { + caption: '\\overbrace{}', + snippet: '\\overbrace{$1}', + meta: 'mathtools-cmd', + score: 0.0006045704778718376, + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'mathtools-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'mathtools-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'mathtools-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'mathtools-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mathtools-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mathtools-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'mathtools-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'mathtools-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mathtools-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mathtools-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'mathtools-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'mathtools-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'mathtools-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'mathtools-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'mathtools-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'mathtools-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'mathtools-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'mathtools-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'mathtools-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'mathtools-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'mathtools-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'mathtools-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'mathtools-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'mathtools-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'mathtools-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'mathtools-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'mathtools-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'mathtools-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'mathtools-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'mathtools-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'mathtools-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'mathtools-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'mathtools-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'mathtools-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'mathtools-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'mathtools-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'mathtools-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'mathtools-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'mathtools-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'mathtools-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'mathtools-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'mathtools-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'mathtools-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'mathtools-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'mathtools-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'mathtools-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'mathtools-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'mathtools-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'mathtools-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'mathtools-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'mathtools-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'mathtools-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'mathtools-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'mathtools-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'mathtools-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'mathtools-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'mathtools-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'mathtools-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'mathtools-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'mathtools-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'mathtools-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'mathtools-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'mathtools-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'mathtools-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'mathtools-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'mathtools-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'mathtools-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'mathtools-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'mathtools-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'mathtools-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'mathtools-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'mathtools-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'mathtools-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'mathtools-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'mathtools-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'mathtools-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'mathtools-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'mathtools-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'mathtools-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'mathtools-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'mathtools-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'mathtools-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'mathtools-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'mathtools-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'mathtools-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'mathtools-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'mathtools-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'mathtools-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'mathtools-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'mathtools-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'mathtools-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'mathtools-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'mathtools-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'mathtools-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'mathtools-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'mathtools-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'mathtools-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'mathtools-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'mathtools-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'mathtools-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'mathtools-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'mathtools-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'mathtools-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'mathtools-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'mathtools-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'mathtools-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'mathtools-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'mathtools-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'mathtools-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'mathtools-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'mathtools-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'mathtools-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'mathtools-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'mathtools-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'mathtools-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'mathtools-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'mathtools-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'mathtools-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'mathtools-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'mathtools-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'mathtools-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'mathtools-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'mathtools-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'mathtools-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'mathtools-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'mathtools-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'mathtools-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'mathtools-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'mathtools-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'mathtools-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'mathtools-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'mathtools-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'mathtools-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'mathtools-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'mathtools-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'mathtools-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'mathtools-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'mathtools-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'mathtools-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'mathtools-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'mathtools-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'mathtools-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'mathtools-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'mathtools-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'mathtools-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'mathtools-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'mathtools-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mathtools-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'mathtools-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'mathtools-cmd', + score: 0.0063276692758974925, + }, + ], + verbatim: [ + { + caption: '\\endverbatim', + snippet: '\\endverbatim', + meta: 'verbatim-cmd', + score: 0.0022216421267780076, + }, + { + caption: '\\verbatim', + snippet: '\\verbatim', + meta: 'verbatim-cmd', + score: 0.0072203369120285256, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'verbatim-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'verbatim-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\par', + snippet: '\\par', + meta: 'verbatim-cmd', + score: 0.413853376001159, + }, + { + caption: '\\verbatiminput{}', + snippet: '\\verbatiminput{$1}', + meta: 'verbatim-cmd', + score: 0.0024547099784948665, + }, + { + caption: '\\verbatiminput', + snippet: '\\verbatiminput', + meta: 'verbatim-cmd', + score: 0.0024547099784948665, + }, + ], + wrapfig: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'wrapfig-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\par', + snippet: '\\par', + meta: 'wrapfig-cmd', + score: 0.413853376001159, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'wrapfig-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'wrapfig-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\wrapfigure{}{}', + snippet: '\\wrapfigure{$1}{$2}', + meta: 'wrapfig-cmd', + score: 0.0003295435821387379, + }, + ], + epsfig: [ + { + caption: '\\epsfbox{}', + snippet: '\\epsfbox{$1}', + meta: 'epsfig-cmd', + score: 0.00013712781345832882, + }, + { + caption: '\\psfig{}', + snippet: '\\psfig{$1}', + meta: 'epsfig-cmd', + score: 0.0017552046452897515, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'epsfig-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'epsfig-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'epsfig-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'epsfig-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'epsfig-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'epsfig-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'epsfig-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'epsfig-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'epsfig-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epsfig-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'epsfig-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'epsfig-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'epsfig-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'epsfig-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'epsfig-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'epsfig-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'epsfig-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'epsfig-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'epsfig-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epsfig-cmd', + score: 0.008565354665444157, + }, + ], + cite: [ + { + caption: '\\citeonline{}', + snippet: '\\citeonline{$1}', + meta: 'cite-cmd', + score: 0.014277840409455324, + }, + { + caption: '\\citenum{}', + snippet: '\\citenum{$1}', + meta: 'cite-cmd', + score: 0.0027420903627423383, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'cite-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'cite-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\nocite{}', + snippet: '\\nocite{$1}', + meta: 'cite-cmd', + score: 0.04990693820960752, + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'cite-cmd', + score: 2.341195220791228, + }, + ], + lipsum: [ + { + caption: '\\setlipsumdefault{}', + snippet: '\\setlipsumdefault{$1}', + meta: 'lipsum-cmd', + score: 0.00024112945034541791, + }, + { + caption: '\\lipsum[]', + snippet: '\\lipsum[$1]', + meta: 'lipsum-cmd', + score: 0.0300787181624191, + }, + ], + algpseudocode: [ + { + caption: '\\algrenewcommand', + snippet: '\\algrenewcommand', + meta: 'algpseudocode-cmd', + score: 0.0019861803661869416, + }, + { + caption: '\\Statex', + snippet: '\\Statex', + meta: 'algpseudocode-cmd', + score: 0.008622777195102994, + }, + { + caption: '\\BState{}', + snippet: '\\BState{$1}', + meta: 'algpseudocode-cmd', + score: 0.0008685861525307122, + }, + { + caption: '\\BState', + snippet: '\\BState', + meta: 'algpseudocode-cmd', + score: 0.0008685861525307122, + }, + { + caption: '\\algloopdefx{}[][]{}', + snippet: '\\algloopdefx{$1}[$2][$3]{$4}', + meta: 'algpseudocode-cmd', + score: 0.00025315185701145097, + }, + { + caption: '\\algnewcommand', + snippet: '\\algnewcommand', + meta: 'algpseudocode-cmd', + score: 0.0030209395012065327, + }, + { + caption: '\\algnewcommand{}[]{}', + snippet: '\\algnewcommand{$1}[$2]{$3}', + meta: 'algpseudocode-cmd', + score: 0.0030209395012065327, + }, + { + caption: '\\Comment{}', + snippet: '\\Comment{$1}', + meta: 'algpseudocode-cmd', + score: 0.005178604573219454, + }, + { + caption: '\\algblockdefx{}{}[]', + snippet: '\\algblockdefx{$1}{$2}[$3]', + meta: 'algpseudocode-cmd', + score: 0.00025315185701145097, + }, + { + caption: '\\algrenewtext{}{}', + snippet: '\\algrenewtext{$1}{$2}', + meta: 'algpseudocode-cmd', + score: 0.0024415580558825975, + }, + { + caption: '\\algrenewtext{}[]{}', + snippet: '\\algrenewtext{$1}[$2]{$3}', + meta: 'algpseudocode-cmd', + score: 0.0024415580558825975, + }, + { + caption: '\\algblock{}{}', + snippet: '\\algblock{$1}{$2}', + meta: 'algpseudocode-cmd', + score: 0.0007916858220314837, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'algpseudocode-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\algdef{}[]{}{}{}{}', + snippet: '\\algdef{$1}[$2]{$3}{$4}{$5}{$6}', + meta: 'algpseudocode-cmd', + score: 0.0003102486920966127, + }, + { + caption: '\\algdef{}[]{}{}[]{}{}', + snippet: '\\algdef{$1}[$2]{$3}{$4}[$5]{$6}{$7}', + meta: 'algpseudocode-cmd', + score: 0.0003102486920966127, + }, + { + caption: '\\algdef{}[]{}[]{}', + snippet: '\\algdef{$1}[$2]{$3}[$4]{$5}', + meta: 'algpseudocode-cmd', + score: 0.0003102486920966127, + }, + { + caption: '\\algtext{}', + snippet: '\\algtext{$1}', + meta: 'algpseudocode-cmd', + score: 0.0005463612015579842, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'algpseudocode-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'algpseudocode-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'algpseudocode-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'algpseudocode-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'algpseudocode-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'algpseudocode-cmd', + score: 0.0018957469739775527, + }, + ], + textpos: [ + { + caption: '\\textblockorigin{}{}', + snippet: '\\textblockorigin{$1}{$2}', + meta: 'textpos-cmd', + score: 0.016306266556901577, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'textpos-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'textpos-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'textpos-cmd', + score: 0.00037306820619479756, + }, + ], + subfig: [ + { + caption: '\\subref{}', + snippet: '\\subref{$1}', + meta: 'subfig-cmd', + score: 0.007192033516871399, + }, + { + caption: '\\protect', + snippet: '\\protect', + meta: 'subfig-cmd', + score: 0.0200686676229443, + }, + { + caption: '\\subfloat[]{}', + snippet: '\\subfloat[$1]{$2}', + meta: 'subfig-cmd', + score: 0.0286920437310672, + }, + { + caption: '\\subfloat{}', + snippet: '\\subfloat{$1}', + meta: 'subfig-cmd', + score: 0.0286920437310672, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'subfig-cmd', + score: 0.00037306820619479756, + }, + ], + enumerate: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'enumerate-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\makelabel', + snippet: '\\makelabel', + meta: 'enumerate-cmd', + score: 5.739925426740175e-5, + }, + { + caption: '\\makelabel{}', + snippet: '\\makelabel{$1}', + meta: 'enumerate-cmd', + score: 5.739925426740175e-5, + }, + { + caption: '\\makelabel[]{}', + snippet: '\\makelabel[$1]{$2}', + meta: 'enumerate-cmd', + score: 5.739925426740175e-5, + }, + ], + pdfpages: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pdfpages-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'pdfpages-cmd', + score: 0.07503475348393239, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pdfpages-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\includepdf[]{}', + snippet: '\\includepdf[$1]{$2}', + meta: 'pdfpages-cmd', + score: 0.023931732745590156, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pdfpages-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'pdfpages-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'pdfpages-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'pdfpages-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pdfpages-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pdfpages-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'pdfpages-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'pdfpages-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'pdfpages-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'pdfpages-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pdfpages-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pdfpages-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pdfpages-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'pdfpages-cmd', + score: 0.00047530324346933345, + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'pdfpages-cmd', + score: 0.0005277905480209891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pdfpages-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\AddToShipoutPictureFG{}', + snippet: '\\AddToShipoutPictureFG{$1}', + meta: 'pdfpages-cmd', + score: 0.000325977535138643, + }, + { + caption: '\\AddToShipoutPictureBG{}', + snippet: '\\AddToShipoutPictureBG{$1}', + meta: 'pdfpages-cmd', + score: 0.0008957666085644653, + }, + { + caption: '\\AtPageUpperLeft{}', + snippet: '\\AtPageUpperLeft{$1}', + meta: 'pdfpages-cmd', + score: 0.0003608141410278152, + }, + { + caption: '\\LenToUnit{}', + snippet: '\\LenToUnit{$1}', + meta: 'pdfpages-cmd', + score: 0.0007216282820556304, + }, + { + caption: '\\AddToShipoutPicture{}', + snippet: '\\AddToShipoutPicture{$1}', + meta: 'pdfpages-cmd', + score: 0.0017658629469099734, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'pdfpages-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'pdfpages-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'pdfpages-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'pdfpages-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'pdfpages-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'pdfpages-cmd', + score: 0.0018957469739775527, + }, + ], + epstopdf: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\AppendGraphicsExtensions{}', + snippet: '\\AppendGraphicsExtensions{$1}', + meta: 'epstopdf-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'epstopdf-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'epstopdf-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'epstopdf-cmd', + score: 0.0058342578961340175, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'epstopdf-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'epstopdf-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'epstopdf-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'epstopdf-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\epstopdfsetup{}', + snippet: '\\epstopdfsetup{$1}', + meta: 'epstopdf-cmd', + score: 0.0009941134326203623, + }, + { + caption: '\\epstopdfDeclareGraphicsRule{}{}{}{}', + snippet: '\\epstopdfDeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'epstopdf-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\OutputFile', + snippet: '\\OutputFile', + meta: 'epstopdf-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'epstopdf-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'epstopdf-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'epstopdf-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'epstopdf-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'epstopdf-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epstopdf-cmd', + score: 0.008565354665444157, + }, + ], + lmodern: [ + { + caption: '\\rmdefault', + snippet: '\\rmdefault', + meta: 'lmodern-cmd', + score: 0.0012870877747432935, + }, + { + caption: '\\sfdefault', + snippet: '\\sfdefault', + meta: 'lmodern-cmd', + score: 0.008427383388519996, + }, + { + caption: '\\sfdefault{}', + snippet: '\\sfdefault{$1}', + meta: 'lmodern-cmd', + score: 0.008427383388519996, + }, + ], + pifont: [ + { + caption: '\\ding{}', + snippet: '\\ding{$1}', + meta: 'pifont-cmd', + score: 0.009992300665793867, + }, + ], + ragged2e: [ + { + caption: '\\justifying', + snippet: '\\justifying', + meta: 'ragged2e-cmd', + score: 0.010373702256548788, + }, + { + caption: '\\justifying{}', + snippet: '\\justifying{$1}', + meta: 'ragged2e-cmd', + score: 0.010373702256548788, + }, + { + caption: '\\RaggedRight', + snippet: '\\RaggedRight', + meta: 'ragged2e-cmd', + score: 0.001021021782267457, + }, + { + caption: '\\Centering', + snippet: '\\Centering', + meta: 'ragged2e-cmd', + score: 0.00037395241488843035, + }, + { + caption: '\\selectfont', + snippet: '\\selectfont', + meta: 'ragged2e-cmd', + score: 0.04598628699063736, + }, + ], + rotating: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rotating-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'rotating-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'rotating-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'rotating-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'rotating-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'rotating-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'rotating-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'rotating-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'rotating-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'rotating-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'rotating-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rotating-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'rotating-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'rotating-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'rotating-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'rotating-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'rotating-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'rotating-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'rotating-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'rotating-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'rotating-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'rotating-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'rotating-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'rotating-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'rotating-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'rotating-cmd', + score: 0.004719094298848707, + }, + ], + xltxtra: [ + { + caption: '\\textsubscript{}', + snippet: '\\textsubscript{$1}', + meta: 'xltxtra-cmd', + score: 0.058405875394131175, + }, + { + caption: '\\textsuperscript{}', + snippet: '\\textsuperscript{$1}', + meta: 'xltxtra-cmd', + score: 0.05216393882408519, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'xltxtra-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xltxtra-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'xltxtra-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'xltxtra-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'xltxtra-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'xltxtra-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xltxtra-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xltxtra-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'xltxtra-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'xltxtra-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'xltxtra-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'xltxtra-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'xltxtra-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xltxtra-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'xltxtra-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xltxtra-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'xltxtra-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'xltxtra-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'xltxtra-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'xltxtra-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'xltxtra-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\XeTeX', + snippet: '\\XeTeX', + meta: 'xltxtra-cmd', + score: 0.0010635559050357936, + }, + { + caption: '\\TeX', + snippet: '\\TeX', + meta: 'xltxtra-cmd', + score: 0.02873756018238537, + }, + { + caption: '\\TeX{}', + snippet: '\\TeX{$1}', + meta: 'xltxtra-cmd', + score: 0.02873756018238537, + }, + { + caption: '\\LaTeX', + snippet: '\\LaTeX', + meta: 'xltxtra-cmd', + score: 0.2334089308452787, + }, + { + caption: '\\LaTeX{}', + snippet: '\\LaTeX{$1}', + meta: 'xltxtra-cmd', + score: 0.2334089308452787, + }, + { + caption: '\\XeLaTeX', + snippet: '\\XeLaTeX', + meta: 'xltxtra-cmd', + score: 0.002009786035379175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xltxtra-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xltxtra-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xltxtra-cmd', + score: 0.2864294797053033, + }, + ], + marvosym: [ + { + caption: '\\Mundus', + snippet: '\\Mundus', + meta: 'marvosym-cmd', + score: 0.0006349134235582933, + }, + { + caption: '\\Telefon', + snippet: '\\Telefon', + meta: 'marvosym-cmd', + score: 0.0003618274070138519, + }, + { + caption: '\\Letter', + snippet: '\\Letter', + meta: 'marvosym-cmd', + score: 0.0012281130571092198, + }, + { + caption: '\\Mobilefone', + snippet: '\\Mobilefone', + meta: 'marvosym-cmd', + score: 0.0005432037068220953, + }, + ], + dcolumn: [ + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'dcolumn-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'dcolumn-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'dcolumn-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'dcolumn-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'dcolumn-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'dcolumn-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'dcolumn-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'dcolumn-cmd', + score: 0.018615449342361392, + }, + ], + xspace: [ + { + caption: '\\xspace', + snippet: '\\xspace', + meta: 'xspace-cmd', + score: 0.07560370351316588, + }, + ], + xunicode: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xunicode-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xunicode-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xunicode-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xunicode-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'xunicode-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'xunicode-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'xunicode-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'xunicode-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'xunicode-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xunicode-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'xunicode-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xunicode-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'xunicode-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'xunicode-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'xunicode-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'xunicode-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'xunicode-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'xunicode-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'xunicode-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'xunicode-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'xunicode-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xunicode-cmd', + score: 0.008565354665444157, + }, + ], + csquotes: [ + { + caption: '\\mkcitation', + snippet: '\\mkcitation', + meta: 'csquotes-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\DeclareQuoteAlias{}{}', + snippet: '\\DeclareQuoteAlias{$1}{$2}', + meta: 'csquotes-cmd', + score: 0.0004906235524176374, + }, + { + caption: '\\quote{}', + snippet: '\\quote{$1}', + meta: 'csquotes-cmd', + score: 0.030690393112264815, + }, + { + caption: '\\quote', + snippet: '\\quote', + meta: 'csquotes-cmd', + score: 0.030690393112264815, + }, + { + caption: '\\setquotestyle[]{}', + snippet: '\\setquotestyle[$1]{$2}', + meta: 'csquotes-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\blockquote{}', + snippet: '\\blockquote{$1}', + meta: 'csquotes-cmd', + score: 0.00023365626458085812, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'csquotes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'csquotes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\mkbegdispquote', + snippet: '\\mkbegdispquote', + meta: 'csquotes-cmd', + score: 4.203362017075738e-5, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'csquotes-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\break', + snippet: '\\break', + meta: 'csquotes-cmd', + score: 0.016352452390960115, + }, + { + caption: '\\break{}', + snippet: '\\break{$1}', + meta: 'csquotes-cmd', + score: 0.016352452390960115, + }, + { + caption: '\\break{}{}', + snippet: '\\break{$1}{$2}', + meta: 'csquotes-cmd', + score: 0.016352452390960115, + }, + { + caption: '\\ifpunctmark{}', + snippet: '\\ifpunctmark{$1}', + meta: 'csquotes-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\endquote', + snippet: '\\endquote', + meta: 'csquotes-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\par', + snippet: '\\par', + meta: 'csquotes-cmd', + score: 0.413853376001159, + }, + { + caption: '\\DeclareQuoteStyle[]{}', + snippet: '\\DeclareQuoteStyle[$1]{$2}', + meta: 'csquotes-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\SetBlockEnvironment{}', + snippet: '\\SetBlockEnvironment{$1}', + meta: 'csquotes-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'csquotes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\MakeOuterQuote{}', + snippet: '\\MakeOuterQuote{$1}', + meta: 'csquotes-cmd', + score: 0.0019170811203505262, + }, + { + caption: '\\enquote{}', + snippet: '\\enquote{$1}', + meta: 'csquotes-cmd', + score: 0.0077432730806830915, + }, + { + caption: '\\SetCiteCommand{}', + snippet: '\\SetCiteCommand{$1}', + meta: 'csquotes-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'csquotes-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'csquotes-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'csquotes-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'csquotes-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'csquotes-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'csquotes-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'csquotes-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'csquotes-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'csquotes-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'csquotes-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'csquotes-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'csquotes-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'csquotes-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'csquotes-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'csquotes-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'csquotes-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'csquotes-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'csquotes-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'csquotes-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'csquotes-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'csquotes-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'csquotes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'csquotes-cmd', + score: 0.00037306820619479756, + }, + ], + xparse: [ + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xparse-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xparse-cmd', + score: 0.2864294797053033, + }, + ], + soul: [ + { + caption: '\\DeclareRobustCommand{}{}', + snippet: '\\DeclareRobustCommand{$1}{$2}', + meta: 'soul-cmd', + score: 0.0010373158471650705, + }, + { + caption: '\\DeclareRobustCommand{}[]{}', + snippet: '\\DeclareRobustCommand{$1}[$2]{$3}', + meta: 'soul-cmd', + score: 0.0010373158471650705, + }, + { + caption: '\\sethlcolor{}', + snippet: '\\sethlcolor{$1}', + meta: 'soul-cmd', + score: 0.01970230898277056, + }, + { + caption: '\\st', + snippet: '\\st', + meta: 'soul-cmd', + score: 0.004652662833362787, + }, + { + caption: '\\st{}', + snippet: '\\st{$1}', + meta: 'soul-cmd', + score: 0.004652662833362787, + }, + { + caption: '\\def', + snippet: '\\def', + meta: 'soul-cmd', + score: 0.21357759092476175, + }, + { + caption: '\\hl{}', + snippet: '\\hl{$1}', + meta: 'soul-cmd', + score: 0.03421486301062431, + }, + { + caption: '\\sodef', + snippet: '\\sodef', + meta: 'soul-cmd', + score: 0.0017045357696831268, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'soul-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\so', + snippet: '\\so', + meta: 'soul-cmd', + score: 0.004308800134587786, + }, + { + caption: '\\so{}', + snippet: '\\so{$1}', + meta: 'soul-cmd', + score: 0.004308800134587786, + }, + ], + comment: [ + { + caption: '\\specialcomment{}{}{}', + snippet: '\\specialcomment{$1}{$2}{$3}', + meta: 'comment-cmd', + score: 9.120209837787948e-5, + }, + { + caption: '\\includecomment{}', + snippet: '\\includecomment{$1}', + meta: 'comment-cmd', + score: 8.21804444236254e-5, + }, + ], + algorithm2e: [ + { + caption: '\\FuncSty{}', + snippet: '\\FuncSty{$1}', + meta: 'algorithm2e-cmd', + score: 7.576875738934807e-5, + }, + { + caption: '\\algorithmautorefname', + snippet: '\\algorithmautorefname', + meta: 'algorithm2e-cmd', + score: 2.0085955839419213e-5, + }, + { + caption: '\\SetAlgoNoLine', + snippet: '\\SetAlgoNoLine', + meta: 'algorithm2e-cmd', + score: 0.00015722499147840545, + }, + { + caption: '\\Indp', + snippet: '\\Indp', + meta: 'algorithm2e-cmd', + score: 6.068942580823901e-5, + }, + { + caption: '\\AlCapFnt', + snippet: '\\AlCapFnt', + meta: 'algorithm2e-cmd', + score: 3.0307502955739227e-5, + }, + { + caption: '\\LinesNumbered', + snippet: '\\LinesNumbered', + meta: 'algorithm2e-cmd', + score: 0.000162125616653719, + }, + { + caption: '\\SetAlFnt{}', + snippet: '\\SetAlFnt{$1}', + meta: 'algorithm2e-cmd', + score: 0.0024446198714390757, + }, + { + caption: '\\SetKw{}{}', + snippet: '\\SetKw{$1}{$2}', + meta: 'algorithm2e-cmd', + score: 9.292434841280213e-5, + }, + { + caption: '\\RestyleAlgo{}', + snippet: '\\RestyleAlgo{$1}', + meta: 'algorithm2e-cmd', + score: 0.00019243311960945823, + }, + { + caption: '\\listofalgorithms', + snippet: '\\listofalgorithms', + meta: 'algorithm2e-cmd', + score: 0.0012576983422794912, + }, + { + caption: '\\IncMargin{}', + snippet: '\\IncMargin{$1}', + meta: 'algorithm2e-cmd', + score: 0.0024294661199612063, + }, + { + caption: '\\BlankLine', + snippet: '\\BlankLine', + meta: 'algorithm2e-cmd', + score: 0.005049617303688214, + }, + { + caption: '\\SetCommentSty{}', + snippet: '\\SetCommentSty{$1}', + meta: 'algorithm2e-cmd', + score: 0.0001778112853266571, + }, + { + caption: '\\SetAlgoNoEnd', + snippet: '\\SetAlgoNoEnd', + meta: 'algorithm2e-cmd', + score: 0.00015722499147840545, + }, + { + caption: '\\theAlgoLine{}', + snippet: '\\theAlgoLine{$1}', + meta: 'algorithm2e-cmd', + score: 1.5153751477869614e-5, + }, + { + caption: '\\SetKwBlock{}{}{}', + snippet: '\\SetKwBlock{$1}{$2}{$3}', + meta: 'algorithm2e-cmd', + score: 0.000981463850523159, + }, + { + caption: '\\SetKwBlock{}{}', + snippet: '\\SetKwBlock{$1}{$2}', + meta: 'algorithm2e-cmd', + score: 0.000981463850523159, + }, + { + caption: '\\AlCapNameFnt', + snippet: '\\AlCapNameFnt', + meta: 'algorithm2e-cmd', + score: 3.0307502955739227e-5, + }, + { + caption: '\\SetAlgoSkip{}', + snippet: '\\SetAlgoSkip{$1}', + meta: 'algorithm2e-cmd', + score: 0.00017454032258926576, + }, + { + caption: '\\SetKwFunction{}{}', + snippet: '\\SetKwFunction{$1}{$2}', + meta: 'algorithm2e-cmd', + score: 0.0015332307832994817, + }, + { + caption: '\\nllabel{}', + snippet: '\\nllabel{$1}', + meta: 'algorithm2e-cmd', + score: 0.0001844460347791443, + }, + { + caption: '\\SetAlgoInsideSkip{}', + snippet: '\\SetAlgoInsideSkip{$1}', + meta: 'algorithm2e-cmd', + score: 4.5812360816321294e-5, + }, + { + caption: '\\DataSty{}', + snippet: '\\DataSty{$1}', + meta: 'algorithm2e-cmd', + score: 1.5153751477869614e-5, + }, + { + caption: '\\SetKwInOut{}{}', + snippet: '\\SetKwInOut{$1}{$2}', + meta: 'algorithm2e-cmd', + score: 0.0017021978326807814, + }, + { + caption: '\\SetAlCapFnt{}', + snippet: '\\SetAlCapFnt{$1}', + meta: 'algorithm2e-cmd', + score: 0.0024294661199612063, + }, + { + caption: '\\CommentSty{}', + snippet: '\\CommentSty{$1}', + meta: 'algorithm2e-cmd', + score: 0.0001111448631633176, + }, + { + caption: '\\SetAlCapHSkip{}', + snippet: '\\SetAlCapHSkip{$1}', + meta: 'algorithm2e-cmd', + score: 0.0024294661199612063, + }, + { + caption: '\\renewcommand{}{}', + snippet: '\\renewcommand{$1}{$2}', + meta: 'algorithm2e-cmd', + score: 0.3267437011085663, + }, + { + caption: '\\renewcommand', + snippet: '\\renewcommand', + meta: 'algorithm2e-cmd', + score: 0.3267437011085663, + }, + { + caption: '\\algorithmcfname', + snippet: '\\algorithmcfname', + meta: 'algorithm2e-cmd', + score: 0.0024445413067013134, + }, + { + caption: '\\SetKwIF{}{}{}{}{}{}{}{}', + snippet: '\\SetKwIF{$1}{$2}{$3}{$4}{$5}{$6}{$7}{$8}', + meta: 'algorithm2e-cmd', + score: 1.5153751477869614e-5, + }, + { + caption: '\\SetAlgoCaptionSeparator{}', + snippet: '\\SetAlgoCaptionSeparator{$1}', + meta: 'algorithm2e-cmd', + score: 1.5153751477869614e-5, + }, + { + caption: '\\AlCapSty{}', + snippet: '\\AlCapSty{$1}', + meta: 'algorithm2e-cmd', + score: 3.0307502955739227e-5, + }, + { + caption: '\\ArgSty{}', + snippet: '\\ArgSty{$1}', + meta: 'algorithm2e-cmd', + score: 3.0307502955739227e-5, + }, + { + caption: '\\AlCapNameSty{}', + snippet: '\\AlCapNameSty{$1}', + meta: 'algorithm2e-cmd', + score: 3.0307502955739227e-5, + }, + { + caption: '\\SetKwData{}{}', + snippet: '\\SetKwData{$1}{$2}', + meta: 'algorithm2e-cmd', + score: 0.00235652682860263, + }, + { + caption: '\\listalgorithmcfname', + snippet: '\\listalgorithmcfname', + meta: 'algorithm2e-cmd', + score: 1.5075186740106946e-5, + }, + { + caption: '\\Indm', + snippet: '\\Indm', + meta: 'algorithm2e-cmd', + score: 6.068942580823901e-5, + }, + { + caption: '\\SetAlCapNameFnt{}', + snippet: '\\SetAlCapNameFnt{$1}', + meta: 'algorithm2e-cmd', + score: 0.0024294661199612063, + }, + { + caption: '\\DontPrintSemicolon', + snippet: '\\DontPrintSemicolon', + meta: 'algorithm2e-cmd', + score: 0.001062087490197768, + }, + { + caption: '\\SetAlgoLined', + snippet: '\\SetAlgoLined', + meta: 'algorithm2e-cmd', + score: 0.0017151361342403852, + }, + { + caption: '\\SetAlCapSkip{}', + snippet: '\\SetAlCapSkip{$1}', + meta: 'algorithm2e-cmd', + score: 0.0006213942502400296, + }, + { + caption: '\\LinesNotNumbered', + snippet: '\\LinesNotNumbered', + meta: 'algorithm2e-cmd', + score: 1.5153751477869614e-5, + }, + { + caption: '\\SetKwProg{}{}{}{}', + snippet: '\\SetKwProg{$1}{$2}{$3}{$4}', + meta: 'algorithm2e-cmd', + score: 0.0008518783278391971, + }, + { + caption: '\\SetAlgoVlined', + snippet: '\\SetAlgoVlined', + meta: 'algorithm2e-cmd', + score: 1.5153751477869614e-5, + }, + { + caption: '\\SetKwRepeat{}{}{}', + snippet: '\\SetKwRepeat{$1}{$2}{$3}', + meta: 'algorithm2e-cmd', + score: 6.110202388233705e-5, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'algorithm2e-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\chapter{}', + snippet: '\\chapter{$1}', + meta: 'algorithm2e-cmd', + score: 0.422097569591803, + }, + { + caption: '\\SetKwFor{}{}{}{}', + snippet: '\\SetKwFor{$1}{$2}{$3}{$4}', + meta: 'algorithm2e-cmd', + score: 0.00010699539949594301, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'algorithm2e-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'algorithm2e-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'algorithm2e-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'algorithm2e-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'algorithm2e-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'algorithm2e-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\xspace', + snippet: '\\xspace', + meta: 'algorithm2e-cmd', + score: 0.07560370351316588, + }, + ], + tocbibind: [ + { + caption: '\\contentsname', + snippet: '\\contentsname', + meta: 'tocbibind-cmd', + score: 0.010205180337548728, + }, + { + caption: '\\contentsname{}', + snippet: '\\contentsname{$1}', + meta: 'tocbibind-cmd', + score: 0.010205180337548728, + }, + { + caption: '\\tocchapter', + snippet: '\\tocchapter', + meta: 'tocbibind-cmd', + score: 0.00016023188758771694, + }, + { + caption: '\\indexname', + snippet: '\\indexname', + meta: 'tocbibind-cmd', + score: 0.0007544109314450072, + }, + { + caption: '\\listoffigures', + snippet: '\\listoffigures', + meta: 'tocbibind-cmd', + score: 0.03447318897846567, + }, + { + caption: '\\tocfile{}{}', + snippet: '\\tocfile{$1}{$2}', + meta: 'tocbibind-cmd', + score: 0.00016023188758771694, + }, + { + caption: '\\tocbibname', + snippet: '\\tocbibname', + meta: 'tocbibind-cmd', + score: 0.0020762574479507175, + }, + { + caption: '\\settocbibname{}', + snippet: '\\settocbibname{$1}', + meta: 'tocbibind-cmd', + score: 0.00010668677119599426, + }, + { + caption: '\\listoftables', + snippet: '\\listoftables', + meta: 'tocbibind-cmd', + score: 0.02104656820469027, + }, + { + caption: '\\tableofcontents', + snippet: '\\tableofcontents', + meta: 'tocbibind-cmd', + score: 0.13360595130994957, + }, + { + caption: '\\listfigurename', + snippet: '\\listfigurename', + meta: 'tocbibind-cmd', + score: 0.0034407237779350256, + }, + ], + pgfplots: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfplots-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfplots-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfplots-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfplots-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfplots-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfplots-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfplots-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfplots-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfplots-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfplots-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfplots-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfplots-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfplots-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfplots-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfplots-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfplots-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfplots-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfplots-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfplots-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfplots-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfplots-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfplots-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfplots-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfplots-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfplots-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfplots-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfplots-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfplots-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfplots-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfplots-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfplots-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfplots-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfplots-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfplots-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfplots-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfplots-cmd', + score: 0.2864294797053033, + }, + ], + lastpage: [ + { + caption: '\\string', + snippet: '\\string', + meta: 'lastpage-cmd', + score: 0.001042697111754002, + }, + ], + graphics: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'graphics-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'graphics-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'graphics-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'graphics-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'graphics-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'graphics-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'graphics-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'graphics-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'graphics-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'graphics-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'graphics-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'graphics-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'graphics-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'graphics-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'graphics-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'graphics-cmd', + score: 0.008565354665444157, + }, + ], + algorithmic: [ + { + caption: '\\REPEAT', + snippet: '\\REPEAT', + meta: 'algorithmic-cmd', + score: 0.0004816110638193742, + }, + { + caption: '\\ENDIF', + snippet: '\\ENDIF', + meta: 'algorithmic-cmd', + score: 0.003585213685098552, + }, + { + caption: '\\algorithmicwhile', + snippet: '\\algorithmicwhile', + meta: 'algorithmic-cmd', + score: 0.0005769483780443573, + }, + { + caption: '\\algorithmicwhile{}', + snippet: '\\algorithmicwhile{$1}', + meta: 'algorithmic-cmd', + score: 0.0005769483780443573, + }, + { + caption: '\\FOR{}', + snippet: '\\FOR{$1}', + meta: 'algorithmic-cmd', + score: 0.004074774218819945, + }, + { + caption: '\\algorithmicif', + snippet: '\\algorithmicif', + meta: 'algorithmic-cmd', + score: 0.00039654130753044966, + }, + { + caption: '\\algorithmicif{}', + snippet: '\\algorithmicif{$1}', + meta: 'algorithmic-cmd', + score: 0.00039654130753044966, + }, + { + caption: '\\ENDFOR', + snippet: '\\ENDFOR', + meta: 'algorithmic-cmd', + score: 0.004428141530092572, + }, + { + caption: '\\UNTIL', + snippet: '\\UNTIL', + meta: 'algorithmic-cmd', + score: 0.0004816110638193742, + }, + { + caption: '\\UNTIL{}', + snippet: '\\UNTIL{$1}', + meta: 'algorithmic-cmd', + score: 0.0004816110638193742, + }, + { + caption: '\\IF{}', + snippet: '\\IF{$1}', + meta: 'algorithmic-cmd', + score: 0.0036985887706967417, + }, + { + caption: '\\ENSURE', + snippet: '\\ENSURE', + meta: 'algorithmic-cmd', + score: 0.0013188761425395954, + }, + { + caption: '\\algorithmiccomment', + snippet: '\\algorithmiccomment', + meta: 'algorithmic-cmd', + score: 0.00021737766481978388, + }, + { + caption: '\\ENDWHILE', + snippet: '\\ENDWHILE', + meta: 'algorithmic-cmd', + score: 0.00047037943460091465, + }, + { + caption: '\\algorithmicend', + snippet: '\\algorithmicend', + meta: 'algorithmic-cmd', + score: 0.0011128218085672747, + }, + { + caption: '\\algorithmicend{}', + snippet: '\\algorithmicend{$1}', + meta: 'algorithmic-cmd', + score: 0.0011128218085672747, + }, + { + caption: '\\algorithmicrequire', + snippet: '\\algorithmicrequire', + meta: 'algorithmic-cmd', + score: 0.004751598472180266, + }, + { + caption: '\\algorithmicdo', + snippet: '\\algorithmicdo', + meta: 'algorithmic-cmd', + score: 0.0005655570358533174, + }, + { + caption: '\\algorithmicdo{}', + snippet: '\\algorithmicdo{$1}', + meta: 'algorithmic-cmd', + score: 0.0005655570358533174, + }, + { + caption: '\\algorithmicfor', + snippet: '\\algorithmicfor', + meta: 'algorithmic-cmd', + score: 0.0005681785898943757, + }, + { + caption: '\\algorithmicfor{}', + snippet: '\\algorithmicfor{$1}', + meta: 'algorithmic-cmd', + score: 0.0005681785898943757, + }, + { + caption: '\\RETURN', + snippet: '\\RETURN', + meta: 'algorithmic-cmd', + score: 0.0013054907995767408, + }, + { + caption: '\\algorithmicand', + snippet: '\\algorithmicand', + meta: 'algorithmic-cmd', + score: 5.326674280259771e-5, + }, + { + caption: '\\algsetup{}', + snippet: '\\algsetup{$1}', + meta: 'algorithmic-cmd', + score: 0.00012872796177294446, + }, + { + caption: '\\algorithmicreturn{}', + snippet: '\\algorithmicreturn{$1}', + meta: 'algorithmic-cmd', + score: 0.00022490402516652368, + }, + { + caption: '\\algorithmicreturn', + snippet: '\\algorithmicreturn', + meta: 'algorithmic-cmd', + score: 0.00022490402516652368, + }, + { + caption: '\\algorithmicforall{}', + snippet: '\\algorithmicforall{$1}', + meta: 'algorithmic-cmd', + score: 0.00022490402516652368, + }, + { + caption: '\\algorithmicforall', + snippet: '\\algorithmicforall', + meta: 'algorithmic-cmd', + score: 0.00022490402516652368, + }, + { + caption: '\\COMMENT', + snippet: '\\COMMENT', + meta: 'algorithmic-cmd', + score: 0.00025669572555354604, + }, + { + caption: '\\COMMENT{}', + snippet: '\\COMMENT{$1}', + meta: 'algorithmic-cmd', + score: 0.00025669572555354604, + }, + { + caption: '\\REQUIRE', + snippet: '\\REQUIRE', + meta: 'algorithmic-cmd', + score: 0.001870681168192269, + }, + { + caption: '\\algorithmicor', + snippet: '\\algorithmicor', + meta: 'algorithmic-cmd', + score: 5.326674280259771e-5, + }, + { + caption: '\\ELSE', + snippet: '\\ELSE', + meta: 'algorithmic-cmd', + score: 0.0007599864146830139, + }, + { + caption: '\\STATE', + snippet: '\\STATE', + meta: 'algorithmic-cmd', + score: 0.0266684860947573, + }, + { + caption: '\\WHILE{}', + snippet: '\\WHILE{$1}', + meta: 'algorithmic-cmd', + score: 0.00047037943460091465, + }, + { + caption: '\\ELSIF{}', + snippet: '\\ELSIF{$1}', + meta: 'algorithmic-cmd', + score: 0.0001991613148371481, + }, + { + caption: '\\FALSE', + snippet: '\\FALSE', + meta: 'algorithmic-cmd', + score: 3.34222699937868e-5, + }, + { + caption: '\\AND', + snippet: '\\AND', + meta: 'algorithmic-cmd', + score: 6.401730289932545e-5, + }, + { + caption: '\\algorithmicensure', + snippet: '\\algorithmicensure', + meta: 'algorithmic-cmd', + score: 0.003439482525198322, + }, + { + caption: '\\OR', + snippet: '\\OR', + meta: 'algorithmic-cmd', + score: 6.401730289932545e-5, + }, + { + caption: '\\algorithmicrepeat', + snippet: '\\algorithmicrepeat', + meta: 'algorithmic-cmd', + score: 5.326674280259771e-5, + }, + { + caption: '\\TRUE', + snippet: '\\TRUE', + meta: 'algorithmic-cmd', + score: 0.0001336890799751472, + }, + { + caption: '\\FORALL{}', + snippet: '\\FORALL{$1}', + meta: 'algorithmic-cmd', + score: 0.0003533673112726266, + }, + { + caption: '\\algorithmicthen{}', + snippet: '\\algorithmicthen{$1}', + meta: 'algorithmic-cmd', + score: 0.00032476571672371697, + }, + { + caption: '\\algorithmicthen', + snippet: '\\algorithmicthen', + meta: 'algorithmic-cmd', + score: 0.00032476571672371697, + }, + { + caption: '\\algorithmicuntil', + snippet: '\\algorithmicuntil', + meta: 'algorithmic-cmd', + score: 5.326674280259771e-5, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'algorithmic-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'algorithmic-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'algorithmic-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'algorithmic-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'algorithmic-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'algorithmic-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'algorithmic-cmd', + score: 0.00037306820619479756, + }, + ], + lineno: [ + { + caption: '\\pagewiselinenumbers', + snippet: '\\pagewiselinenumbers', + meta: 'lineno-cmd', + score: 0.00016870831850106035, + }, + { + caption: '\\linenomath', + snippet: '\\linenomath', + meta: 'lineno-cmd', + score: 1.4517338420208715e-5, + }, + { + caption: '\\linenumberfont{}', + snippet: '\\linenumberfont{$1}', + meta: 'lineno-cmd', + score: 0.0001811784338695797, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'lineno-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'lineno-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\endlinenomath', + snippet: '\\endlinenomath', + meta: 'lineno-cmd', + score: 1.4517338420208715e-5, + }, + { + caption: '\\nolinenumbers', + snippet: '\\nolinenumbers', + meta: 'lineno-cmd', + score: 0.0009805246614299932, + }, + { + caption: '\\path', + snippet: '\\path', + meta: 'lineno-cmd', + score: 0.028200474217322108, + }, + { + caption: '\\path[]', + snippet: '\\path[$1]', + meta: 'lineno-cmd', + score: 0.028200474217322108, + }, + { + caption: '\\path{}', + snippet: '\\path{$1}', + meta: 'lineno-cmd', + score: 0.028200474217322108, + }, + { + caption: '\\filedate{}', + snippet: '\\filedate{$1}', + meta: 'lineno-cmd', + score: 0.000578146635331119, + }, + { + caption: '\\filedate', + snippet: '\\filedate', + meta: 'lineno-cmd', + score: 0.000578146635331119, + }, + { + caption: '\\linenumbers', + snippet: '\\linenumbers', + meta: 'lineno-cmd', + score: 0.004687680659497865, + }, + { + caption: '\\modulolinenumbers[]', + snippet: '\\modulolinenumbers[$1]', + meta: 'lineno-cmd', + score: 0.0027194991933605197, + }, + { + caption: '\\fileversion{}', + snippet: '\\fileversion{$1}', + meta: 'lineno-cmd', + score: 0.000578146635331119, + }, + { + caption: '\\fileversion', + snippet: '\\fileversion', + meta: 'lineno-cmd', + score: 0.000578146635331119, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'lineno-cmd', + score: 0.008565354665444157, + }, + ], + mathptmx: [ + { + caption: '\\rmdefault', + snippet: '\\rmdefault', + meta: 'mathptmx-cmd', + score: 0.0012870877747432935, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'mathptmx-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'mathptmx-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'mathptmx-cmd', + score: 0.05613164277964739, + }, + ], + todonotes: [ + { + caption: '\\missingfigure[]{}', + snippet: '\\missingfigure[$1]{$2}', + meta: 'todonotes-cmd', + score: 0.001558719179721163, + }, + { + caption: '\\missingfigure', + snippet: '\\missingfigure', + meta: 'todonotes-cmd', + score: 0.001558719179721163, + }, + { + caption: '\\todototoc', + snippet: '\\todototoc', + meta: 'todonotes-cmd', + score: 0.000325977535138643, + }, + { + caption: '\\todo{}', + snippet: '\\todo{$1}', + meta: 'todonotes-cmd', + score: 0.04115074278362878, + }, + { + caption: '\\todo[]{}', + snippet: '\\todo[$1]{$2}', + meta: 'todonotes-cmd', + score: 0.04115074278362878, + }, + { + caption: '\\todo', + snippet: '\\todo', + meta: 'todonotes-cmd', + score: 0.04115074278362878, + }, + { + caption: '\\listoftodos', + snippet: '\\listoftodos', + meta: 'todonotes-cmd', + score: 0.0005325975940754609, + }, + { + caption: '\\listoftodos[]', + snippet: '\\listoftodos[$1]', + meta: 'todonotes-cmd', + score: 0.0005325975940754609, + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'todonotes-cmd', + score: 0.0174633138331273, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'todonotes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'todonotes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'todonotes-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'todonotes-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'todonotes-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'todonotes-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'todonotes-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'todonotes-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'todonotes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'todonotes-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'todonotes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'todonotes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'todonotes-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'todonotes-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'todonotes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'todonotes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'todonotes-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'todonotes-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'todonotes-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'todonotes-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'todonotes-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'todonotes-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'todonotes-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'todonotes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'todonotes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'todonotes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'todonotes-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'todonotes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'todonotes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'todonotes-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'todonotes-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'todonotes-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'todonotes-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'todonotes-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'todonotes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'todonotes-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'todonotes-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'todonotes-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'todonotes-cmd', + score: 0.2864294797053033, + }, + ], + ulem: [ + { + caption: '\\sout{}', + snippet: '\\sout{$1}', + meta: 'ulem-cmd', + score: 0.0010443313503631364, + }, + { + caption: '\\sout', + snippet: '\\sout', + meta: 'ulem-cmd', + score: 0.0010443313503631364, + }, + { + caption: '\\MakeRobust', + snippet: '\\MakeRobust', + meta: 'ulem-cmd', + score: 3.140504277052775e-5, + }, + { + caption: '\\hss', + snippet: '\\hss', + meta: 'ulem-cmd', + score: 0.0020627882815078768, + }, + { + caption: '\\uline{}', + snippet: '\\uline{$1}', + meta: 'ulem-cmd', + score: 0.005956273219192909, + }, + { + caption: '\\uline', + snippet: '\\uline', + meta: 'ulem-cmd', + score: 0.005956273219192909, + }, + { + caption: '\\markoverwith{}', + snippet: '\\markoverwith{$1}', + meta: 'ulem-cmd', + score: 0.0004888431085285657, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'ulem-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\hfill', + snippet: '\\hfill', + meta: 'ulem-cmd', + score: 0.2058248088519886, + }, + { + caption: '\\ULon', + snippet: '\\ULon', + meta: 'ulem-cmd', + score: 0.0004888431085285657, + }, + { + caption: '\\normalem', + snippet: '\\normalem', + meta: 'ulem-cmd', + score: 0.00015564484081028078, + }, + { + caption: '\\useunder{}{}{}', + snippet: '\\useunder{$1}{$2}{$3}', + meta: 'ulem-cmd', + score: 0.0013185833851097916, + }, + { + caption: '\\hfil', + snippet: '\\hfil', + meta: 'ulem-cmd', + score: 0.006880789969115855, + }, + { + caption: '\\sout{}', + snippet: '\\sout{$1}', + meta: 'ulem-cmd', + score: 0.0010443313503631364, + }, + { + caption: '\\sout', + snippet: '\\sout', + meta: 'ulem-cmd', + score: 0.0010443313503631364, + }, + { + caption: '\\MakeRobust', + snippet: '\\MakeRobust', + meta: 'ulem-cmd', + score: 3.140504277052775e-5, + }, + { + caption: '\\hss', + snippet: '\\hss', + meta: 'ulem-cmd', + score: 0.0020627882815078768, + }, + { + caption: '\\uline{}', + snippet: '\\uline{$1}', + meta: 'ulem-cmd', + score: 0.005956273219192909, + }, + { + caption: '\\uline', + snippet: '\\uline', + meta: 'ulem-cmd', + score: 0.005956273219192909, + }, + { + caption: '\\markoverwith{}', + snippet: '\\markoverwith{$1}', + meta: 'ulem-cmd', + score: 0.0004888431085285657, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'ulem-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\hfill', + snippet: '\\hfill', + meta: 'ulem-cmd', + score: 0.2058248088519886, + }, + { + caption: '\\ULon', + snippet: '\\ULon', + meta: 'ulem-cmd', + score: 0.0004888431085285657, + }, + { + caption: '\\normalem', + snippet: '\\normalem', + meta: 'ulem-cmd', + score: 0.00015564484081028078, + }, + { + caption: '\\useunder{}{}{}', + snippet: '\\useunder{$1}{$2}{$3}', + meta: 'ulem-cmd', + score: 0.0013185833851097916, + }, + { + caption: '\\hfil', + snippet: '\\hfil', + meta: 'ulem-cmd', + score: 0.006880789969115855, + }, + ], + gensymb: [ + { + caption: '\\degree', + snippet: '\\degree', + meta: 'gensymb-cmd', + score: 0.044752043138360405, + }, + { + caption: '\\ohm', + snippet: '\\ohm', + meta: 'gensymb-cmd', + score: 0.0038146685721293138, + }, + { + caption: '\\micro', + snippet: '\\micro', + meta: 'gensymb-cmd', + score: 0.011051971930487929, + }, + { + caption: '\\celsius', + snippet: '\\celsius', + meta: 'gensymb-cmd', + score: 0.0010806983851157788, + }, + ], + siunitx: [ + { + caption: '\\DeclareSIUnit{}{}', + snippet: '\\DeclareSIUnit{$1}{$2}', + meta: 'siunitx-cmd', + score: 0.00017911905960739648, + }, + { + caption: '\\DeclareSIUnit', + snippet: '\\DeclareSIUnit', + meta: 'siunitx-cmd', + score: 0.00017911905960739648, + }, + { + caption: '\\si{}', + snippet: '\\si{$1}', + meta: 'siunitx-cmd', + score: 0.015042996547458706, + }, + { + caption: '\\num{}', + snippet: '\\num{$1}', + meta: 'siunitx-cmd', + score: 0.0005077454796577224, + }, + { + caption: '\\num[]{}', + snippet: '\\num[$1]{$2}', + meta: 'siunitx-cmd', + score: 0.0005077454796577224, + }, + { + caption: '\\ang{}', + snippet: '\\ang{$1}', + meta: 'siunitx-cmd', + score: 0.00026216419341458844, + }, + { + caption: '\\SIrange{}{}{}', + snippet: '\\SIrange{$1}{$2}{$3}', + meta: 'siunitx-cmd', + score: 0.0004920776847142836, + }, + { + caption: '\\SIrange[]{}{}{}', + snippet: '\\SIrange[$1]{$2}{$3}{$4}', + meta: 'siunitx-cmd', + score: 0.0004920776847142836, + }, + { + caption: '\\SIlist{}{}', + snippet: '\\SIlist{$1}{$2}', + meta: 'siunitx-cmd', + score: 2.5005836362206937e-5, + }, + { + caption: '\\SI{}{}', + snippet: '\\SI{$1}{$2}', + meta: 'siunitx-cmd', + score: 0.04233098901537305, + }, + { + caption: '\\sisetup{}', + snippet: '\\sisetup{$1}', + meta: 'siunitx-cmd', + score: 0.0011875061630332172, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'siunitx-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'siunitx-cmd', + score: 0.0063276692758974925, + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'siunitx-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'siunitx-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'siunitx-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'siunitx-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'siunitx-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'siunitx-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'siunitx-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'siunitx-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'siunitx-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'siunitx-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'siunitx-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'siunitx-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'siunitx-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'siunitx-cmd', + score: 0.2864294797053033, + }, + ], + adjustbox: [ + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'adjustbox-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'adjustbox-cmd', + score: 0.354445763583904, + }, + { + caption: '\\adjustbox{}{}', + snippet: '\\adjustbox{$1}{$2}', + meta: 'adjustbox-cmd', + score: 0.002008185536556013, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'adjustbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'adjustbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'adjustbox-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'adjustbox-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'adjustbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'adjustbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'adjustbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'adjustbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'adjustbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'adjustbox-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'adjustbox-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'adjustbox-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'adjustbox-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'adjustbox-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'adjustbox-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'adjustbox-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'adjustbox-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'adjustbox-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'adjustbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'adjustbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'adjustbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'adjustbox-cmd', + score: 0.004649150613625593, + }, + ], + moderncvcompatibility: [ + { + caption: '\\cvitem{}{}', + snippet: '\\cvitem{$1}{$2}', + meta: 'moderncvcompatibility-cmd', + score: 0.19605476980016281, + }, + { + caption: '\\cvlanguage{}{}{}', + snippet: '\\cvlanguage{$1}{$2}{$3}', + meta: 'moderncvcompatibility-cmd', + score: 0.00832363305853651, + }, + { + caption: '\\moderncvtheme[]{}', + snippet: '\\moderncvtheme[$1]{$2}', + meta: 'moderncvcompatibility-cmd', + score: 0.002355125248305291, + }, + { + caption: '\\moderncvtheme{}', + snippet: '\\moderncvtheme{$1}', + meta: 'moderncvcompatibility-cmd', + score: 0.002355125248305291, + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'moderncvcompatibility-cmd', + score: 0.7504160124360846, + }, + { + caption: '\\phone[]{}', + snippet: '\\phone[$1]{$2}', + meta: 'moderncvcompatibility-cmd', + score: 0.09602264063533228, + }, + { + caption: '\\moderncvstyle{}', + snippet: '\\moderncvstyle{$1}', + meta: 'moderncvcompatibility-cmd', + score: 0.09378844125415692, + }, + { + caption: '\\firstname{}', + snippet: '\\firstname{$1}', + meta: 'moderncvcompatibility-cmd', + score: 0.0070031590875754435, + }, + { + caption: '\\cvline{}{}', + snippet: '\\cvline{$1}{$2}', + meta: 'moderncvcompatibility-cmd', + score: 0.007378490468121007, + }, + { + caption: '\\mobile{}', + snippet: '\\mobile{$1}', + meta: 'moderncvcompatibility-cmd', + score: 0.022907406369946367, + }, + { + caption: '\\familyname{}', + snippet: '\\familyname{$1}', + meta: 'moderncvcompatibility-cmd', + score: 0.0070031590875754435, + }, + { + caption: '\\section{}', + snippet: '\\section{$1}', + meta: 'moderncvcompatibility-cmd', + score: 3.0952612541683835, + }, + ], + helvet: [ + { + caption: '\\sfdefault', + snippet: '\\sfdefault', + meta: 'helvet-cmd', + score: 0.008427383388519996, + }, + { + caption: '\\sfdefault{}', + snippet: '\\sfdefault{$1}', + meta: 'helvet-cmd', + score: 0.008427383388519996, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'helvet-cmd', + score: 0.00037306820619479756, + }, + ], + placeins: [ + { + caption: '\\FloatBarrier', + snippet: '\\FloatBarrier', + meta: 'placeins-cmd', + score: 0.015841933780270347, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'placeins-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'placeins-cmd', + score: 0.021170869458413965, + }, + ], + colortbl: [ + { + caption: '\\rowcolor{}', + snippet: '\\rowcolor{$1}', + meta: 'colortbl-cmd', + score: 0.05564476491638024, + }, + { + caption: '\\rowcolor[]{}', + snippet: '\\rowcolor[$1]{$2}', + meta: 'colortbl-cmd', + score: 0.05564476491638024, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'colortbl-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'colortbl-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\arrayrulecolor{}', + snippet: '\\arrayrulecolor{$1}', + meta: 'colortbl-cmd', + score: 0.008538501902241319, + }, + { + caption: '\\arrayrulecolor[]{}', + snippet: '\\arrayrulecolor[$1]{$2}', + meta: 'colortbl-cmd', + score: 0.008538501902241319, + }, + { + caption: '\\hline', + snippet: '\\hline', + meta: 'colortbl-cmd', + score: 1.3209538327406387, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'colortbl-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\cellcolor[]{}', + snippet: '\\cellcolor[$1]{$2}', + meta: 'colortbl-cmd', + score: 0.11068275858524645, + }, + { + caption: '\\cellcolor{}', + snippet: '\\cellcolor{$1}', + meta: 'colortbl-cmd', + score: 0.11068275858524645, + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'colortbl-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'colortbl-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'colortbl-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'colortbl-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'colortbl-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'colortbl-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'colortbl-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'colortbl-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'colortbl-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'colortbl-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'colortbl-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'colortbl-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'colortbl-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'colortbl-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'colortbl-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'colortbl-cmd', + score: 0.2864294797053033, + }, + ], + appendix: [ + { + caption: '\\appendixpagename', + snippet: '\\appendixpagename', + meta: 'appendix-cmd', + score: 0.0005082989114039268, + }, + { + caption: '\\appendixpagename{}', + snippet: '\\appendixpagename{$1}', + meta: 'appendix-cmd', + score: 0.0005082989114039268, + }, + { + caption: '\\thechapter', + snippet: '\\thechapter', + meta: 'appendix-cmd', + score: 0.011821300392639589, + }, + { + caption: '\\sectionmark', + snippet: '\\sectionmark', + meta: 'appendix-cmd', + score: 0.005008938879210868, + }, + { + caption: '\\thesubsection', + snippet: '\\thesubsection', + meta: 'appendix-cmd', + score: 0.004364729212023423, + }, + { + caption: '\\appendixname', + snippet: '\\appendixname', + meta: 'appendix-cmd', + score: 0.006491295958752496, + }, + { + caption: '\\appendixname{}', + snippet: '\\appendixname{$1}', + meta: 'appendix-cmd', + score: 0.006491295958752496, + }, + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'appendix-cmd', + score: 0.07503475348393239, + }, + { + caption: '\\thesection', + snippet: '\\thesection', + meta: 'appendix-cmd', + score: 0.011068945893347528, + }, + { + caption: '\\thesection{}', + snippet: '\\thesection{$1}', + meta: 'appendix-cmd', + score: 0.011068945893347528, + }, + { + caption: '\\appendixpage', + snippet: '\\appendixpage', + meta: 'appendix-cmd', + score: 0.0003193786370376004, + }, + { + caption: '\\appendixpage{}', + snippet: '\\appendixpage{$1}', + meta: 'appendix-cmd', + score: 0.0003193786370376004, + }, + { + caption: '\\appendixtocname', + snippet: '\\appendixtocname', + meta: 'appendix-cmd', + score: 0.0005082989114039268, + }, + { + caption: '\\appendixtocname{}', + snippet: '\\appendixtocname{$1}', + meta: 'appendix-cmd', + score: 0.0005082989114039268, + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'appendix-cmd', + score: 0.0174633138331273, + }, + ], + supertabular: [ + { + caption: '\\tabletail{}', + snippet: '\\tabletail{$1}', + meta: 'supertabular-cmd', + score: 0.00284734590996941, + }, + { + caption: '\\tablehead{}', + snippet: '\\tablehead{$1}', + meta: 'supertabular-cmd', + score: 0.002940437317353234, + }, + { + caption: '\\tablelasttail{}', + snippet: '\\tablelasttail{$1}', + meta: 'supertabular-cmd', + score: 0.00284734590996941, + }, + { + caption: '\\tablefirsthead{}', + snippet: '\\tablefirsthead{$1}', + meta: 'supertabular-cmd', + score: 0.00284734590996941, + }, + ], + makeidx: [ + { + caption: '\\printindex', + snippet: '\\printindex', + meta: 'makeidx-cmd', + score: 0.004417016910870522, + }, + ], + framed: [ + { + caption: '\\fbox{}', + snippet: '\\fbox{$1}', + meta: 'framed-cmd', + score: 0.020865450075016792, + }, + ], + layaureo: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'layaureo-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'layaureo-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'layaureo-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'layaureo-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'layaureo-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'layaureo-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'layaureo-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'layaureo-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'layaureo-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'layaureo-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\savegeometry{}', + snippet: '\\savegeometry{$1}', + meta: 'layaureo-cmd', + score: 6.461638865465447e-5, + }, + { + caption: '\\loadgeometry{}', + snippet: '\\loadgeometry{$1}', + meta: 'layaureo-cmd', + score: 6.461638865465447e-5, + }, + { + caption: '\\newgeometry{}', + snippet: '\\newgeometry{$1}', + meta: 'layaureo-cmd', + score: 0.0025977479207639352, + }, + { + caption: '\\geometry{}', + snippet: '\\geometry{$1}', + meta: 'layaureo-cmd', + score: 0.046218420429973615, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'layaureo-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\restoregeometry', + snippet: '\\restoregeometry', + meta: 'layaureo-cmd', + score: 0.0007546303842143648, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'layaureo-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'layaureo-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'layaureo-cmd', + score: 0.002958865219480927, + }, + ], + keyval: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'keyval-cmd', + score: 0.00037306820619479756, + }, + ], + physics: [ + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'physics-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'physics-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\curl{}', + snippet: '\\curl{$1}', + meta: 'physics-cmd', + score: 0.001039136354388696, + }, + { + caption: '\\curl', + snippet: '\\curl', + meta: 'physics-cmd', + score: 0.001039136354388696, + }, + { + caption: '\\dd', + snippet: '\\dd', + meta: 'physics-cmd', + score: 0.0049652819784537965, + }, + { + caption: '\\expval{}', + snippet: '\\expval{$1}', + meta: 'physics-cmd', + score: 0.0006729185293892782, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'physics-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'physics-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\mqty', + snippet: '\\mqty', + meta: 'physics-cmd', + score: 0.0002048562866401335, + }, + { + caption: '\\order{}', + snippet: '\\order{$1}', + meta: 'physics-cmd', + score: 0.00019980403788140113, + }, + { + caption: '\\order', + snippet: '\\order', + meta: 'physics-cmd', + score: 0.00019980403788140113, + }, + { + caption: '\\abs{}', + snippet: '\\abs{$1}', + meta: 'physics-cmd', + score: 0.016268920166928613, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'physics-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'physics-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\dv{}{}', + snippet: '\\dv{$1}{$2}', + meta: 'physics-cmd', + score: 0.005139463745615663, + }, + { + caption: '\\dv[]{}{}', + snippet: '\\dv[$1]{$2}{$3}', + meta: 'physics-cmd', + score: 0.005139463745615663, + }, + { + caption: '\\eval{}', + snippet: '\\eval{$1}', + meta: 'physics-cmd', + score: 0.00021313621676565867, + }, + { + caption: '\\eval', + snippet: '\\eval', + meta: 'physics-cmd', + score: 0.00021313621676565867, + }, + { + caption: '\\eval[]{}', + snippet: '\\eval[$1]{$2}', + meta: 'physics-cmd', + score: 0.00021313621676565867, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'physics-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'physics-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ket{}', + snippet: '\\ket{$1}', + meta: 'physics-cmd', + score: 0.0326276280979336, + }, + { + caption: '\\mel{}{}{}', + snippet: '\\mel{$1}{$2}{$3}', + meta: 'physics-cmd', + score: 0.001123156900573353, + }, + { + caption: '\\ip', + snippet: '\\ip', + meta: 'physics-cmd', + score: 0.0008534664860896849, + }, + { + caption: '\\ip{}{}', + snippet: '\\ip{$1}{$2}', + meta: 'physics-cmd', + score: 0.0008534664860896849, + }, + { + caption: '\\ip[]{}', + snippet: '\\ip[$1]{$2}', + meta: 'physics-cmd', + score: 0.0008534664860896849, + }, + { + caption: '\\Im', + snippet: '\\Im', + meta: 'physics-cmd', + score: 0.0013451768070134808, + }, + { + caption: '\\Im{}', + snippet: '\\Im{$1}', + meta: 'physics-cmd', + score: 0.0013451768070134808, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'physics-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'physics-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\comm{}{}', + snippet: '\\comm{$1}{$2}', + meta: 'physics-cmd', + score: 0.0012026610554672049, + }, + { + caption: '\\qty', + snippet: '\\qty', + meta: 'physics-cmd', + score: 0.0017737618641299655, + }, + { + caption: '\\qty{}', + snippet: '\\qty{$1}', + meta: 'physics-cmd', + score: 0.0017737618641299655, + }, + { + caption: '\\Tr', + snippet: '\\Tr', + meta: 'physics-cmd', + score: 0.004615158124783136, + }, + { + caption: '\\Tr{}', + snippet: '\\Tr{$1}', + meta: 'physics-cmd', + score: 0.004615158124783136, + }, + { + caption: '\\bra{}', + snippet: '\\bra{$1}', + meta: 'physics-cmd', + score: 0.005609763332417241, + }, + { + caption: '\\poissonbracket{}{}', + snippet: '\\poissonbracket{$1}{$2}', + meta: 'physics-cmd', + score: 2.2761809626681494e-5, + }, + { + caption: '\\pmat{}', + snippet: '\\pmat{$1}', + meta: 'physics-cmd', + score: 0.00010356789132354732, + }, + { + caption: '\\norm{}', + snippet: '\\norm{$1}', + meta: 'physics-cmd', + score: 0.006576610603906938, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'physics-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'physics-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cross', + snippet: '\\cross', + meta: 'physics-cmd', + score: 0.0005412940211650938, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'physics-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\dmat{}', + snippet: '\\dmat{$1}', + meta: 'physics-cmd', + score: 2.2761809626681494e-5, + }, + { + caption: '\\Re', + snippet: '\\Re', + meta: 'physics-cmd', + score: 0.0031525922563281736, + }, + { + caption: '\\Re{}', + snippet: '\\Re{$1}', + meta: 'physics-cmd', + score: 0.0031525922563281736, + }, + { + caption: '\\qq{}', + snippet: '\\qq{$1}', + meta: 'physics-cmd', + score: 8.241282620919185e-5, + }, + { + caption: '\\qq', + snippet: '\\qq', + meta: 'physics-cmd', + score: 8.241282620919185e-5, + }, + { + caption: '\\vb{}', + snippet: '\\vb{$1}', + meta: 'physics-cmd', + score: 0.007377410801695042, + }, + { + caption: '\\pdv{}{}', + snippet: '\\pdv{$1}{$2}', + meta: 'physics-cmd', + score: 0.0014087913646471247, + }, + { + caption: '\\pdv{}{}{}', + snippet: '\\pdv{$1}{$2}{$3}', + meta: 'physics-cmd', + score: 0.0014087913646471247, + }, + { + caption: '\\braket{}{}', + snippet: '\\braket{$1}{$2}', + meta: 'physics-cmd', + score: 0.004421747491186916, + }, + { + caption: '\\braket{}', + snippet: '\\braket{$1}', + meta: 'physics-cmd', + score: 0.004421747491186916, + }, + { + caption: '\\div', + snippet: '\\div', + meta: 'physics-cmd', + score: 0.002403050103349905, + }, + { + caption: '\\div{}', + snippet: '\\div{$1}', + meta: 'physics-cmd', + score: 0.002403050103349905, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'physics-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'physics-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'physics-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'physics-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'physics-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'physics-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'physics-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'physics-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'physics-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'physics-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'physics-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'physics-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'physics-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'physics-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'physics-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'physics-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'physics-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'physics-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'physics-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'physics-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'physics-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'physics-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'physics-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'physics-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'physics-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'physics-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'physics-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'physics-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'physics-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'physics-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'physics-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'physics-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'physics-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'physics-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'physics-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'physics-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'physics-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'physics-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'physics-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'physics-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'physics-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'physics-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'physics-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'physics-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'physics-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'physics-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'physics-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'physics-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'physics-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'physics-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'physics-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'physics-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'physics-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'physics-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'physics-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'physics-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'physics-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'physics-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'physics-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'physics-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'physics-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'physics-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'physics-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'physics-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'physics-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'physics-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'physics-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'physics-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'physics-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'physics-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'physics-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'physics-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'physics-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'physics-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'physics-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'physics-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'physics-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'physics-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'physics-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'physics-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'physics-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'physics-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'physics-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'physics-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'physics-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'physics-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'physics-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'physics-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'physics-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'physics-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'physics-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'physics-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'physics-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'physics-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'physics-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'physics-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'physics-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'physics-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'physics-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'physics-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'physics-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'physics-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'physics-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'physics-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'physics-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'physics-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'physics-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'physics-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'physics-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'physics-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'physics-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'physics-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'physics-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'physics-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'physics-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'physics-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'physics-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'physics-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'physics-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'physics-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'physics-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'physics-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'physics-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'physics-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'physics-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'physics-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'physics-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'physics-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'physics-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'physics-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'physics-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'physics-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'physics-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'physics-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'physics-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'physics-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'physics-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'physics-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'physics-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'physics-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'physics-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'physics-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'physics-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'physics-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'physics-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'physics-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'physics-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'physics-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'physics-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'physics-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'physics-cmd', + score: 0.0063276692758974925, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'physics-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'physics-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'physics-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'physics-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'physics-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'physics-cmd', + score: 0.2864294797053033, + }, + ], + authblk: [ + { + caption: '\\Authfont{}', + snippet: '\\Authfont{$1}', + meta: 'authblk-cmd', + score: 0.00019538157043798684, + }, + { + caption: '\\thanks{}', + snippet: '\\thanks{$1}', + meta: 'authblk-cmd', + score: 0.08382259880654083, + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'authblk-cmd', + score: 0.7504160124360846, + }, + { + caption: '\\rlap{}', + snippet: '\\rlap{$1}', + meta: 'authblk-cmd', + score: 0.01269300721396509, + }, + { + caption: '\\Authands{}', + snippet: '\\Authands{$1}', + meta: 'authblk-cmd', + score: 0.00043932814970131613, + }, + { + caption: '\\author{}', + snippet: '\\author{$1}', + meta: 'authblk-cmd', + score: 0.8973590434087177, + }, + { + caption: '\\author[]{}', + snippet: '\\author[$1]{$2}', + meta: 'authblk-cmd', + score: 0.8973590434087177, + }, + { + caption: '\\textsuperscript{}', + snippet: '\\textsuperscript{$1}', + meta: 'authblk-cmd', + score: 0.05216393882408519, + }, + { + caption: '\\Affilfont{}', + snippet: '\\Affilfont{$1}', + meta: 'authblk-cmd', + score: 0.0004505484831792931, + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'authblk-cmd', + score: 0.2253056071787701, + }, + { + caption: '\\affil[]{}', + snippet: '\\affil[$1]{$2}', + meta: 'authblk-cmd', + score: 0.014174618039587864, + }, + { + caption: '\\affil{}', + snippet: '\\affil{$1}', + meta: 'authblk-cmd', + score: 0.014174618039587864, + }, + ], + tabu: [ + { + caption: '\\extrarowheight', + snippet: '\\extrarowheight', + meta: 'tabu-cmd', + score: 0.003735645243417412, + }, + { + caption: '\\extrarowheight{}', + snippet: '\\extrarowheight{$1}', + meta: 'tabu-cmd', + score: 0.003735645243417412, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'tabu-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'tabu-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\hfill', + snippet: '\\hfill', + meta: 'tabu-cmd', + score: 0.2058248088519886, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'tabu-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'tabu-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\tabulinesep', + snippet: '\\tabulinesep', + meta: 'tabu-cmd', + score: 0.0008256968285249214, + }, + { + caption: '\\hskip', + snippet: '\\hskip', + meta: 'tabu-cmd', + score: 0.04339822811565144, + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'tabu-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'tabu-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'tabu-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'tabu-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'tabu-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tabu-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'tabu-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'tabu-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\par', + snippet: '\\par', + meta: 'tabu-cmd', + score: 0.413853376001159, + }, + ], + CJKutf8: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'CJKutf8-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'CJKutf8-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'CJKutf8-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\selectfont', + snippet: '\\selectfont', + meta: 'CJKutf8-cmd', + score: 0.04598628699063736, + }, + { + caption: '\\inputencoding{}', + snippet: '\\inputencoding{$1}', + meta: 'CJKutf8-cmd', + score: 0.0002447047447770061, + }, + ], + sectsty: [ + { + caption: '\\chapterfont{}', + snippet: '\\chapterfont{$1}', + meta: 'sectsty-cmd', + score: 0.0001572081344977262, + }, + { + caption: '\\raggedright', + snippet: '\\raggedright', + meta: 'sectsty-cmd', + score: 0.05314494127699766, + }, + { + caption: '\\sectionfont{}', + snippet: '\\sectionfont{$1}', + meta: 'sectsty-cmd', + score: 0.003867941482301249, + }, + { + caption: '\\paragraph{}', + snippet: '\\paragraph{$1}', + meta: 'sectsty-cmd', + score: 0.152074250347974, + }, + { + caption: '\\allsectionsfont{}', + snippet: '\\allsectionsfont{$1}', + meta: 'sectsty-cmd', + score: 0.0011367198619746117, + }, + { + caption: '\\subsection{}', + snippet: '\\subsection{$1}', + meta: 'sectsty-cmd', + score: 1.3890912739512353, + }, + { + caption: '\\subsectionfont{}', + snippet: '\\subsectionfont{$1}', + meta: 'sectsty-cmd', + score: 0.002811633808315226, + }, + { + caption: '\\interlinepenalty', + snippet: '\\interlinepenalty', + meta: 'sectsty-cmd', + score: 0.00032069955588347133, + }, + { + caption: '\\subsubsectionfont{}', + snippet: '\\subsubsectionfont{$1}', + meta: 'sectsty-cmd', + score: 0.0011363939259266408, + }, + { + caption: '\\underline{}', + snippet: '\\underline{$1}', + meta: 'sectsty-cmd', + score: 0.14748550887002482, + }, + { + caption: '\\subsubsection{}', + snippet: '\\subsubsection{$1}', + meta: 'sectsty-cmd', + score: 0.3727781330132016, + }, + { + caption: '\\section{}', + snippet: '\\section{$1}', + meta: 'sectsty-cmd', + score: 3.0952612541683835, + }, + ], + lscape: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'lscape-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'lscape-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'lscape-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'lscape-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'lscape-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'lscape-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'lscape-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'lscape-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'lscape-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'lscape-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'lscape-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'lscape-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'lscape-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'lscape-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'lscape-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'lscape-cmd', + score: 0.004649150613625593, + }, + ], + hyphenat: [ + { + caption: '\\hyp{}', + snippet: '\\hyp{$1}', + meta: 'hyphenat-cmd', + score: 0.0013359874951570454, + }, + ], + tocloft: [ + { + caption: '\\cftsecleader', + snippet: '\\cftsecleader', + meta: 'tocloft-cmd', + score: 0.0011340882025681251, + }, + { + caption: '\\cftloftitlefont', + snippet: '\\cftloftitlefont', + meta: 'tocloft-cmd', + score: 6.2350576842596716e-6, + }, + { + caption: '\\cftchappresnum{}', + snippet: '\\cftchappresnum{$1}', + meta: 'tocloft-cmd', + score: 2.8671864736205568e-5, + }, + { + caption: '\\cftchappresnum', + snippet: '\\cftchappresnum', + meta: 'tocloft-cmd', + score: 2.8671864736205568e-5, + }, + { + caption: '\\listoftables', + snippet: '\\listoftables', + meta: 'tocloft-cmd', + score: 0.02104656820469027, + }, + { + caption: '\\cftsecfont{}', + snippet: '\\cftsecfont{$1}', + meta: 'tocloft-cmd', + score: 5.630015640183448e-5, + }, + { + caption: '\\cftchapfont{}', + snippet: '\\cftchapfont{$1}', + meta: 'tocloft-cmd', + score: 6.253521408609416e-5, + }, + { + caption: '\\cftchapfont', + snippet: '\\cftchapfont', + meta: 'tocloft-cmd', + score: 6.253521408609416e-5, + }, + { + caption: '\\cftsubsecleader', + snippet: '\\cftsubsecleader', + meta: 'tocloft-cmd', + score: 1.0644172549700836e-5, + }, + { + caption: '\\cftchapleader', + snippet: '\\cftchapleader', + meta: 'tocloft-cmd', + score: 1.0644172549700836e-5, + }, + { + caption: '\\tocloftpagestyle{}', + snippet: '\\tocloftpagestyle{$1}', + meta: 'tocloft-cmd', + score: 8.392451158032374e-5, + }, + { + caption: '\\cfttoctitlefont', + snippet: '\\cfttoctitlefont', + meta: 'tocloft-cmd', + score: 6.877027177035383e-5, + }, + { + caption: '\\cftdot', + snippet: '\\cftdot', + meta: 'tocloft-cmd', + score: 1.6201749367686227e-5, + }, + { + caption: '\\cftsecdotsep', + snippet: '\\cftsecdotsep', + meta: 'tocloft-cmd', + score: 0.0029383990986223767, + }, + { + caption: '\\cftafterloftitle', + snippet: '\\cftafterloftitle', + meta: 'tocloft-cmd', + score: 6.2350576842596716e-6, + }, + { + caption: '\\listoffigures', + snippet: '\\listoffigures', + meta: 'tocloft-cmd', + score: 0.03447318897846567, + }, + { + caption: '\\cftdotfill{}', + snippet: '\\cftdotfill{$1}', + meta: 'tocloft-cmd', + score: 0.006027562229085753, + }, + { + caption: '\\tableofcontents', + snippet: '\\tableofcontents', + meta: 'tocloft-cmd', + score: 0.13360595130994957, + }, + { + caption: '\\cftdotsep', + snippet: '\\cftdotsep', + meta: 'tocloft-cmd', + score: 0.003089163130463376, + }, + { + caption: '\\numberline{}', + snippet: '\\numberline{$1}', + meta: 'tocloft-cmd', + score: 0.007461440567272885, + }, + { + caption: '\\cftlottitlefont', + snippet: '\\cftlottitlefont', + meta: 'tocloft-cmd', + score: 6.2350576842596716e-6, + }, + { + caption: '\\cftchappagefont{}', + snippet: '\\cftchappagefont{$1}', + meta: 'tocloft-cmd', + score: 5.630015640183448e-5, + }, + { + caption: '\\cftsetindents{}{}{}', + snippet: '\\cftsetindents{$1}{$2}{$3}', + meta: 'tocloft-cmd', + score: 0.00043647269161217853, + }, + { + caption: '\\cftsecpagefont{}', + snippet: '\\cftsecpagefont{$1}', + meta: 'tocloft-cmd', + score: 5.630015640183448e-5, + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'tocloft-cmd', + score: 0.0174633138331273, + }, + { + caption: '\\cftaftertoctitle', + snippet: '\\cftaftertoctitle', + meta: 'tocloft-cmd', + score: 6.2350576842596716e-6, + }, + { + caption: '\\cftafterlottitle', + snippet: '\\cftafterlottitle', + meta: 'tocloft-cmd', + score: 6.2350576842596716e-6, + }, + { + caption: '\\newlistof{}{}{}', + snippet: '\\newlistof{$1}{$2}{$3}', + meta: 'tocloft-cmd', + score: 0.0005381264966408724, + }, + ], + glossaries: [ + { + caption: '\\glslongpluralkey', + snippet: '\\glslongpluralkey', + meta: 'glossaries-cmd', + score: 1.4538687447297259e-5, + }, + { + caption: '\\Glspl{}', + snippet: '\\Glspl{$1}', + meta: 'glossaries-cmd', + score: 0.0025291265119320736, + }, + { + caption: '\\glossarysection', + snippet: '\\glossarysection', + meta: 'glossaries-cmd', + score: 9.579755294730752e-5, + }, + { + caption: '\\printglossaries', + snippet: '\\printglossaries', + meta: 'glossaries-cmd', + score: 0.0010106582768889887, + }, + { + caption: '\\Gls{}', + snippet: '\\Gls{$1}', + meta: 'glossaries-cmd', + score: 0.003696678698317109, + }, + { + caption: '\\setglossarystyle{}', + snippet: '\\setglossarystyle{$1}', + meta: 'glossaries-cmd', + score: 0.0003758893277679221, + }, + { + caption: '\\printglossary', + snippet: '\\printglossary', + meta: 'glossaries-cmd', + score: 0.009139682306158714, + }, + { + caption: '\\printglossary[]', + snippet: '\\printglossary[$1]', + meta: 'glossaries-cmd', + score: 0.009139682306158714, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'glossaries-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\setglossarysection{}', + snippet: '\\setglossarysection{$1}', + meta: 'glossaries-cmd', + score: 3.6081414102781514e-5, + }, + { + caption: '\\glsresetall', + snippet: '\\glsresetall', + meta: 'glossaries-cmd', + score: 0.0006123462672467326, + }, + { + caption: '\\the', + snippet: '\\the', + meta: 'glossaries-cmd', + score: 0.007238960303946444, + }, + { + caption: '\\acrshort{}', + snippet: '\\acrshort{$1}', + meta: 'glossaries-cmd', + score: 0.009936841864059727, + }, + { + caption: '\\printnoidxglossary[]', + snippet: '\\printnoidxglossary[$1]', + meta: 'glossaries-cmd', + score: 0.00021912375285685037, + }, + { + caption: '\\newglossary{}{}', + snippet: '\\newglossary{$1}{$2}', + meta: 'glossaries-cmd', + score: 1.4547244650032571e-5, + }, + { + caption: '\\gls{}', + snippet: '\\gls{$1}', + meta: 'glossaries-cmd', + score: 0.06939353309055077, + }, + { + caption: '\\printnoidxglossaries', + snippet: '\\printnoidxglossaries', + meta: 'glossaries-cmd', + score: 5.6789564226023136e-5, + }, + { + caption: '\\printindex', + snippet: '\\printindex', + meta: 'glossaries-cmd', + score: 0.004417016910870522, + }, + { + caption: '\\defglsentryfmt[]{}', + snippet: '\\defglsentryfmt[$1]{$2}', + meta: 'glossaries-cmd', + score: 4.8990621725283124e-5, + }, + { + caption: '\\glspostdescription', + snippet: '\\glspostdescription', + meta: 'glossaries-cmd', + score: 0.0006337376579591112, + }, + { + caption: '\\number', + snippet: '\\number', + meta: 'glossaries-cmd', + score: 0.000968714260809983, + }, + { + caption: '\\glsaddall', + snippet: '\\glsaddall', + meta: 'glossaries-cmd', + score: 0.0008363820557740373, + }, + { + caption: '\\glsaddall[]', + snippet: '\\glsaddall[$1]', + meta: 'glossaries-cmd', + score: 0.0008363820557740373, + }, + { + caption: '\\makeglossaries', + snippet: '\\makeglossaries', + meta: 'glossaries-cmd', + score: 0.0056737600836936995, + }, + { + caption: '\\glossaryname', + snippet: '\\glossaryname', + meta: 'glossaries-cmd', + score: 0.0006174536302752427, + }, + { + caption: '\\newglossaryentry{}{}', + snippet: '\\newglossaryentry{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.018524394136900962, + }, + { + caption: '\\glslabel', + snippet: '\\glslabel', + meta: 'glossaries-cmd', + score: 4.8990621725283124e-5, + }, + { + caption: '\\glsadd{}', + snippet: '\\glsadd{$1}', + meta: 'glossaries-cmd', + score: 3.0150373480213892e-5, + }, + { + caption: '\\makenoidxglossaries', + snippet: '\\makenoidxglossaries', + meta: 'glossaries-cmd', + score: 0.0001382210125680805, + }, + { + caption: '\\glsgenentryfmt', + snippet: '\\glsgenentryfmt', + meta: 'glossaries-cmd', + score: 4.8990621725283124e-5, + }, + { + caption: '\\acronymtype', + snippet: '\\acronymtype', + meta: 'glossaries-cmd', + score: 0.002000834271117562, + }, + { + caption: '\\acrfull{}', + snippet: '\\acrfull{$1}', + meta: 'glossaries-cmd', + score: 0.0032622587277765067, + }, + { + caption: '\\newacronym{}{}{}', + snippet: '\\newacronym{$1}{$2}{$3}', + meta: 'glossaries-cmd', + score: 0.03193935544723102, + }, + { + caption: '\\glspl{}', + snippet: '\\glspl{$1}', + meta: 'glossaries-cmd', + score: 0.0034025897522047717, + }, + { + caption: '\\ifglsused{}{}{}', + snippet: '\\ifglsused{$1}{$2}{$3}', + meta: 'glossaries-cmd', + score: 4.8990621725283124e-5, + }, + { + caption: '\\acrlong{}', + snippet: '\\acrlong{$1}', + meta: 'glossaries-cmd', + score: 0.002517821598213752, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'glossaries-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'glossaries-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'glossaries-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'glossaries-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'glossaries-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'glossaries-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'glossaries-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'glossaries-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'glossaries-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'glossaries-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'glossaries-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'glossaries-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'glossaries-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'glossaries-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'glossaries-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'glossaries-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'glossaries-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'glossaries-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'glossaries-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'glossaries-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'glossaries-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'glossaries-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'glossaries-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'glossaries-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'glossaries-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'glossaries-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'glossaries-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'glossaries-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'glossaries-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'glossaries-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'glossaries-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'glossaries-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'glossaries-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'glossaries-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'glossaries-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'glossaries-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'glossaries-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'glossaries-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'glossaries-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'glossaries-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'glossaries-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'glossaries-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'glossaries-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'glossaries-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'glossaries-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'glossaries-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'glossaries-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'glossaries-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'glossaries-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'glossaries-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'glossaries-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'glossaries-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'glossaries-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'glossaries-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'glossaries-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'glossaries-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'glossaries-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'glossaries-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'glossaries-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'glossaries-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'glossaries-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'glossaries-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'glossaries-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'glossaries-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'glossaries-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'glossaries-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'glossaries-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'glossaries-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'glossaries-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'glossaries-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'glossaries-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'glossaries-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'glossaries-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'glossaries-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'glossaries-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'glossaries-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'glossaries-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'glossaries-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'glossaries-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'glossaries-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'glossaries-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'glossaries-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'glossaries-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'glossaries-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'glossaries-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'glossaries-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'glossaries-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'glossaries-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'glossaries-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'glossaries-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'glossaries-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'glossaries-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'glossaries-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'glossaries-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'glossaries-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'glossaries-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'glossaries-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'glossaries-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'glossaries-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'glossaries-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'glossaries-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'glossaries-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'glossaries-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'glossaries-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'glossaries-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'glossaries-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'glossaries-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'glossaries-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'glossaries-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'glossaries-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'glossaries-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'glossaries-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'glossaries-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'glossaries-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'glossaries-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'glossaries-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'glossaries-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'glossaries-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'glossaries-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'glossaries-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'glossaries-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'glossaries-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'glossaries-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'glossaries-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'glossaries-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'glossaries-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'glossaries-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'glossaries-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'glossaries-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'glossaries-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'glossaries-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'glossaries-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'glossaries-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'glossaries-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'glossaries-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'glossaries-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'glossaries-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'glossaries-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'glossaries-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'glossaries-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'glossaries-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'glossaries-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'glossaries-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'glossaries-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'glossaries-cmd', + score: 2.341195220791228, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'glossaries-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'glossaries-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'glossaries-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'glossaries-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'glossaries-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'glossaries-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'glossaries-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'glossaries-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'glossaries-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'glossaries-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'glossaries-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'glossaries-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'glossaries-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'glossaries-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'glossaries-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'glossaries-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'glossaries-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'glossaries-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'glossaries-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'glossaries-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'glossaries-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'glossaries-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'glossaries-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'glossaries-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'glossaries-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'glossaries-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'glossaries-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'glossaries-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'glossaries-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'glossaries-cmd', + score: 0.0063276692758974925, + }, + ], + cleveref: [ + { + caption: '\\crefdefaultlabelformat{}', + snippet: '\\crefdefaultlabelformat{$1}', + meta: 'cleveref-cmd', + score: 8.401009062000455e-6, + }, + { + caption: '\\crefname{}{}{}', + snippet: '\\crefname{$1}{$2}{$3}', + meta: 'cleveref-cmd', + score: 0.0016963440482621792, + }, + { + caption: '\\crefrangeformat{}{}', + snippet: '\\crefrangeformat{$1}{$2}', + meta: 'cleveref-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'cleveref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'cleveref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\crefmultiformat{}{}', + snippet: '\\crefmultiformat{$1}{$2}', + meta: 'cleveref-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\crefformat{}{}', + snippet: '\\crefformat{$1}{$2}', + meta: 'cleveref-cmd', + score: 0.0006776840671975755, + }, + { + caption: '\\Cref{}', + snippet: '\\Cref{$1}', + meta: 'cleveref-cmd', + score: 0.0016649686371949341, + }, + { + caption: '\\refstepcounter{}', + snippet: '\\refstepcounter{$1}', + meta: 'cleveref-cmd', + score: 0.002140559856649122, + }, + { + caption: '\\cref{}', + snippet: '\\cref{$1}', + meta: 'cleveref-cmd', + score: 0.0159491058092361, + }, + { + caption: '\\crefrangeconjunction', + snippet: '\\crefrangeconjunction', + meta: 'cleveref-cmd', + score: 3.2405622997778076e-6, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'cleveref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\creflabelformat{}{}', + snippet: '\\creflabelformat{$1}{$2}', + meta: 'cleveref-cmd', + score: 0.000997031755478214, + }, + { + caption: '\\Crefname{}{}{}', + snippet: '\\Crefname{$1}{$2}{$3}', + meta: 'cleveref-cmd', + score: 0.000239288793927364, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'cleveref-cmd', + score: 1.897791904799601, + }, + { + caption: '\\labelcref{}', + snippet: '\\labelcref{$1}', + meta: 'cleveref-cmd', + score: 6.720807249600364e-5, + }, + { + caption: '\\creflastconjunction', + snippet: '\\creflastconjunction', + meta: 'cleveref-cmd', + score: 3.2405622997778076e-6, + }, + ], + 'eso-pic': [ + { + caption: '\\AddToShipoutPictureFG{}', + snippet: '\\AddToShipoutPictureFG{$1}', + meta: 'eso-pic-cmd', + score: 0.000325977535138643, + }, + { + caption: '\\AddToShipoutPictureBG{}', + snippet: '\\AddToShipoutPictureBG{$1}', + meta: 'eso-pic-cmd', + score: 0.0008957666085644653, + }, + { + caption: '\\AtPageUpperLeft{}', + snippet: '\\AtPageUpperLeft{$1}', + meta: 'eso-pic-cmd', + score: 0.0003608141410278152, + }, + { + caption: '\\LenToUnit{}', + snippet: '\\LenToUnit{$1}', + meta: 'eso-pic-cmd', + score: 0.0007216282820556304, + }, + { + caption: '\\AddToShipoutPicture{}', + snippet: '\\AddToShipoutPicture{$1}', + meta: 'eso-pic-cmd', + score: 0.0017658629469099734, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'eso-pic-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'eso-pic-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'eso-pic-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'eso-pic-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'eso-pic-cmd', + score: 0.00047530324346933345, + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'eso-pic-cmd', + score: 0.0005277905480209891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'eso-pic-cmd', + score: 0.008565354665444157, + }, + ], + mhchem: [ + { + caption: '\\ce{}', + snippet: '\\ce{$1}', + meta: 'mhchem-cmd', + score: 0.04246600383063094, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mhchem-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mhchem-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'mhchem-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'mhchem-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'mhchem-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'mhchem-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mhchem-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'mhchem-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mhchem-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'mhchem-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'mhchem-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'mhchem-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'mhchem-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'mhchem-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'mhchem-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'mhchem-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'mhchem-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'mhchem-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'mhchem-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'mhchem-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'mhchem-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'mhchem-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'mhchem-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'mhchem-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'mhchem-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'mhchem-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'mhchem-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'mhchem-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'mhchem-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'mhchem-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'mhchem-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'mhchem-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'mhchem-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'mhchem-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'mhchem-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'mhchem-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'mhchem-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'mhchem-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'mhchem-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'mhchem-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'mhchem-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'mhchem-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'mhchem-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'mhchem-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'mhchem-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'mhchem-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'mhchem-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'mhchem-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'mhchem-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'mhchem-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'mhchem-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'mhchem-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'mhchem-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'mhchem-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'mhchem-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'mhchem-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'mhchem-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'mhchem-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'mhchem-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'mhchem-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'mhchem-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'mhchem-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'mhchem-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'mhchem-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'mhchem-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'mhchem-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'mhchem-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'mhchem-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'mhchem-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'mhchem-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'mhchem-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'mhchem-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'mhchem-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'mhchem-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'mhchem-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'mhchem-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'mhchem-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'mhchem-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'mhchem-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'mhchem-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'mhchem-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'mhchem-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'mhchem-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'mhchem-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'mhchem-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'mhchem-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'mhchem-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'mhchem-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'mhchem-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'mhchem-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'mhchem-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'mhchem-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'mhchem-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'mhchem-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'mhchem-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'mhchem-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'mhchem-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'mhchem-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'mhchem-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'mhchem-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'mhchem-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'mhchem-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'mhchem-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'mhchem-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'mhchem-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'mhchem-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'mhchem-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'mhchem-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'mhchem-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'mhchem-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'mhchem-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'mhchem-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'mhchem-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'mhchem-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'mhchem-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'mhchem-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'mhchem-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'mhchem-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'mhchem-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'mhchem-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'mhchem-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'mhchem-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'mhchem-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'mhchem-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'mhchem-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'mhchem-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'mhchem-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'mhchem-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'mhchem-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'mhchem-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'mhchem-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'mhchem-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'mhchem-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'mhchem-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'mhchem-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'mhchem-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'mhchem-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'mhchem-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'mhchem-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'mhchem-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'mhchem-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'mhchem-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'mhchem-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'mhchem-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'mhchem-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'mhchem-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'mhchem-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'mhchem-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'mhchem-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mhchem-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'mhchem-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'mhchem-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'mhchem-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'mhchem-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'mhchem-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'mhchem-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mhchem-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mhchem-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'mhchem-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'mhchem-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'mhchem-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mhchem-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'mhchem-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'mhchem-cmd', + score: 0.0063276692758974925, + }, + ], + amscd: [ + { + caption: '\\tag{}', + snippet: '\\tag{$1}', + meta: 'amscd-cmd', + score: 0.00784357461002059, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'amscd-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'amscd-cmd', + score: 0.0063276692758974925, + }, + ], + 'unicode-math': [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'unicode-math-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'unicode-math-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'unicode-math-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'unicode-math-cmd', + score: 0.2864294797053033, + }, + ], + ifxetex: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'ifxetex-cmd', + score: 0.00021116765384691477, + }, + ], + newtxmath: [ + { + caption: '\\int', + snippet: '\\int', + meta: 'newtxmath-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\sqrt{}', + snippet: '\\sqrt{$1}', + meta: 'newtxmath-cmd', + score: 0.20240160977404634, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'newtxmath-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\hbar', + snippet: '\\hbar', + meta: 'newtxmath-cmd', + score: 0.024733493787737763, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'newtxmath-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\surd', + snippet: '\\surd', + meta: 'newtxmath-cmd', + score: 0.002159694087964359, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'newtxmath-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'newtxmath-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'newtxmath-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'newtxmath-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\vdots', + snippet: '\\vdots', + meta: 'newtxmath-cmd', + score: 0.03669355896719803, + }, + { + caption: '\\ddots', + snippet: '\\ddots', + meta: 'newtxmath-cmd', + score: 0.010831382784078964, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'newtxmath-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'newtxmath-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'newtxmath-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'newtxmath-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'newtxmath-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'newtxmath-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'newtxmath-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'newtxmath-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'newtxmath-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'newtxmath-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'newtxmath-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'newtxmath-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'newtxmath-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'newtxmath-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'newtxmath-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'newtxmath-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'newtxmath-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'newtxmath-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'newtxmath-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'newtxmath-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'newtxmath-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'newtxmath-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'newtxmath-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'newtxmath-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'newtxmath-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'newtxmath-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'newtxmath-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'newtxmath-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'newtxmath-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'newtxmath-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'newtxmath-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'newtxmath-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'newtxmath-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'newtxmath-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'newtxmath-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'newtxmath-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'newtxmath-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'newtxmath-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'newtxmath-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'newtxmath-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'newtxmath-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'newtxmath-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'newtxmath-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'newtxmath-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'newtxmath-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'newtxmath-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'newtxmath-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'newtxmath-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'newtxmath-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'newtxmath-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'newtxmath-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'newtxmath-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'newtxmath-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'newtxmath-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'newtxmath-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'newtxmath-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'newtxmath-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'newtxmath-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'newtxmath-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'newtxmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'newtxmath-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'newtxmath-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'newtxmath-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'newtxmath-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'newtxmath-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'newtxmath-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'newtxmath-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'newtxmath-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'newtxmath-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'newtxmath-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'newtxmath-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'newtxmath-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'newtxmath-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'newtxmath-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'newtxmath-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'newtxmath-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'newtxmath-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'newtxmath-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'newtxmath-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'newtxmath-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'newtxmath-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'newtxmath-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'newtxmath-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'newtxmath-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'newtxmath-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'newtxmath-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'newtxmath-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'newtxmath-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'newtxmath-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'newtxmath-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'newtxmath-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'newtxmath-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'newtxmath-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'newtxmath-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'newtxmath-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'newtxmath-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'newtxmath-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'newtxmath-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'newtxmath-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'newtxmath-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'newtxmath-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'newtxmath-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'newtxmath-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'newtxmath-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'newtxmath-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'newtxmath-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'newtxmath-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'newtxmath-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'newtxmath-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'newtxmath-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'newtxmath-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'newtxmath-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'newtxmath-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'newtxmath-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'newtxmath-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'newtxmath-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'newtxmath-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'newtxmath-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'newtxmath-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'newtxmath-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'newtxmath-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'newtxmath-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'newtxmath-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'newtxmath-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'newtxmath-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'newtxmath-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'newtxmath-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'newtxmath-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'newtxmath-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'newtxmath-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'newtxmath-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'newtxmath-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'newtxmath-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'newtxmath-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'newtxmath-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'newtxmath-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'newtxmath-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'newtxmath-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'newtxmath-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'newtxmath-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'newtxmath-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'newtxmath-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'newtxmath-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'newtxmath-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'newtxmath-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'newtxmath-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'newtxmath-cmd', + score: 0.0063276692758974925, + }, + ], + pdflscape: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pdflscape-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'pdflscape-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pdflscape-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pdflscape-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pdflscape-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pdflscape-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pdflscape-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pdflscape-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pdflscape-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pdflscape-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pdflscape-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pdflscape-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pdflscape-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pdflscape-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pdflscape-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pdflscape-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pdflscape-cmd', + score: 0.004649150613625593, + }, + ], + apacite: [ + { + caption: '\\citep{}', + snippet: '\\citep{$1}', + meta: 'apacite-cmd', + score: 0.2941882834697057, + }, + { + caption: '\\citet{}', + snippet: '\\citet{$1}', + meta: 'apacite-cmd', + score: 0.09046048561361801, + }, + { + caption: '\\url{}', + snippet: '\\url{$1}', + meta: 'apacite-cmd', + score: 0.13586474005868793, + }, + { + caption: '\\BPGS', + snippet: '\\BPGS', + meta: 'apacite-cmd', + score: 0.00023651453263545777, + }, + { + caption: '\\shortcite{}', + snippet: '\\shortcite{$1}', + meta: 'apacite-cmd', + score: 0.010082057767216608, + }, + { + caption: '\\shortciteA{}', + snippet: '\\shortciteA{$1}', + meta: 'apacite-cmd', + score: 0.0011019769466422762, + }, + { + caption: '\\nocite{}', + snippet: '\\nocite{$1}', + meta: 'apacite-cmd', + score: 0.04990693820960752, + }, + { + caption: '\\refname', + snippet: '\\refname', + meta: 'apacite-cmd', + score: 0.006490238196722249, + }, + { + caption: '\\refname{}', + snippet: '\\refname{$1}', + meta: 'apacite-cmd', + score: 0.006490238196722249, + }, + { + caption: '\\citeA{}', + snippet: '\\citeA{$1}', + meta: 'apacite-cmd', + score: 0.008470555729707068, + }, + { + caption: '\\citeyear{}', + snippet: '\\citeyear{$1}', + meta: 'apacite-cmd', + score: 0.01091041305836494, + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'apacite-cmd', + score: 2.341195220791228, + }, + { + caption: '\\bibliography{}', + snippet: '\\bibliography{$1}', + meta: 'apacite-cmd', + score: 0.2659628337907604, + }, + { + caption: '\\BPG', + snippet: '\\BPG', + meta: 'apacite-cmd', + score: 0.00023651453263545777, + }, + { + caption: '\\citeNP{}', + snippet: '\\citeNP{$1}', + meta: 'apacite-cmd', + score: 0.0003168688289795556, + }, + { + caption: '\\citeauthor{}', + snippet: '\\citeauthor{$1}', + meta: 'apacite-cmd', + score: 0.01359248786373484, + }, + ], + mathpazo: [ + { + caption: '\\big', + snippet: '\\big', + meta: 'mathpazo-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\mathbb{}', + snippet: '\\mathbb{$1}', + meta: 'mathpazo-cmd', + score: 0.33740449739178857, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'mathpazo-cmd', + score: 0.050370758781422345, + }, + ], + footmisc: [ + { + caption: '\\footref{}', + snippet: '\\footref{$1}', + meta: 'footmisc-cmd', + score: 0.0003680857021151614, + }, + { + caption: '\\footref', + snippet: '\\footref', + meta: 'footmisc-cmd', + score: 0.0003680857021151614, + }, + { + caption: '\\protect', + snippet: '\\protect', + meta: 'footmisc-cmd', + score: 0.0200686676229443, + }, + { + caption: '\\multfootsep', + snippet: '\\multfootsep', + meta: 'footmisc-cmd', + score: 0.00010171098214158578, + }, + { + caption: '\\footnotelayout', + snippet: '\\footnotelayout', + meta: 'footmisc-cmd', + score: 0.0004535003423927585, + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'footmisc-cmd', + score: 0.2253056071787701, + }, + { + caption: '\\footnotemark[]', + snippet: '\\footnotemark[$1]', + meta: 'footmisc-cmd', + score: 0.021473212893597875, + }, + { + caption: '\\footnotemark', + snippet: '\\footnotemark', + meta: 'footmisc-cmd', + score: 0.021473212893597875, + }, + { + caption: '\\thefootnote', + snippet: '\\thefootnote', + meta: 'footmisc-cmd', + score: 0.007676927812687567, + }, + { + caption: '\\thefootnote{}', + snippet: '\\thefootnote{$1}', + meta: 'footmisc-cmd', + score: 0.007676927812687567, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'footmisc-cmd', + score: 0.1789117552185788, + }, + ], + fixltx2e: [ + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'fixltx2e-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'fixltx2e-cmd', + score: 0.354445763583904, + }, + { + caption: '\\textsubscript{}', + snippet: '\\textsubscript{$1}', + meta: 'fixltx2e-cmd', + score: 0.058405875394131175, + }, + { + caption: '\\em', + snippet: '\\em', + meta: 'fixltx2e-cmd', + score: 0.10357353994640862, + }, + ], + sidecap: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'sidecap-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'sidecap-cmd', + score: 1.2569477427490174, + }, + { + caption: '\\sidecaptionvpos{}{}', + snippet: '\\sidecaptionvpos{$1}{$2}', + meta: 'sidecap-cmd', + score: 0.0006587927449241846, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'sidecap-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'sidecap-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'sidecap-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'sidecap-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'sidecap-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'sidecap-cmd', + score: 0.0018957469739775527, + }, + ], + nomencl: [ + { + caption: '\\nomenclature[]{}{}', + snippet: '\\nomenclature[$1]{$2}{$3}', + meta: 'nomencl-cmd', + score: 0.016053526743355948, + }, + { + caption: '\\nomenclature{}{}', + snippet: '\\nomenclature{$1}{$2}', + meta: 'nomencl-cmd', + score: 0.016053526743355948, + }, + { + caption: '\\nomlabel', + snippet: '\\nomlabel', + meta: 'nomencl-cmd', + score: 6.353668036093916e-5, + }, + { + caption: '\\printnomenclature', + snippet: '\\printnomenclature', + meta: 'nomencl-cmd', + score: 0.0014526113324237952, + }, + { + caption: '\\printnomenclature[]', + snippet: '\\printnomenclature[$1]', + meta: 'nomencl-cmd', + score: 0.0014526113324237952, + }, + { + caption: '\\makenomenclature', + snippet: '\\makenomenclature', + meta: 'nomencl-cmd', + score: 0.002310610204652063, + }, + { + caption: '\\nomgroup', + snippet: '\\nomgroup', + meta: 'nomencl-cmd', + score: 0.0005549290951493257, + }, + { + caption: '\\nomgroup[]{}', + snippet: '\\nomgroup[$1]{$2}', + meta: 'nomencl-cmd', + score: 0.0005549290951493257, + }, + { + caption: '\\nomname', + snippet: '\\nomname', + meta: 'nomencl-cmd', + score: 0.0015092617929470952, + }, + { + caption: '\\nompreamble', + snippet: '\\nompreamble', + meta: 'nomencl-cmd', + score: 2.4350510995473236e-5, + }, + { + caption: '\\nomentryend', + snippet: '\\nomentryend', + meta: 'nomencl-cmd', + score: 0.000137692304514793, + }, + ], + afterpage: [ + { + caption: '\\afterpage{}', + snippet: '\\afterpage{$1}', + meta: 'afterpage-cmd', + score: 0.0018578070791608345, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'afterpage-cmd', + score: 0.1789117552185788, + }, + ], + titling: [ + { + caption: '\\thanks{}', + snippet: '\\thanks{$1}', + meta: 'titling-cmd', + score: 0.08382259880654083, + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'titling-cmd', + score: 0.7504160124360846, + }, + { + caption: '\\posttitle{}', + snippet: '\\posttitle{$1}', + meta: 'titling-cmd', + score: 0.002507149245154055, + }, + { + caption: '\\postdate{}', + snippet: '\\postdate{$1}', + meta: 'titling-cmd', + score: 0.002139478682489868, + }, + { + caption: '\\predate{}', + snippet: '\\predate{$1}', + meta: 'titling-cmd', + score: 0.002139478682489868, + }, + { + caption: '\\preauthor{}', + snippet: '\\preauthor{$1}', + meta: 'titling-cmd', + score: 0.0023736543205198435, + }, + { + caption: '\\postauthor{}', + snippet: '\\postauthor{$1}', + meta: 'titling-cmd', + score: 0.0023736543205198435, + }, + { + caption: '\\pretitle{}', + snippet: '\\pretitle{$1}', + meta: 'titling-cmd', + score: 0.002507149245154055, + }, + ], + wasysym: [ + { + caption: '\\checked', + snippet: '\\checked', + meta: 'wasysym-cmd', + score: 0.0027792832228568255, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'wasysym-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\diameter', + snippet: '\\diameter', + meta: 'wasysym-cmd', + score: 0.0001645367385856751, + }, + { + caption: '\\CIRCLE', + snippet: '\\CIRCLE', + meta: 'wasysym-cmd', + score: 0.000250667024953401, + }, + ], + eurosym: [ + { + caption: '\\EUR{}', + snippet: '\\EUR{$1}', + meta: 'eurosym-cmd', + score: 3.661595357097087e-5, + }, + ], + caption2: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'caption2-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\DeclareCaptionJustification{}{}', + snippet: '\\DeclareCaptionJustification{$1}{$2}', + meta: 'caption2-cmd', + score: 0.0001872850414971473, + }, + { + caption: '\\DeclareCaptionLabelSeparator{}{}', + snippet: '\\DeclareCaptionLabelSeparator{$1}{$2}', + meta: 'caption2-cmd', + score: 0.0003890810058478364, + }, + { + caption: '\\DeclareCaptionFormat{}{}', + snippet: '\\DeclareCaptionFormat{$1}{$2}', + meta: 'caption2-cmd', + score: 0.0004717618449370015, + }, + { + caption: '\\DeclareCaptionFont{}{}', + snippet: '\\DeclareCaptionFont{$1}{$2}', + meta: 'caption2-cmd', + score: 5.0133404990680195e-5, + }, + { + caption: '\\DeclareCaptionSubType[]{}', + snippet: '\\DeclareCaptionSubType[$1]{$2}', + meta: 'caption2-cmd', + score: 0.0001872850414971473, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'caption2-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'caption2-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'caption2-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'caption2-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'caption2-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\DeclareCaptionType{}[][]', + snippet: '\\DeclareCaptionType{$1}[$2][$3]', + meta: 'caption2-cmd', + score: 0.00015256647321237863, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'caption2-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'caption2-cmd', + score: 0.2253056071787701, + }, + { + caption: '\\footnotemark[]', + snippet: '\\footnotemark[$1]', + meta: 'caption2-cmd', + score: 0.021473212893597875, + }, + { + caption: '\\footnotemark', + snippet: '\\footnotemark', + meta: 'caption2-cmd', + score: 0.021473212893597875, + }, + ], + amsbsy: [ + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'amsbsy-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'amsbsy-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'amsbsy-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'amsbsy-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'amsbsy-cmd', + score: 0.0063276692758974925, + }, + ], + CJK: [ + { + caption: '\\selectfont', + snippet: '\\selectfont', + meta: 'CJK-cmd', + score: 0.04598628699063736, + }, + ], + makecell: [ + { + caption: '\\diaghead{}{}{}', + snippet: '\\diaghead{$1}{$2}{$3}', + meta: 'makecell-cmd', + score: 2.0417817976377812e-5, + }, + { + caption: '\\makecell{}', + snippet: '\\makecell{$1}', + meta: 'makecell-cmd', + score: 0.005023670619810683, + }, + { + caption: '\\makecell[]{}', + snippet: '\\makecell[$1]{$2}', + meta: 'makecell-cmd', + score: 0.005023670619810683, + }, + { + caption: '\\height', + snippet: '\\height', + meta: 'makecell-cmd', + score: 0.0045883162478394055, + }, + { + caption: '\\height{}', + snippet: '\\height{$1}', + meta: 'makecell-cmd', + score: 0.0045883162478394055, + }, + { + caption: '\\setcellgapes{}', + snippet: '\\setcellgapes{$1}', + meta: 'makecell-cmd', + score: 0.0004960838428758984, + }, + { + caption: '\\thead{}', + snippet: '\\thead{$1}', + meta: 'makecell-cmd', + score: 0.0023087638254186797, + }, + { + caption: '\\Gape[]', + snippet: '\\Gape[$1]', + meta: 'makecell-cmd', + score: 0.000469300371741866, + }, + { + caption: '\\theadgape{}', + snippet: '\\theadgape{$1}', + meta: 'makecell-cmd', + score: 0.000234650185870933, + }, + { + caption: '\\theadalign', + snippet: '\\theadalign', + meta: 'makecell-cmd', + score: 0.0006746935448099005, + }, + { + caption: '\\theadalign{}', + snippet: '\\theadalign{$1}', + meta: 'makecell-cmd', + score: 0.0006746935448099005, + }, + { + caption: '\\theadset{}', + snippet: '\\theadset{$1}', + meta: 'makecell-cmd', + score: 0.0004400433589389675, + }, + { + caption: '\\Xhline{}', + snippet: '\\Xhline{$1}', + meta: 'makecell-cmd', + score: 0.0024175651338281096, + }, + { + caption: '\\theadfont{}', + snippet: '\\theadfont{$1}', + meta: 'makecell-cmd', + score: 0.0007935193556772338, + }, + { + caption: '\\theadfont', + snippet: '\\theadfont', + meta: 'makecell-cmd', + score: 0.0007935193556772338, + }, + { + caption: '\\cellgape{}', + snippet: '\\cellgape{$1}', + meta: 'makecell-cmd', + score: 0.000234650185870933, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'makecell-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'makecell-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\makegapedcells', + snippet: '\\makegapedcells', + meta: 'makecell-cmd', + score: 0.000431467454221244, + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'makecell-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'makecell-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'makecell-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'makecell-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'makecell-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'makecell-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'makecell-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'makecell-cmd', + score: 0.018615449342361392, + }, + ], + xeCJK: [ + { + caption: '\\setCJKmonofont{}', + snippet: '\\setCJKmonofont{$1}', + meta: 'xeCJK-cmd', + score: 0.0057178353252375245, + }, + { + caption: '\\setCJKmainfont{}', + snippet: '\\setCJKmainfont{$1}', + meta: 'xeCJK-cmd', + score: 0.006622926778590894, + }, + { + caption: '\\setCJKmainfont[]{}', + snippet: '\\setCJKmainfont[$1]{$2}', + meta: 'xeCJK-cmd', + score: 0.006622926778590894, + }, + { + caption: '\\setCJKsansfont{}', + snippet: '\\setCJKsansfont{$1}', + meta: 'xeCJK-cmd', + score: 0.0057178353252375245, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xeCJK-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xeCJK-cmd', + score: 0.2864294797053033, + }, + ], + threeparttable: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'threeparttable-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'threeparttable-cmd', + score: 3.800886892251021, + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'threeparttable-cmd', + score: 3.800886892251021, + }, + ], + dirtytalk: [ + { + caption: '\\say{}', + snippet: '\\say{$1}', + meta: 'dirtytalk-cmd', + score: 0.010246289746417045, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'dirtytalk-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'dirtytalk-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'dirtytalk-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'dirtytalk-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'dirtytalk-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'dirtytalk-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'dirtytalk-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'dirtytalk-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'dirtytalk-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'dirtytalk-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'dirtytalk-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'dirtytalk-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'dirtytalk-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'dirtytalk-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'dirtytalk-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'dirtytalk-cmd', + score: 0.021170869458413965, + }, + ], + balance: [ + { + caption: '\\balance', + snippet: '\\balance', + meta: 'balance-cmd', + score: 0.003629066156300264, + }, + { + caption: '\\balance{}', + snippet: '\\balance{$1}', + meta: 'balance-cmd', + score: 0.003629066156300264, + }, + ], + minted: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'minted-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'minted-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\usemintedstyle{}', + snippet: '\\usemintedstyle{$1}', + meta: 'minted-cmd', + score: 0.00184279823796158, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\inputminted[]{}{}', + snippet: '\\inputminted[$1]{$2}{$3}', + meta: 'minted-cmd', + score: 0.0016501519191680601, + }, + { + caption: '\\inputminted{}{}', + snippet: '\\inputminted{$1}{$2}', + meta: 'minted-cmd', + score: 0.0016501519191680601, + }, + { + caption: '\\setminted[]{}', + snippet: '\\setminted[$1]{$2}', + meta: 'minted-cmd', + score: 0.0004017914210172805, + }, + { + caption: '\\setminted{}', + snippet: '\\setminted{$1}', + meta: 'minted-cmd', + score: 0.0004017914210172805, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'minted-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'minted-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'minted-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'minted-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'minted-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'minted-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'minted-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'minted-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\listof{}{}', + snippet: '\\listof{$1}{$2}', + meta: 'minted-cmd', + score: 0.0009837365348002915, + }, + { + caption: '\\floatplacement{}{}', + snippet: '\\floatplacement{$1}{$2}', + meta: 'minted-cmd', + score: 0.0005815474978918903, + }, + { + caption: '\\restylefloat{}', + snippet: '\\restylefloat{$1}', + meta: 'minted-cmd', + score: 0.0008866338267686714, + }, + { + caption: '\\floatstyle{}', + snippet: '\\floatstyle{$1}', + meta: 'minted-cmd', + score: 0.0015470917047414941, + }, + { + caption: '\\floatname{}{}', + snippet: '\\floatname{$1}{$2}', + meta: 'minted-cmd', + score: 0.0011934321931750752, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'minted-cmd', + score: 1.2569477427490174, + }, + { + caption: '\\newfloat{}{}{}', + snippet: '\\newfloat{$1}{$2}{$3}', + meta: 'minted-cmd', + score: 0.0012745874472536625, + }, + { + caption: '\\newfloat', + snippet: '\\newfloat', + meta: 'minted-cmd', + score: 0.0012745874472536625, + }, + { + caption: '\\newfloat{}', + snippet: '\\newfloat{$1}', + meta: 'minted-cmd', + score: 0.0012745874472536625, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'minted-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'minted-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'minted-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'minted-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'minted-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'minted-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'minted-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'minted-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'minted-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'minted-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'minted-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'minted-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'minted-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'minted-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'minted-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'minted-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'minted-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fbox{}', + snippet: '\\fbox{$1}', + meta: 'minted-cmd', + score: 0.020865450075016792, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'minted-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'minted-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'minted-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\pagewiselinenumbers', + snippet: '\\pagewiselinenumbers', + meta: 'minted-cmd', + score: 0.00016870831850106035, + }, + { + caption: '\\linenomath', + snippet: '\\linenomath', + meta: 'minted-cmd', + score: 1.4517338420208715e-5, + }, + { + caption: '\\linenumberfont{}', + snippet: '\\linenumberfont{$1}', + meta: 'minted-cmd', + score: 0.0001811784338695797, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'minted-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'minted-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\endlinenomath', + snippet: '\\endlinenomath', + meta: 'minted-cmd', + score: 1.4517338420208715e-5, + }, + { + caption: '\\nolinenumbers', + snippet: '\\nolinenumbers', + meta: 'minted-cmd', + score: 0.0009805246614299932, + }, + { + caption: '\\path', + snippet: '\\path', + meta: 'minted-cmd', + score: 0.028200474217322108, + }, + { + caption: '\\path[]', + snippet: '\\path[$1]', + meta: 'minted-cmd', + score: 0.028200474217322108, + }, + { + caption: '\\path{}', + snippet: '\\path{$1}', + meta: 'minted-cmd', + score: 0.028200474217322108, + }, + { + caption: '\\filedate{}', + snippet: '\\filedate{$1}', + meta: 'minted-cmd', + score: 0.000578146635331119, + }, + { + caption: '\\filedate', + snippet: '\\filedate', + meta: 'minted-cmd', + score: 0.000578146635331119, + }, + { + caption: '\\linenumbers', + snippet: '\\linenumbers', + meta: 'minted-cmd', + score: 0.004687680659497865, + }, + { + caption: '\\modulolinenumbers[]', + snippet: '\\modulolinenumbers[$1]', + meta: 'minted-cmd', + score: 0.0027194991933605197, + }, + { + caption: '\\fileversion{}', + snippet: '\\fileversion{$1}', + meta: 'minted-cmd', + score: 0.000578146635331119, + }, + { + caption: '\\fileversion', + snippet: '\\fileversion', + meta: 'minted-cmd', + score: 0.000578146635331119, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'minted-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'minted-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\refstepcounter{}', + snippet: '\\refstepcounter{$1}', + meta: 'minted-cmd', + score: 0.002140559856649122, + }, + { + caption: '\\VerbatimEnvironment', + snippet: '\\VerbatimEnvironment', + meta: 'minted-cmd', + score: 4.5350034239275855e-5, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\fvset{}', + snippet: '\\fvset{$1}', + meta: 'minted-cmd', + score: 0.00015476887282479622, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'minted-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'minted-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'minted-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'minted-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'minted-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'minted-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'minted-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'minted-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'minted-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'minted-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'minted-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'minted-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'minted-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'minted-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'minted-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'minted-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'minted-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'minted-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'minted-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'minted-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'minted-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'minted-cmd', + score: 0.008565354665444157, + }, + ], + xifthen: [ + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'xifthen-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'xifthen-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'xifthen-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'xifthen-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'xifthen-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'xifthen-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'xifthen-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'xifthen-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'xifthen-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'xifthen-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'xifthen-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'xifthen-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xifthen-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xifthen-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'xifthen-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'xifthen-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'xifthen-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'xifthen-cmd', + score: 0.028955796305270766, + }, + ], + relsize: [ + { + caption: '\\mathlarger{}', + snippet: '\\mathlarger{$1}', + meta: 'relsize-cmd', + score: 0.0031475241540308316, + }, + { + caption: '\\smaller', + snippet: '\\smaller', + meta: 'relsize-cmd', + score: 0.001271007880944704, + }, + ], + epsf: [ + { + caption: '\\epsfbox{}', + snippet: '\\epsfbox{$1}', + meta: 'epsf-cmd', + score: 0.00013712781345832882, + }, + ], + datetime: [ + { + caption: '\\shortmonthname[]', + snippet: '\\shortmonthname[$1]', + meta: 'datetime-cmd', + score: 0.00018524143860552933, + }, + { + caption: '\\THEYEAR', + snippet: '\\THEYEAR', + meta: 'datetime-cmd', + score: 8.638115929876123e-5, + }, + { + caption: '\\currenttime', + snippet: '\\currenttime', + meta: 'datetime-cmd', + score: 0.0002884868472087627, + }, + { + caption: '\\monthname', + snippet: '\\monthname', + meta: 'datetime-cmd', + score: 8.847106423071211e-5, + }, + { + caption: '\\monthname[]', + snippet: '\\monthname[$1]', + meta: 'datetime-cmd', + score: 8.847106423071211e-5, + }, + { + caption: '\\today', + snippet: '\\today', + meta: 'datetime-cmd', + score: 0.10733849317324783, + }, + { + caption: '\\THEMONTH', + snippet: '\\THEMONTH', + meta: 'datetime-cmd', + score: 8.638115929876123e-5, + }, + { + caption: '\\yyyymmdddate', + snippet: '\\yyyymmdddate', + meta: 'datetime-cmd', + score: 0.0002568405365040184, + }, + { + caption: '\\pdfdate', + snippet: '\\pdfdate', + meta: 'datetime-cmd', + score: 9.673490669434574e-5, + }, + { + caption: '\\dateseparator', + snippet: '\\dateseparator', + meta: 'datetime-cmd', + score: 0.00010966778823652713, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'datetime-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\THEDAY', + snippet: '\\THEDAY', + meta: 'datetime-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\usdate', + snippet: '\\usdate', + meta: 'datetime-cmd', + score: 0.00020980148911330757, + }, + { + caption: '\\newdateformat{}{}', + snippet: '\\newdateformat{$1}{$2}', + meta: 'datetime-cmd', + score: 8.638115929876123e-5, + }, + { + caption: '\\settimeformat{}', + snippet: '\\settimeformat{$1}', + meta: 'datetime-cmd', + score: 0.00010966778823652713, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'datetime-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'datetime-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'datetime-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'datetime-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'datetime-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'datetime-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'datetime-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'datetime-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'datetime-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'datetime-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'datetime-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'datetime-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'datetime-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'datetime-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'datetime-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'datetime-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'datetime-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'datetime-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'datetime-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'datetime-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'datetime-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'datetime-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'datetime-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'datetime-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'datetime-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'datetime-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'datetime-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'datetime-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'datetime-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'datetime-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'datetime-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'datetime-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'datetime-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'datetime-cmd', + score: 0.0063276692758974925, + }, + ], + fontawesome: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'fontawesome-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'fontawesome-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'fontawesome-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'fontawesome-cmd', + score: 0.2864294797053033, + }, + ], + forest: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'forest-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\bracketset{}', + snippet: '\\bracketset{$1}', + meta: 'forest-cmd', + score: 0.00014301574866674164, + }, + { + caption: '\\forestset{}', + snippet: '\\forestset{$1}', + meta: 'forest-cmd', + score: 0.0020596473883671114, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'forest-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'forest-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'forest-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'forest-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'forest-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'forest-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'forest-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'forest-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'forest-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'forest-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'forest-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'forest-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'forest-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'forest-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'forest-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'forest-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'forest-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'forest-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'forest-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'forest-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'forest-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'forest-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'forest-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'forest-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'forest-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'forest-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'forest-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'forest-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'forest-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'forest-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'forest-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'forest-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'forest-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'forest-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'forest-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'forest-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'forest-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'forest-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'forest-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'forest-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'forest-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'forest-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'forest-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'forest-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'forest-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'forest-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'forest-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\reserveinserts{}', + snippet: '\\reserveinserts{$1}', + meta: 'forest-cmd', + score: 0.0018653410309739879, + }, + { + caption: '\\newtoks', + snippet: '\\newtoks', + meta: 'forest-cmd', + score: 0.00031058155311734754, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'forest-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'forest-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'forest-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'forest-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'forest-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'forest-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'forest-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'forest-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'forest-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'forest-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'forest-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'forest-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'forest-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'forest-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'forest-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'forest-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'forest-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'forest-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'forest-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'forest-cmd', + score: 0.2864294797053033, + }, + ], + pgf: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgf-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgf-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgf-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgf-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgf-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgf-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgf-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgf-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgf-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgf-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgf-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgf-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgf-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgf-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgf-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgf-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgf-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgf-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgf-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgf-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgf-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgf-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgf-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgf-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgf-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgf-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgf-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgf-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgf-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgf-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgf-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgf-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgf-cmd', + score: 0.2864294797053033, + }, + ], + pstricks: [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pstricks-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pstricks-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pstricks-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pstricks-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pstricks-cmd', + score: 0.0005786730478266738, + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pstricks-cmd', + score: 0.006520475264573554, + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pstricks-cmd', + score: 0.006520475264573554, + }, + ], + fancybox: [ + { + caption: '\\shadowbox{}', + snippet: '\\shadowbox{$1}', + meta: 'fancybox-cmd', + score: 0.00107667147399019, + }, + { + caption: '\\doublebox', + snippet: '\\doublebox', + meta: 'fancybox-cmd', + score: 0.00015142240898356106, + }, + { + caption: '\\VerbatimEnvironment', + snippet: '\\VerbatimEnvironment', + meta: 'fancybox-cmd', + score: 4.5350034239275855e-5, + }, + { + caption: '\\thisfancypage{}{}', + snippet: '\\thisfancypage{$1}{$2}', + meta: 'fancybox-cmd', + score: 0.00015142240898356106, + }, + { + caption: '\\TheSbox', + snippet: '\\TheSbox', + meta: 'fancybox-cmd', + score: 4.5350034239275855e-5, + }, + ], + braket: [ + { + caption: '\\ket{}', + snippet: '\\ket{$1}', + meta: 'braket-cmd', + score: 0.0326276280979336, + }, + { + caption: '\\braket{}{}', + snippet: '\\braket{$1}{$2}', + meta: 'braket-cmd', + score: 0.004421747491186916, + }, + { + caption: '\\braket{}', + snippet: '\\braket{$1}', + meta: 'braket-cmd', + score: 0.004421747491186916, + }, + { + caption: '\\ketbra{}{}', + snippet: '\\ketbra{$1}{$2}', + meta: 'braket-cmd', + score: 0.0006317858348936015, + }, + { + caption: '\\ketbra', + snippet: '\\ketbra', + meta: 'braket-cmd', + score: 0.0006317858348936015, + }, + { + caption: '\\bra{}', + snippet: '\\bra{$1}', + meta: 'braket-cmd', + score: 0.005609763332417241, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'braket-cmd', + score: 0.008565354665444157, + }, + ], + import: [ + { + caption: '\\import{}{}', + snippet: '\\import{$1}{$2}', + meta: 'import-cmd', + score: 0.1265354812350108, + }, + ], + abntex2cite: [ + { + caption: '\\citeonline{}', + snippet: '\\citeonline{$1}', + meta: 'abntex2cite-cmd', + score: 0.014277840409455324, + }, + { + caption: '\\bibitem{}', + snippet: '\\bibitem{$1}', + meta: 'abntex2cite-cmd', + score: 0.3689547570562042, + }, + { + caption: '\\bibitem[]{}', + snippet: '\\bibitem[$1]{$2}', + meta: 'abntex2cite-cmd', + score: 0.3689547570562042, + }, + { + caption: '\\bibliographystyle{}', + snippet: '\\bibliographystyle{$1}', + meta: 'abntex2cite-cmd', + score: 0.25122317941387773, + }, + { + caption: '\\citeyear{}', + snippet: '\\citeyear{$1}', + meta: 'abntex2cite-cmd', + score: 0.01091041305836494, + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'abntex2cite-cmd', + score: 2.341195220791228, + }, + { + caption: '\\bibliography{}', + snippet: '\\bibliography{$1}', + meta: 'abntex2cite-cmd', + score: 0.2659628337907604, + }, + { + caption: '\\setstretch{}', + snippet: '\\setstretch{$1}', + meta: 'abntex2cite-cmd', + score: 0.019634763572332112, + }, + { + caption: '\\onehalfspacing', + snippet: '\\onehalfspacing', + meta: 'abntex2cite-cmd', + score: 0.010655415521079565, + }, + { + caption: '\\singlespacing', + snippet: '\\singlespacing', + meta: 'abntex2cite-cmd', + score: 0.008351544612280968, + }, + { + caption: '\\doublespacing', + snippet: '\\doublespacing', + meta: 'abntex2cite-cmd', + score: 0.007835428951987135, + }, + { + caption: '\\baselinestretch', + snippet: '\\baselinestretch', + meta: 'abntex2cite-cmd', + score: 0.03225350148161425, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'abntex2cite-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'abntex2cite-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'abntex2cite-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'abntex2cite-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'abntex2cite-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'abntex2cite-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'abntex2cite-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'abntex2cite-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'abntex2cite-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'abntex2cite-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'abntex2cite-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'abntex2cite-cmd', + score: 0.0002854206807593436, + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'abntex2cite-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'abntex2cite-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'abntex2cite-cmd', + score: 0.010515056688180681, + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'abntex2cite-cmd', + score: 0.008041789461944983, + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'abntex2cite-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'abntex2cite-cmd', + score: 0.0032990580087398644, + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'abntex2cite-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'abntex2cite-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'abntex2cite-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'abntex2cite-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'abntex2cite-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'abntex2cite-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'abntex2cite-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'abntex2cite-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'abntex2cite-cmd', + score: 0.0018957469739775527, + }, + ], + isodate: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'isodate-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'isodate-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'isodate-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'isodate-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'isodate-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'isodate-cmd', + score: 0.0018957469739775527, + }, + ], + tcolorbox: [ + { + caption: '\\tcbset{}', + snippet: '\\tcbset{$1}', + meta: 'tcolorbox-cmd', + score: 0.00012246447222402193, + }, + { + caption: '\\tcbuselibrary{}', + snippet: '\\tcbuselibrary{$1}', + meta: 'tcolorbox-cmd', + score: 4.347671035621014e-5, + }, + { + caption: '\\newtcolorbox[]{}[][]{}', + snippet: '\\newtcolorbox[$1]{$2}[$3][$4]{$5}', + meta: 'tcolorbox-cmd', + score: 7.216282820556303e-5, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'tcolorbox-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'tcolorbox-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\newtcbox{}[][]{}', + snippet: '\\newtcbox{$1}[$2][$3]{$4}', + meta: 'tcolorbox-cmd', + score: 3.558785984219631e-5, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tcolorbox-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tcolorbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\endverbatim', + snippet: '\\endverbatim', + meta: 'tcolorbox-cmd', + score: 0.0022216421267780076, + }, + { + caption: '\\verbatim', + snippet: '\\verbatim', + meta: 'tcolorbox-cmd', + score: 0.0072203369120285256, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tcolorbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tcolorbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\par', + snippet: '\\par', + meta: 'tcolorbox-cmd', + score: 0.413853376001159, + }, + { + caption: '\\verbatiminput{}', + snippet: '\\verbatiminput{$1}', + meta: 'tcolorbox-cmd', + score: 0.0024547099784948665, + }, + { + caption: '\\verbatiminput', + snippet: '\\verbatiminput', + meta: 'tcolorbox-cmd', + score: 0.0024547099784948665, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tcolorbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tcolorbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tcolorbox-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tcolorbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tcolorbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tcolorbox-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tcolorbox-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tcolorbox-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tcolorbox-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tcolorbox-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tcolorbox-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tcolorbox-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tcolorbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tcolorbox-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'tcolorbox-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'tcolorbox-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'tcolorbox-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'tcolorbox-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'tcolorbox-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'tcolorbox-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'tcolorbox-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'tcolorbox-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'tcolorbox-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'tcolorbox-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tcolorbox-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'tcolorbox-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'tcolorbox-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tcolorbox-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tcolorbox-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tcolorbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tcolorbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tcolorbox-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tcolorbox-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tcolorbox-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tcolorbox-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tcolorbox-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tcolorbox-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tcolorbox-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tcolorbox-cmd', + score: 0.2864294797053033, + }, + ], + vmargin: [ + { + caption: '\\setmargins{}', + snippet: '\\setmargins{$1}', + meta: 'vmargin-cmd', + score: 3.138510306083217e-5, + }, + { + caption: '\\setmarginsrb{}{}{}{}{}{}{}{}', + snippet: '\\setmarginsrb{$1}{$2}{$3}{$4}{$5}{$6}{$7}{$8}', + meta: 'vmargin-cmd', + score: 0.0004759508676929243, + }, + { + caption: '\\setpapersize{}', + snippet: '\\setpapersize{$1}', + meta: 'vmargin-cmd', + score: 3.138510306083217e-5, + }, + ], + mdframed: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newmdenv[]{}', + snippet: '\\newmdenv[$1]{$2}', + meta: 'mdframed-cmd', + score: 0.0008776774843208122, + }, + { + caption: '\\surroundwithmdframed[]{}', + snippet: '\\surroundwithmdframed[$1]{$2}', + meta: 'mdframed-cmd', + score: 5.535446508489438e-5, + }, + { + caption: '\\newmdtheoremenv{}{}', + snippet: '\\newmdtheoremenv{$1}{$2}', + meta: 'mdframed-cmd', + score: 3.558785984219631e-5, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'mdframed-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mdframed-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mdframed-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'mdframed-cmd', + score: 0.00047530324346933345, + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'mdframed-cmd', + score: 0.0005277905480209891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'mdframed-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'mdframed-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'mdframed-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'mdframed-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'mdframed-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'mdframed-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'mdframed-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'mdframed-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'mdframed-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'mdframed-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mdframed-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'mdframed-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'mdframed-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mdframed-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'mdframed-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'mdframed-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'mdframed-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'mdframed-cmd', + score: 0.0058342578961340175, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'mdframed-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mdframed-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'mdframed-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mdframed-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mdframed-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'mdframed-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'mdframed-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'mdframed-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'mdframed-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'mdframed-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'mdframed-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'mdframed-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'mdframed-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'mdframed-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'mdframed-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'mdframed-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'mdframed-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'mdframed-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'mdframed-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'mdframed-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'mdframed-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'mdframed-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mdframed-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'mdframed-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'mdframed-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'mdframed-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'mdframed-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mdframed-cmd', + score: 0.008565354665444157, + }, + ], + cancel: [ + { + caption: '\\cancel{}', + snippet: '\\cancel{$1}', + meta: 'cancel-cmd', + score: 0.00017782514657538044, + }, + { + caption: '\\cancelto{}{}', + snippet: '\\cancelto{$1}{$2}', + meta: 'cancel-cmd', + score: 7.809089624140706e-5, + }, + ], + textcase: [ + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'textcase-cmd', + score: 2.341195220791228, + }, + ], + libertine: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'libertine-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'libertine-cmd', + score: 0.008565354665444157, + }, + ], + flushend: [ + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'flushend-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'flushend-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'flushend-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'flushend-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'flushend-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'flushend-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'flushend-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'flushend-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'flushend-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'flushend-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'flushend-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'flushend-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'flushend-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'flushend-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'flushend-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'flushend-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'flushend-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'flushend-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'flushend-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'flushend-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'flushend-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'flushend-cmd', + score: 0.008565354665444157, + }, + ], + psfrag: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'psfrag-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'psfrag-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'psfrag-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'psfrag-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'psfrag-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'psfrag-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'psfrag-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'psfrag-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'psfrag-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'psfrag-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'psfrag-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'psfrag-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'psfrag-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'psfrag-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'psfrag-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'psfrag-cmd', + score: 0.004649150613625593, + }, + ], + tablefootnote: [ + { + caption: '\\tablefootnote{}', + snippet: '\\tablefootnote{$1}', + meta: 'tablefootnote-cmd', + score: 0.00017554048326570823, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'tablefootnote-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'tablefootnote-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'tablefootnote-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tablefootnote-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tablefootnote-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'tablefootnote-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'tablefootnote-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'tablefootnote-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'tablefootnote-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tablefootnote-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'tablefootnote-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'tablefootnote-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'tablefootnote-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'tablefootnote-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'tablefootnote-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'tablefootnote-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'tablefootnote-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'tablefootnote-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'tablefootnote-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tablefootnote-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tablefootnote-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tablefootnote-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tablefootnote-cmd', + score: 0.021170869458413965, + }, + ], + amstext: [ + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'amstext-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'amstext-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'amstext-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'amstext-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'amstext-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'amstext-cmd', + score: 0.0063276692758974925, + }, + ], + units: [ + { + caption: '\\unitfrac{}{}', + snippet: '\\unitfrac{$1}{$2}', + meta: 'units-cmd', + score: 0.0009264866770139672, + }, + { + caption: '\\unitfrac[]{}{}', + snippet: '\\unitfrac[$1]{$2}{$3}', + meta: 'units-cmd', + score: 0.0009264866770139672, + }, + { + caption: '\\unit[]{}', + snippet: '\\unit[$1]{$2}', + meta: 'units-cmd', + score: 0.028299796173135428, + }, + { + caption: '\\unit{}', + snippet: '\\unit{$1}', + meta: 'units-cmd', + score: 0.028299796173135428, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'units-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'units-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'units-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'units-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'units-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'units-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\nicefrac{}{}', + snippet: '\\nicefrac{$1}{$2}', + meta: 'units-cmd', + score: 0.0018011350423659288, + }, + ], + scrextend: [ + { + caption: '\\footref{}', + snippet: '\\footref{$1}', + meta: 'scrextend-cmd', + score: 0.0003680857021151614, + }, + { + caption: '\\footref', + snippet: '\\footref', + meta: 'scrextend-cmd', + score: 0.0003680857021151614, + }, + { + caption: '\\scriptsize', + snippet: '\\scriptsize', + meta: 'scrextend-cmd', + score: 0.05550618634921613, + }, + { + caption: '\\scriptsize{}', + snippet: '\\scriptsize{$1}', + meta: 'scrextend-cmd', + score: 0.05550618634921613, + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'scrextend-cmd', + score: 0.7504160124360846, + }, + { + caption: '\\Large', + snippet: '\\Large', + meta: 'scrextend-cmd', + score: 0.1987771081149759, + }, + { + caption: '\\Large{}', + snippet: '\\Large{$1}', + meta: 'scrextend-cmd', + score: 0.1987771081149759, + }, + { + caption: '\\and', + snippet: '\\and', + meta: 'scrextend-cmd', + score: 0.09847866956528724, + }, + { + caption: '\\LARGE', + snippet: '\\LARGE', + meta: 'scrextend-cmd', + score: 0.05947642043953873, + }, + { + caption: '\\LARGE{}', + snippet: '\\LARGE{$1}', + meta: 'scrextend-cmd', + score: 0.05947642043953873, + }, + { + caption: '\\subtitle{}', + snippet: '\\subtitle{$1}', + meta: 'scrextend-cmd', + score: 0.01803265454797817, + }, + { + caption: '\\large', + snippet: '\\large', + meta: 'scrextend-cmd', + score: 0.20377416734108866, + }, + { + caption: '\\large{}', + snippet: '\\large{$1}', + meta: 'scrextend-cmd', + score: 0.20377416734108866, + }, + { + caption: '\\Huge', + snippet: '\\Huge', + meta: 'scrextend-cmd', + score: 0.04725806985998919, + }, + { + caption: '\\footnotesize', + snippet: '\\footnotesize', + meta: 'scrextend-cmd', + score: 0.2038592081252624, + }, + { + caption: '\\footnotesize{}', + snippet: '\\footnotesize{$1}', + meta: 'scrextend-cmd', + score: 0.2038592081252624, + }, + { + caption: '\\small', + snippet: '\\small', + meta: 'scrextend-cmd', + score: 0.2447632045426295, + }, + { + caption: '\\small{}', + snippet: '\\small{$1}', + meta: 'scrextend-cmd', + score: 0.2447632045426295, + }, + { + caption: '\\huge', + snippet: '\\huge', + meta: 'scrextend-cmd', + score: 0.04229832859754922, + }, + { + caption: '\\huge{}', + snippet: '\\huge{$1}', + meta: 'scrextend-cmd', + score: 0.04229832859754922, + }, + { + caption: '\\cleardoublepage', + snippet: '\\cleardoublepage', + meta: 'scrextend-cmd', + score: 0.044016804142963585, + }, + { + caption: '\\tiny{}', + snippet: '\\tiny{$1}', + meta: 'scrextend-cmd', + score: 0.047727606910742924, + }, + { + caption: '\\tiny', + snippet: '\\tiny', + meta: 'scrextend-cmd', + score: 0.047727606910742924, + }, + { + caption: '\\deffootnote[]{}{}{}', + snippet: '\\deffootnote[$1]{$2}{$3}{$4}', + meta: 'scrextend-cmd', + score: 2.545393270896533e-5, + }, + { + caption: '\\thefootnote', + snippet: '\\thefootnote', + meta: 'scrextend-cmd', + score: 0.007676927812687567, + }, + { + caption: '\\thefootnote{}', + snippet: '\\thefootnote{$1}', + meta: 'scrextend-cmd', + score: 0.007676927812687567, + }, + { + caption: '\\normalsize', + snippet: '\\normalsize', + meta: 'scrextend-cmd', + score: 0.14261697855738878, + }, + { + caption: '\\normalsize{}', + snippet: '\\normalsize{$1}', + meta: 'scrextend-cmd', + score: 0.14261697855738878, + }, + { + caption: '\\titlefont', + snippet: '\\titlefont', + meta: 'scrextend-cmd', + score: 0.0005278519180709353, + }, + { + caption: '\\thefootnotemark', + snippet: '\\thefootnotemark', + meta: 'scrextend-cmd', + score: 2.545393270896533e-5, + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'scrextend-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'scrextend-cmd', + score: 0.1789117552185788, + }, + { + caption: '\\addtokomafont{}{}', + snippet: '\\addtokomafont{$1}{$2}', + meta: 'scrextend-cmd', + score: 0.0008555564394100388, + }, + { + caption: '\\setkomafont{}{}', + snippet: '\\setkomafont{$1}{$2}', + meta: 'scrextend-cmd', + score: 0.012985816912639263, + }, + { + caption: '\\KOMAoptions{}', + snippet: '\\KOMAoptions{$1}', + meta: 'scrextend-cmd', + score: 0.000396664302361659, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'scrextend-cmd', + score: 0.00037306820619479756, + }, + ], + mwe: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mwe-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mwe-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'mwe-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'mwe-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'mwe-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'mwe-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'mwe-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mwe-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'mwe-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mwe-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'mwe-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'mwe-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'mwe-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'mwe-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'mwe-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'mwe-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'mwe-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'mwe-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'mwe-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mwe-cmd', + score: 0.008565354665444157, + }, + ], + beamerposter: [ + { + caption: '\\scriptsize', + snippet: '\\scriptsize', + meta: 'beamerposter-cmd', + score: 0.05550618634921613, + }, + { + caption: '\\scriptsize{}', + snippet: '\\scriptsize{$1}', + meta: 'beamerposter-cmd', + score: 0.05550618634921613, + }, + { + caption: '\\Large', + snippet: '\\Large', + meta: 'beamerposter-cmd', + score: 0.1987771081149759, + }, + { + caption: '\\Large{}', + snippet: '\\Large{$1}', + meta: 'beamerposter-cmd', + score: 0.1987771081149759, + }, + { + caption: '\\footnotesize', + snippet: '\\footnotesize', + meta: 'beamerposter-cmd', + score: 0.2038592081252624, + }, + { + caption: '\\footnotesize{}', + snippet: '\\footnotesize{$1}', + meta: 'beamerposter-cmd', + score: 0.2038592081252624, + }, + { + caption: '\\LARGE', + snippet: '\\LARGE', + meta: 'beamerposter-cmd', + score: 0.05947642043953873, + }, + { + caption: '\\LARGE{}', + snippet: '\\LARGE{$1}', + meta: 'beamerposter-cmd', + score: 0.05947642043953873, + }, + { + caption: '\\large', + snippet: '\\large', + meta: 'beamerposter-cmd', + score: 0.20377416734108866, + }, + { + caption: '\\large{}', + snippet: '\\large{$1}', + meta: 'beamerposter-cmd', + score: 0.20377416734108866, + }, + { + caption: '\\VeryHuge', + snippet: '\\VeryHuge', + meta: 'beamerposter-cmd', + score: 0.000892251826639951, + }, + { + caption: '\\small', + snippet: '\\small', + meta: 'beamerposter-cmd', + score: 0.2447632045426295, + }, + { + caption: '\\small{}', + snippet: '\\small{$1}', + meta: 'beamerposter-cmd', + score: 0.2447632045426295, + }, + { + caption: '\\VERYHuge', + snippet: '\\VERYHuge', + meta: 'beamerposter-cmd', + score: 0.0011668714784222325, + }, + { + caption: '\\veryHuge', + snippet: '\\veryHuge', + meta: 'beamerposter-cmd', + score: 0.000892251826639951, + }, + { + caption: '\\normalsize', + snippet: '\\normalsize', + meta: 'beamerposter-cmd', + score: 0.14261697855738878, + }, + { + caption: '\\normalsize{}', + snippet: '\\normalsize{$1}', + meta: 'beamerposter-cmd', + score: 0.14261697855738878, + }, + { + caption: '\\tiny{}', + snippet: '\\tiny{$1}', + meta: 'beamerposter-cmd', + score: 0.047727606910742924, + }, + { + caption: '\\tiny', + snippet: '\\tiny', + meta: 'beamerposter-cmd', + score: 0.047727606910742924, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'beamerposter-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'beamerposter-cmd', + score: 0.021170869458413965, + }, + ], + footnote: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'footnote-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'footnote-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\makesavenoteenv{}', + snippet: '\\makesavenoteenv{$1}', + meta: 'footnote-cmd', + score: 0.0018587414325895479, + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'footnote-cmd', + score: 0.2253056071787701, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'footnote-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\parbox{}{}', + snippet: '\\parbox{$1}{$2}', + meta: 'footnote-cmd', + score: 0.04800611019618169, + }, + ], + invoice: [ + { + caption: '\\Fee{}{}{}', + snippet: '\\Fee{$1}{$2}{$3}', + meta: 'invoice-cmd', + score: 0.003295435821387378, + }, + { + caption: '\\ProjectTitle{}', + snippet: '\\ProjectTitle{$1}', + meta: 'invoice-cmd', + score: 0.003295435821387378, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'invoice-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'invoice-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'invoice-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'invoice-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'invoice-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'invoice-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\endhead', + snippet: '\\endhead', + meta: 'invoice-cmd', + score: 0.0023853501147448834, + }, + { + caption: '\\endfoot', + snippet: '\\endfoot', + meta: 'invoice-cmd', + score: 0.00044045261916551967, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'invoice-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'invoice-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\nopagebreak', + snippet: '\\nopagebreak', + meta: 'invoice-cmd', + score: 9.952664522415981e-5, + }, + { + caption: '\\endfirsthead', + snippet: '\\endfirsthead', + meta: 'invoice-cmd', + score: 0.0016148498709822416, + }, + { + caption: '\\endlastfoot', + snippet: '\\endlastfoot', + meta: 'invoice-cmd', + score: 0.00044045261916551967, + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'invoice-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\tablename', + snippet: '\\tablename', + meta: 'invoice-cmd', + score: 0.0029238994233674776, + }, + { + caption: '\\pagebreak', + snippet: '\\pagebreak', + meta: 'invoice-cmd', + score: 0.0313525090421608, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'invoice-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'invoice-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'invoice-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'invoice-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'invoice-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'invoice-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'invoice-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'invoice-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'invoice-cmd', + score: 0.028955796305270766, + }, + ], + tikzpeople: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzpeople-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'tikzpeople-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikzpeople-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikzpeople-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'tikzpeople-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'tikzpeople-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikzpeople-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikzpeople-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikzpeople-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikzpeople-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikzpeople-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikzpeople-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikzpeople-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzpeople-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikzpeople-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikzpeople-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikzpeople-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikzpeople-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'tikzpeople-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'tikzpeople-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'tikzpeople-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'tikzpeople-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'tikzpeople-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'tikzpeople-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'tikzpeople-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'tikzpeople-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'tikzpeople-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'tikzpeople-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikzpeople-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'tikzpeople-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'tikzpeople-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzpeople-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tikzpeople-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikzpeople-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikzpeople-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tikzpeople-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikzpeople-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tikzpeople-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzpeople-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tikzpeople-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tikzpeople-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikzpeople-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikzpeople-cmd', + score: 0.2864294797053033, + }, + ], + titletoc: [ + { + caption: '\\thecontentspage', + snippet: '\\thecontentspage', + meta: 'titletoc-cmd', + score: 0.0008054115902675176, + }, + { + caption: '\\startcontents', + snippet: '\\startcontents', + meta: 'titletoc-cmd', + score: 0.00026847053008917257, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'titletoc-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'titletoc-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\printcontents{}{}{}', + snippet: '\\printcontents{$1}{$2}{$3}', + meta: 'titletoc-cmd', + score: 0.00013423526504458629, + }, + { + caption: '\\titlecontents{}[]', + snippet: '\\titlecontents{$1}[$2]', + meta: 'titletoc-cmd', + score: 0.0017036290423289926, + }, + { + caption: '\\titlecontents{}[]{}{}{}{}[]', + snippet: '\\titlecontents{$1}[$2]{$3}{$4}{$5}{$6}[$7]', + meta: 'titletoc-cmd', + score: 0.0017036290423289926, + }, + { + caption: '\\titlecontents{}[]{}{}{}{}', + snippet: '\\titlecontents{$1}[$2]{$3}{$4}{$5}{$6}', + meta: 'titletoc-cmd', + score: 0.0017036290423289926, + }, + { + caption: '\\numberline{}', + snippet: '\\numberline{$1}', + meta: 'titletoc-cmd', + score: 0.007461440567272885, + }, + { + caption: '\\dottedcontents{}[]{}{}{}', + snippet: '\\dottedcontents{$1}[$2]{$3}{$4}{$5}', + meta: 'titletoc-cmd', + score: 4.743909531747666e-5, + }, + { + caption: '\\filcenter', + snippet: '\\filcenter', + meta: 'titletoc-cmd', + score: 0.0004835660211260246, + }, + { + caption: '\\thecontentslabel', + snippet: '\\thecontentslabel', + meta: 'titletoc-cmd', + score: 0.0010521864830662522, + }, + { + caption: '\\contentsuse{}{}', + snippet: '\\contentsuse{$1}{$2}', + meta: 'titletoc-cmd', + score: 6.110202388233705e-5, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'titletoc-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\contentspage', + snippet: '\\contentspage', + meta: 'titletoc-cmd', + score: 0.0004955116569277163, + }, + { + caption: '\\contentslabel[]{}', + snippet: '\\contentslabel[$1]{$2}', + meta: 'titletoc-cmd', + score: 0.0011055859582683105, + }, + { + caption: '\\contentslabel{}', + snippet: '\\contentslabel{$1}', + meta: 'titletoc-cmd', + score: 0.0011055859582683105, + }, + { + caption: '\\contentsmargin{}', + snippet: '\\contentsmargin{$1}', + meta: 'titletoc-cmd', + score: 0.00013423526504458629, + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'titletoc-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\titlerule', + snippet: '\\titlerule', + meta: 'titletoc-cmd', + score: 0.019273712561461216, + }, + { + caption: '\\titlerule[]{}', + snippet: '\\titlerule[$1]{$2}', + meta: 'titletoc-cmd', + score: 0.019273712561461216, + }, + ], + dblfloatfix: [ + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'dblfloatfix-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'dblfloatfix-cmd', + score: 0.354445763583904, + }, + { + caption: '\\textsubscript{}', + snippet: '\\textsubscript{$1}', + meta: 'dblfloatfix-cmd', + score: 0.058405875394131175, + }, + { + caption: '\\em', + snippet: '\\em', + meta: 'dblfloatfix-cmd', + score: 0.10357353994640862, + }, + ], + pgfplotstable: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfplotstable-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfplotstable-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfplotstable-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'pgfplotstable-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'pgfplotstable-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'pgfplotstable-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'pgfplotstable-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'pgfplotstable-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfplotstable-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'pgfplotstable-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfplotstable-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfplotstable-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfplotstable-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfplotstable-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfplotstable-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfplotstable-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfplotstable-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfplotstable-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfplotstable-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfplotstable-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfplotstable-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfplotstable-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfplotstable-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfplotstable-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfplotstable-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfplotstable-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfplotstable-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfplotstable-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfplotstable-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfplotstable-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfplotstable-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfplotstable-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfplotstable-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfplotstable-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfplotstable-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfplotstable-cmd', + score: 0.2864294797053033, + }, + ], + acronym: [ + { + caption: '\\acp{}', + snippet: '\\acp{$1}', + meta: 'acronym-cmd', + score: 0.0005185177930914685, + }, + { + caption: '\\acsfont{}', + snippet: '\\acsfont{$1}', + meta: 'acronym-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\aclabelfont', + snippet: '\\aclabelfont', + meta: 'acronym-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\acro{}{}', + snippet: '\\acro{$1}{$2}', + meta: 'acronym-cmd', + score: 0.023587207425038587, + }, + { + caption: '\\acl{}', + snippet: '\\acl{$1}', + meta: 'acronym-cmd', + score: 0.0008131607751426444, + }, + { + caption: '\\acf{}', + snippet: '\\acf{$1}', + meta: 'acronym-cmd', + score: 0.0006845634165950408, + }, + { + caption: '\\acrodef{}[]{}', + snippet: '\\acrodef{$1}[$2]{$3}', + meta: 'acronym-cmd', + score: 0.0002902047200830372, + }, + { + caption: '\\acs{}', + snippet: '\\acs{$1}', + meta: 'acronym-cmd', + score: 0.002351209826598939, + }, + { + caption: '\\acfp{}', + snippet: '\\acfp{$1}', + meta: 'acronym-cmd', + score: 2.2013599341265054e-5, + }, + { + caption: '\\ac{}', + snippet: '\\ac{$1}', + meta: 'acronym-cmd', + score: 0.04714113215364704, + }, + { + caption: '\\let', + snippet: '\\let', + meta: 'acronym-cmd', + score: 0.03789745970461662, + }, + ], + nicefrac: [ + { + caption: '\\nicefrac{}{}', + snippet: '\\nicefrac{$1}{$2}', + meta: 'nicefrac-cmd', + score: 0.0018011350423659288, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'nicefrac-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'nicefrac-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'nicefrac-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'nicefrac-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'nicefrac-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'nicefrac-cmd', + score: 0.0018957469739775527, + }, + ], + smartdiagram: [ + { + caption: '\\usesmartdiagramlibrary{}', + snippet: '\\usesmartdiagramlibrary{$1}', + meta: 'smartdiagram-cmd', + score: 7.216282820556303e-5, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'smartdiagram-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'smartdiagram-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'smartdiagram-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'smartdiagram-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'smartdiagram-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'smartdiagram-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'smartdiagram-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'smartdiagram-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'smartdiagram-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'smartdiagram-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'smartdiagram-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'smartdiagram-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'smartdiagram-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'smartdiagram-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'smartdiagram-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'smartdiagram-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'smartdiagram-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'smartdiagram-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'smartdiagram-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'smartdiagram-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'smartdiagram-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'smartdiagram-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'smartdiagram-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'smartdiagram-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'smartdiagram-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'smartdiagram-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'smartdiagram-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'smartdiagram-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'smartdiagram-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'smartdiagram-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'smartdiagram-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'smartdiagram-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'smartdiagram-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'smartdiagram-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'smartdiagram-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'smartdiagram-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'smartdiagram-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'smartdiagram-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'smartdiagram-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'smartdiagram-cmd', + score: 0.2864294797053033, + }, + ], + qtree: [ + { + caption: '\\qroof{}', + snippet: '\\qroof{$1}', + meta: 'qtree-cmd', + score: 0.00012663929287995903, + }, + { + caption: '\\Tree[]', + snippet: '\\Tree[$1]', + meta: 'qtree-cmd', + score: 0.0008894716589418522, + }, + { + caption: '\\Tree', + snippet: '\\Tree', + meta: 'qtree-cmd', + score: 0.0008894716589418522, + }, + ], + backref: [ + { + caption: '\\backrefpagesname', + snippet: '\\backrefpagesname', + meta: 'backref-cmd', + score: 0.0022756001200686213, + }, + { + caption: '\\backref', + snippet: '\\backref', + meta: 'backref-cmd', + score: 0.0025820187198826706, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'backref-cmd', + score: 0.1789117552185788, + }, + { + caption: '\\global', + snippet: '\\global', + meta: 'backref-cmd', + score: 0.006609629561859019, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'backref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'backref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'backref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'backref-cmd', + score: 0.0058342578961340175, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'backref-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'backref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'backref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'backref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'backref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'backref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'backref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'backref-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'backref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\makeindex', + snippet: '\\makeindex', + meta: 'backref-cmd', + score: 0.010304996748556729, + }, + { + caption: '\\index{}', + snippet: '\\index{$1}', + meta: 'backref-cmd', + score: 0.013774721817648336, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'backref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'backref-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'backref-cmd', + score: 0.008565354665444157, + }, + ], + epigraph: [ + { + caption: '\\epigraphflush{}', + snippet: '\\epigraphflush{$1}', + meta: 'epigraph-cmd', + score: 1.8073688234300064e-5, + }, + { + caption: '\\epigraphsize{}', + snippet: '\\epigraphsize{$1}', + meta: 'epigraph-cmd', + score: 6.820709322498027e-5, + }, + { + caption: '\\epigraphsize', + snippet: '\\epigraphsize', + meta: 'epigraph-cmd', + score: 6.820709322498027e-5, + }, + { + caption: '\\epigraph{}{}', + snippet: '\\epigraph{$1}{$2}', + meta: 'epigraph-cmd', + score: 0.0031428856022970054, + }, + ], + chngcntr: [ + { + caption: '\\counterwithin{}{}', + snippet: '\\counterwithin{$1}{$2}', + meta: 'chngcntr-cmd', + score: 0.001287401394784382, + }, + { + caption: '\\counterwithout{}{}', + snippet: '\\counterwithout{$1}{$2}', + meta: 'chngcntr-cmd', + score: 0.0026127666246546326, + }, + ], + empheq: [ + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'empheq-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'empheq-cmd', + score: 1.897791904799601, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'empheq-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'empheq-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'empheq-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'empheq-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'empheq-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'empheq-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'empheq-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'empheq-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'empheq-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'empheq-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'empheq-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'empheq-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'empheq-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'empheq-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'empheq-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'empheq-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'empheq-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'empheq-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'empheq-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'empheq-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'empheq-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'empheq-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'empheq-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'empheq-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'empheq-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'empheq-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'empheq-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'empheq-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'empheq-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'empheq-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'empheq-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'empheq-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'empheq-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'empheq-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'empheq-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'empheq-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'empheq-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'empheq-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'empheq-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'empheq-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'empheq-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'empheq-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'empheq-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'empheq-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'empheq-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'empheq-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'empheq-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'empheq-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'empheq-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'empheq-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'empheq-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'empheq-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'empheq-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'empheq-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'empheq-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'empheq-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'empheq-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'empheq-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'empheq-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'empheq-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'empheq-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'empheq-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'empheq-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'empheq-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'empheq-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'empheq-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'empheq-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'empheq-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'empheq-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'empheq-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'empheq-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'empheq-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'empheq-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'empheq-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'empheq-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'empheq-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'empheq-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'empheq-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'empheq-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'empheq-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'empheq-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'empheq-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'empheq-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'empheq-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'empheq-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'empheq-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'empheq-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'empheq-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'empheq-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'empheq-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'empheq-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'empheq-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'empheq-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'empheq-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'empheq-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'empheq-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'empheq-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'empheq-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'empheq-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'empheq-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'empheq-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'empheq-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'empheq-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'empheq-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'empheq-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'empheq-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'empheq-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'empheq-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\xleftrightarrow[][]{}', + snippet: '\\xleftrightarrow[$1][$2]{$3}', + meta: 'empheq-cmd', + score: 4.015559489911509e-5, + }, + { + caption: '\\vcentcolon', + snippet: '\\vcentcolon', + meta: 'empheq-cmd', + score: 0.00021361943526711615, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'empheq-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\coloneqq', + snippet: '\\coloneqq', + meta: 'empheq-cmd', + score: 0.0014407293323958122, + }, + { + caption: '\\mathclap{}', + snippet: '\\mathclap{$1}', + meta: 'empheq-cmd', + score: 7.84378567451772e-5, + }, + { + caption: '\\adjustlimits', + snippet: '\\adjustlimits', + meta: 'empheq-cmd', + score: 0.0005307066890271085, + }, + { + caption: '\\MoveEqLeft', + snippet: '\\MoveEqLeft', + meta: 'empheq-cmd', + score: 5.343949980628182e-5, + }, + { + caption: '\\mathrlap{}', + snippet: '\\mathrlap{$1}', + meta: 'empheq-cmd', + score: 0.0003112817211637952, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'empheq-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\xhookrightarrow{}', + snippet: '\\xhookrightarrow{$1}', + meta: 'empheq-cmd', + score: 5.444260823474129e-5, + }, + { + caption: '\\DeclarePairedDelimiter{}{}{}', + snippet: '\\DeclarePairedDelimiter{$1}{$2}{$3}', + meta: 'empheq-cmd', + score: 0.0033916678416372487, + }, + { + caption: '\\DeclarePairedDelimiter', + snippet: '\\DeclarePairedDelimiter', + meta: 'empheq-cmd', + score: 0.0033916678416372487, + }, + { + caption: '\\prescript{}{}{}', + snippet: '\\prescript{$1}{$2}{$3}', + meta: 'empheq-cmd', + score: 8.833369785705982e-6, + }, + { + caption: '\\underbrace{}', + snippet: '\\underbrace{$1}', + meta: 'empheq-cmd', + score: 0.010373780436850907, + }, + { + caption: '\\mathllap{}', + snippet: '\\mathllap{$1}', + meta: 'empheq-cmd', + score: 3.140504277052775e-5, + }, + { + caption: '\\overbrace{}', + snippet: '\\overbrace{$1}', + meta: 'empheq-cmd', + score: 0.0006045704778718376, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'empheq-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'empheq-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'empheq-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'empheq-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'empheq-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'empheq-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'empheq-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'empheq-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'empheq-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'empheq-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'empheq-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'empheq-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'empheq-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'empheq-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'empheq-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'empheq-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'empheq-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'empheq-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'empheq-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'empheq-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'empheq-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'empheq-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'empheq-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'empheq-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'empheq-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'empheq-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'empheq-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'empheq-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'empheq-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'empheq-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'empheq-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'empheq-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'empheq-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'empheq-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'empheq-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'empheq-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'empheq-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'empheq-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'empheq-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'empheq-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'empheq-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'empheq-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'empheq-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'empheq-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'empheq-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'empheq-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'empheq-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'empheq-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'empheq-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'empheq-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'empheq-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'empheq-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'empheq-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'empheq-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'empheq-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'empheq-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'empheq-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'empheq-cmd', + score: 0.0063276692758974925, + }, + ], + mathexam: [ + { + caption: '\\ExamInstrBox{}', + snippet: '\\ExamInstrBox{$1}', + meta: 'mathexam-cmd', + score: 0.00035308240943436196, + }, + { + caption: '\\ExamName{}', + snippet: '\\ExamName{$1}', + meta: 'mathexam-cmd', + score: 0.00165391233892938, + }, + { + caption: '\\ExamNameLine', + snippet: '\\ExamNameLine', + meta: 'mathexam-cmd', + score: 0.00165391233892938, + }, + { + caption: '\\ExamClass{}', + snippet: '\\ExamClass{$1}', + meta: 'mathexam-cmd', + score: 0.00165391233892938, + }, + { + caption: '\\ExamHead{}', + snippet: '\\ExamHead{$1}', + meta: 'mathexam-cmd', + score: 0.00165391233892938, + }, + { + caption: '\\answer{}', + snippet: '\\answer{$1}', + meta: 'mathexam-cmd', + score: 0.0034436236729672894, + }, + { + caption: '\\answer', + snippet: '\\answer', + meta: 'mathexam-cmd', + score: 0.0034436236729672894, + }, + { + caption: '\\lhead{}', + snippet: '\\lhead{$1}', + meta: 'mathexam-cmd', + score: 0.05268978171228714, + }, + { + caption: '\\chaptermark', + snippet: '\\chaptermark', + meta: 'mathexam-cmd', + score: 0.005924520024686584, + }, + { + caption: '\\chaptermark{}', + snippet: '\\chaptermark{$1}', + meta: 'mathexam-cmd', + score: 0.005924520024686584, + }, + { + caption: '\\fancypagestyle{}{}', + snippet: '\\fancypagestyle{$1}{$2}', + meta: 'mathexam-cmd', + score: 0.009430919590937878, + }, + { + caption: '\\footrule', + snippet: '\\footrule', + meta: 'mathexam-cmd', + score: 0.0010032754348913366, + }, + { + caption: '\\footrule{}', + snippet: '\\footrule{$1}', + meta: 'mathexam-cmd', + score: 0.0010032754348913366, + }, + { + caption: '\\fancyfoot[]{}', + snippet: '\\fancyfoot[$1]{$2}', + meta: 'mathexam-cmd', + score: 0.024973618823189894, + }, + { + caption: '\\fancyfoot{}', + snippet: '\\fancyfoot{$1}', + meta: 'mathexam-cmd', + score: 0.024973618823189894, + }, + { + caption: '\\fancyfootoffset[]{}', + snippet: '\\fancyfootoffset[$1]{$2}', + meta: 'mathexam-cmd', + score: 0.0015373246231684555, + }, + { + caption: '\\fancyfootoffset{}', + snippet: '\\fancyfootoffset{$1}', + meta: 'mathexam-cmd', + score: 0.0015373246231684555, + }, + { + caption: '\\footruleskip', + snippet: '\\footruleskip', + meta: 'mathexam-cmd', + score: 0.000830117957327721, + }, + { + caption: '\\fancyheadoffset[]{}', + snippet: '\\fancyheadoffset[$1]{$2}', + meta: 'mathexam-cmd', + score: 0.0016786568695309166, + }, + { + caption: '\\fancyheadoffset{}', + snippet: '\\fancyheadoffset{$1}', + meta: 'mathexam-cmd', + score: 0.0016786568695309166, + }, + { + caption: '\\iffloatpage{}{}', + snippet: '\\iffloatpage{$1}{$2}', + meta: 'mathexam-cmd', + score: 6.606286310833368e-5, + }, + { + caption: '\\cfoot{}', + snippet: '\\cfoot{$1}', + meta: 'mathexam-cmd', + score: 0.013411641301057813, + }, + { + caption: '\\subsectionmark', + snippet: '\\subsectionmark', + meta: 'mathexam-cmd', + score: 3.1153423008593836e-5, + }, + { + caption: '\\footrulewidth', + snippet: '\\footrulewidth', + meta: 'mathexam-cmd', + score: 0.011424740897486949, + }, + { + caption: '\\fancyhfoffset[]{}', + snippet: '\\fancyhfoffset[$1]{$2}', + meta: 'mathexam-cmd', + score: 3.741978601121172e-5, + }, + { + caption: '\\rhead{}', + snippet: '\\rhead{$1}', + meta: 'mathexam-cmd', + score: 0.022782817416731292, + }, + { + caption: '\\fancyplain{}{}', + snippet: '\\fancyplain{$1}{$2}', + meta: 'mathexam-cmd', + score: 0.007402339896386138, + }, + { + caption: '\\rfoot{}', + snippet: '\\rfoot{$1}', + meta: 'mathexam-cmd', + score: 0.013393817825547868, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mathexam-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\plainheadrulewidth', + snippet: '\\plainheadrulewidth', + meta: 'mathexam-cmd', + score: 6.2350576842596716e-6, + }, + { + caption: '\\baselinestretch', + snippet: '\\baselinestretch', + meta: 'mathexam-cmd', + score: 0.03225350148161425, + }, + { + caption: '\\lfoot{}', + snippet: '\\lfoot{$1}', + meta: 'mathexam-cmd', + score: 0.00789399846642229, + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'mathexam-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'mathexam-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\fancyhf{}', + snippet: '\\fancyhf{$1}', + meta: 'mathexam-cmd', + score: 0.02314618933449356, + }, + { + caption: '\\sectionmark', + snippet: '\\sectionmark', + meta: 'mathexam-cmd', + score: 0.005008938879210868, + }, + { + caption: '\\fancyhead[]{}', + snippet: '\\fancyhead[$1]{$2}', + meta: 'mathexam-cmd', + score: 0.039101068064744296, + }, + { + caption: '\\fancyhead{}', + snippet: '\\fancyhead{$1}', + meta: 'mathexam-cmd', + score: 0.039101068064744296, + }, + { + caption: '\\nouppercase{}', + snippet: '\\nouppercase{$1}', + meta: 'mathexam-cmd', + score: 0.006416387071584083, + }, + { + caption: '\\nouppercase', + snippet: '\\nouppercase', + meta: 'mathexam-cmd', + score: 0.006416387071584083, + }, + { + caption: '\\headrule', + snippet: '\\headrule', + meta: 'mathexam-cmd', + score: 0.0008327432627715623, + }, + { + caption: '\\headrule{}', + snippet: '\\headrule{$1}', + meta: 'mathexam-cmd', + score: 0.0008327432627715623, + }, + { + caption: '\\chead{}', + snippet: '\\chead{$1}', + meta: 'mathexam-cmd', + score: 0.00755042164734884, + }, + { + caption: '\\headrulewidth', + snippet: '\\headrulewidth', + meta: 'mathexam-cmd', + score: 0.02268137935335823, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'mathexam-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'mathexam-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'mathexam-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'mathexam-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'mathexam-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'mathexam-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'mathexam-cmd', + score: 0.001042697111754002, + }, + ], + floatrow: [ + { + caption: '\\floatfoot{}', + snippet: '\\floatfoot{$1}', + meta: 'floatrow-cmd', + score: 0.0015365464531749851, + }, + { + caption: '\\restylefloat{}', + snippet: '\\restylefloat{$1}', + meta: 'floatrow-cmd', + score: 0.0008866338267686714, + }, + { + caption: '\\floatsetup[]{}', + snippet: '\\floatsetup[$1]{$2}', + meta: 'floatrow-cmd', + score: 0.0005456136119914056, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'floatrow-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\DeclareCaptionJustification{}{}', + snippet: '\\DeclareCaptionJustification{$1}{$2}', + meta: 'floatrow-cmd', + score: 0.0001872850414971473, + }, + { + caption: '\\DeclareCaptionLabelSeparator{}{}', + snippet: '\\DeclareCaptionLabelSeparator{$1}{$2}', + meta: 'floatrow-cmd', + score: 0.0003890810058478364, + }, + { + caption: '\\DeclareCaptionFormat{}{}', + snippet: '\\DeclareCaptionFormat{$1}{$2}', + meta: 'floatrow-cmd', + score: 0.0004717618449370015, + }, + { + caption: '\\DeclareCaptionFont{}{}', + snippet: '\\DeclareCaptionFont{$1}{$2}', + meta: 'floatrow-cmd', + score: 5.0133404990680195e-5, + }, + { + caption: '\\DeclareCaptionSubType[]{}', + snippet: '\\DeclareCaptionSubType[$1]{$2}', + meta: 'floatrow-cmd', + score: 0.0001872850414971473, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'floatrow-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'floatrow-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'floatrow-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'floatrow-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'floatrow-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\DeclareCaptionType{}[][]', + snippet: '\\DeclareCaptionType{$1}[$2][$3]', + meta: 'floatrow-cmd', + score: 0.00015256647321237863, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'floatrow-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'floatrow-cmd', + score: 0.2253056071787701, + }, + { + caption: '\\footnotemark[]', + snippet: '\\footnotemark[$1]', + meta: 'floatrow-cmd', + score: 0.021473212893597875, + }, + { + caption: '\\footnotemark', + snippet: '\\footnotemark', + meta: 'floatrow-cmd', + score: 0.021473212893597875, + }, + ], + scrpage2: [ + { + caption: '\\automark[]{}', + snippet: '\\automark[$1]{$2}', + meta: 'scrpage2-cmd', + score: 0.0006703031783997437, + }, + { + caption: '\\automark{}', + snippet: '\\automark{$1}', + meta: 'scrpage2-cmd', + score: 0.0006703031783997437, + }, + { + caption: '\\ofoot{}', + snippet: '\\ofoot{$1}', + meta: 'scrpage2-cmd', + score: 0.000605620621498142, + }, + { + caption: '\\ofoot[]{}', + snippet: '\\ofoot[$1]{$2}', + meta: 'scrpage2-cmd', + score: 0.000605620621498142, + }, + { + caption: '\\ohead{}', + snippet: '\\ohead{$1}', + meta: 'scrpage2-cmd', + score: 0.004845161937670253, + }, + { + caption: '\\ohead[]{}', + snippet: '\\ohead[$1]{$2}', + meta: 'scrpage2-cmd', + score: 0.004845161937670253, + }, + { + caption: '\\headfont', + snippet: '\\headfont', + meta: 'scrpage2-cmd', + score: 0.0011116915941419892, + }, + { + caption: '\\setheadsepline{}', + snippet: '\\setheadsepline{$1}', + meta: 'scrpage2-cmd', + score: 0.00023538827295624133, + }, + { + caption: '\\clearscrheadings', + snippet: '\\clearscrheadings', + meta: 'scrpage2-cmd', + score: 0.0003679125016983611, + }, + { + caption: '\\clearscrheadfoot', + snippet: '\\clearscrheadfoot', + meta: 'scrpage2-cmd', + score: 0.000558377093879783, + }, + { + caption: '\\pagemark', + snippet: '\\pagemark', + meta: 'scrpage2-cmd', + score: 0.0017520841736604843, + }, + { + caption: '\\chead{}', + snippet: '\\chead{$1}', + meta: 'scrpage2-cmd', + score: 0.00755042164734884, + }, + { + caption: '\\clearscrplain', + snippet: '\\clearscrplain', + meta: 'scrpage2-cmd', + score: 0.00013252422874211978, + }, + { + caption: '\\ifoot{}', + snippet: '\\ifoot{$1}', + meta: 'scrpage2-cmd', + score: 0.0003620142864171218, + }, + { + caption: '\\ifoot[]{}', + snippet: '\\ifoot[$1]{$2}', + meta: 'scrpage2-cmd', + score: 0.0003620142864171218, + }, + { + caption: '\\ihead{}', + snippet: '\\ihead{$1}', + meta: 'scrpage2-cmd', + score: 0.0004507603139230655, + }, + { + caption: '\\ihead[]{}', + snippet: '\\ihead[$1]{$2}', + meta: 'scrpage2-cmd', + score: 0.0004507603139230655, + }, + { + caption: '\\cfoot{}', + snippet: '\\cfoot{$1}', + meta: 'scrpage2-cmd', + score: 0.013411641301057813, + }, + ], + pbox: [ + { + caption: '\\pbox{}{}', + snippet: '\\pbox{$1}{$2}', + meta: 'pbox-cmd', + score: 0.0010883030320478486, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'pbox-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'pbox-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'pbox-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'pbox-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'pbox-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'pbox-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'pbox-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'pbox-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'pbox-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'pbox-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'pbox-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'pbox-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'pbox-cmd', + score: 0.028955796305270766, + }, + ], + esint: [ + { + caption: '\\int', + snippet: '\\int', + meta: 'esint-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\iint', + snippet: '\\iint', + meta: 'esint-cmd', + score: 0.003916494384710151, + }, + { + caption: '\\varoiint', + snippet: '\\varoiint', + meta: 'esint-cmd', + score: 0.0001069175284516453, + }, + { + caption: '\\iiint', + snippet: '\\iiint', + meta: 'esint-cmd', + score: 0.0010383179918633135, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'esint-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\oiint', + snippet: '\\oiint', + meta: 'esint-cmd', + score: 7.127835230109687e-5, + }, + ], + algorithmicx: [ + { + caption: '\\algrenewcommand', + snippet: '\\algrenewcommand', + meta: 'algorithmicx-cmd', + score: 0.0019861803661869416, + }, + { + caption: '\\Statex', + snippet: '\\Statex', + meta: 'algorithmicx-cmd', + score: 0.008622777195102994, + }, + { + caption: '\\BState{}', + snippet: '\\BState{$1}', + meta: 'algorithmicx-cmd', + score: 0.0008685861525307122, + }, + { + caption: '\\BState', + snippet: '\\BState', + meta: 'algorithmicx-cmd', + score: 0.0008685861525307122, + }, + { + caption: '\\algloopdefx{}[][]{}', + snippet: '\\algloopdefx{$1}[$2][$3]{$4}', + meta: 'algorithmicx-cmd', + score: 0.00025315185701145097, + }, + { + caption: '\\algnewcommand', + snippet: '\\algnewcommand', + meta: 'algorithmicx-cmd', + score: 0.0030209395012065327, + }, + { + caption: '\\algnewcommand{}[]{}', + snippet: '\\algnewcommand{$1}[$2]{$3}', + meta: 'algorithmicx-cmd', + score: 0.0030209395012065327, + }, + { + caption: '\\Comment{}', + snippet: '\\Comment{$1}', + meta: 'algorithmicx-cmd', + score: 0.005178604573219454, + }, + { + caption: '\\algblockdefx{}{}[]', + snippet: '\\algblockdefx{$1}{$2}[$3]', + meta: 'algorithmicx-cmd', + score: 0.00025315185701145097, + }, + { + caption: '\\algrenewtext{}{}', + snippet: '\\algrenewtext{$1}{$2}', + meta: 'algorithmicx-cmd', + score: 0.0024415580558825975, + }, + { + caption: '\\algrenewtext{}[]{}', + snippet: '\\algrenewtext{$1}[$2]{$3}', + meta: 'algorithmicx-cmd', + score: 0.0024415580558825975, + }, + { + caption: '\\algblock{}{}', + snippet: '\\algblock{$1}{$2}', + meta: 'algorithmicx-cmd', + score: 0.0007916858220314837, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'algorithmicx-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\algdef{}[]{}{}{}{}', + snippet: '\\algdef{$1}[$2]{$3}{$4}{$5}{$6}', + meta: 'algorithmicx-cmd', + score: 0.0003102486920966127, + }, + { + caption: '\\algdef{}[]{}{}[]{}{}', + snippet: '\\algdef{$1}[$2]{$3}{$4}[$5]{$6}{$7}', + meta: 'algorithmicx-cmd', + score: 0.0003102486920966127, + }, + { + caption: '\\algdef{}[]{}[]{}', + snippet: '\\algdef{$1}[$2]{$3}[$4]{$5}', + meta: 'algorithmicx-cmd', + score: 0.0003102486920966127, + }, + { + caption: '\\algtext{}', + snippet: '\\algtext{$1}', + meta: 'algorithmicx-cmd', + score: 0.0005463612015579842, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'algorithmicx-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'algorithmicx-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'algorithmicx-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'algorithmicx-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'algorithmicx-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'algorithmicx-cmd', + score: 0.0018957469739775527, + }, + ], + bibentry: [ + { + caption: '\\bibentry{}', + snippet: '\\bibentry{$1}', + meta: 'bibentry-cmd', + score: 0.002786693424998083, + }, + { + caption: '\\url{}', + snippet: '\\url{$1}', + meta: 'bibentry-cmd', + score: 0.13586474005868793, + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'bibentry-cmd', + score: 3.800886892251021, + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'bibentry-cmd', + score: 3.800886892251021, + }, + { + caption: '\\nobibliography', + snippet: '\\nobibliography', + meta: 'bibentry-cmd', + score: 0.0009870472135074372, + }, + { + caption: '\\bibliography{}', + snippet: '\\bibliography{$1}', + meta: 'bibentry-cmd', + score: 0.2659628337907604, + }, + { + caption: '\\doi{}', + snippet: '\\doi{$1}', + meta: 'bibentry-cmd', + score: 0.004001210811454663, + }, + { + caption: '\\doi', + snippet: '\\doi', + meta: 'bibentry-cmd', + score: 0.004001210811454663, + }, + ], + txfonts: [ + { + caption: '\\sqrt{}', + snippet: '\\sqrt{$1}', + meta: 'txfonts-cmd', + score: 0.20240160977404634, + }, + ], + ngerman: [ + { + caption: '\\figurename', + snippet: '\\figurename', + meta: 'ngerman-cmd', + score: 0.008169568707145965, + }, + { + caption: '\\figurename{}', + snippet: '\\figurename{$1}', + meta: 'ngerman-cmd', + score: 0.008169568707145965, + }, + { + caption: '\\indexname', + snippet: '\\indexname', + meta: 'ngerman-cmd', + score: 0.0007544109314450072, + }, + { + caption: '\\glqq', + snippet: '\\glqq', + meta: 'ngerman-cmd', + score: 0.0039133256714254504, + }, + { + caption: '\\glqq{}', + snippet: '\\glqq{$1}', + meta: 'ngerman-cmd', + score: 0.0039133256714254504, + }, + { + caption: '\\today', + snippet: '\\today', + meta: 'ngerman-cmd', + score: 0.10733849317324783, + }, + { + caption: '\\bibname', + snippet: '\\bibname', + meta: 'ngerman-cmd', + score: 0.007599529252128519, + }, + { + caption: '\\bibname{}', + snippet: '\\bibname{$1}', + meta: 'ngerman-cmd', + score: 0.007599529252128519, + }, + { + caption: '\\captionsngerman{}', + snippet: '\\captionsngerman{$1}', + meta: 'ngerman-cmd', + score: 0.00010171098214158578, + }, + { + caption: '\\grqq', + snippet: '\\grqq', + meta: 'ngerman-cmd', + score: 0.006659522189248266, + }, + { + caption: '\\grqq{}', + snippet: '\\grqq{$1}', + meta: 'ngerman-cmd', + score: 0.006659522189248266, + }, + { + caption: '\\tablename', + snippet: '\\tablename', + meta: 'ngerman-cmd', + score: 0.0029238994233674776, + }, + ], + eucal: [ + { + caption: '\\mathscr{}', + snippet: '\\mathscr{$1}', + meta: 'eucal-cmd', + score: 0.025302230226027712, + }, + { + caption: '\\mathcal{}', + snippet: '\\mathcal{$1}', + meta: 'eucal-cmd', + score: 0.35084018920966636, + }, + ], + ifluatex: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ifluatex-cmd', + score: 0.008565354665444157, + }, + ], + chemfig: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'chemfig-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemfig-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chemfig-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chemfig-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'chemfig-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'chemfig-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'chemfig-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'chemfig-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'chemfig-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chemfig-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'chemfig-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemfig-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'chemfig-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chemfig-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chemfig-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chemfig-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'chemfig-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chemfig-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chemfig-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chemfig-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'chemfig-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chemfig-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chemfig-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'chemfig-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'chemfig-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'chemfig-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'chemfig-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'chemfig-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chemfig-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'chemfig-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'chemfig-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemfig-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'chemfig-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'chemfig-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'chemfig-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'chemfig-cmd', + score: 0.2864294797053033, + }, + ], + abstract: [ + { + caption: '\\abstractnamefont', + snippet: '\\abstractnamefont', + meta: 'abstract-cmd', + score: 6.2350576842596716e-6, + }, + ], + 'tikz-cd': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikz-cd-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-cd-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-cd-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-cd-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikz-cd-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikz-cd-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikz-cd-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikz-cd-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikz-cd-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-cd-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikz-cd-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-cd-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikz-cd-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-cd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-cd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-cd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikz-cd-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-cd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-cd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-cd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tikz-cd-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-cd-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-cd-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tikz-cd-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tikz-cd-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tikz-cd-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tikz-cd-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tikz-cd-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-cd-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tikz-cd-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tikz-cd-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-cd-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tikz-cd-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tikz-cd-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikz-cd-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikz-cd-cmd', + score: 0.2864294797053033, + }, + ], + flowfram: [ + { + caption: '\\framebreak', + snippet: '\\framebreak', + meta: 'flowfram-cmd', + score: 0.004019097827091264, + }, + { + caption: '\\newstaticframe{}{}{}{}', + snippet: '\\newstaticframe{$1}{$2}{$3}{$4}', + meta: 'flowfram-cmd', + score: 0.0014762683341407986, + }, + { + caption: '\\newflowframe{}{}{}{}[]', + snippet: '\\newflowframe{$1}{$2}{$3}{$4}[$5]', + meta: 'flowfram-cmd', + score: 0.002952536668281597, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'flowfram-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'flowfram-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'flowfram-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'flowfram-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'flowfram-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'flowfram-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'flowfram-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'flowfram-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'flowfram-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'flowfram-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'flowfram-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'flowfram-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'flowfram-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'flowfram-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'flowfram-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'flowfram-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'flowfram-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'flowfram-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'flowfram-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'flowfram-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'flowfram-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'flowfram-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'flowfram-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'flowfram-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'flowfram-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'flowfram-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'flowfram-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'flowfram-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'flowfram-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'flowfram-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'flowfram-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'flowfram-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'flowfram-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'flowfram-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'flowfram-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'flowfram-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'flowfram-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'flowfram-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'flowfram-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'flowfram-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\afterpage{}', + snippet: '\\afterpage{$1}', + meta: 'flowfram-cmd', + score: 0.0018578070791608345, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'flowfram-cmd', + score: 0.1789117552185788, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'flowfram-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'flowfram-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'flowfram-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'flowfram-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'flowfram-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'flowfram-cmd', + score: 0.0018957469739775527, + }, + ], + marginnote: [ + { + caption: '\\marginnote{}', + snippet: '\\marginnote{$1}', + meta: 'marginnote-cmd', + score: 0.010285502283803235, + }, + { + caption: '\\marginnote', + snippet: '\\marginnote', + meta: 'marginnote-cmd', + score: 0.010285502283803235, + }, + { + caption: '\\raggedleftmarginnote', + snippet: '\\raggedleftmarginnote', + meta: 'marginnote-cmd', + score: 0.0011268470793267921, + }, + ], + xfrac: [ + { + caption: '\\sfrac{}{}', + snippet: '\\sfrac{$1}{$2}', + meta: 'xfrac-cmd', + score: 0.0030164694688453453, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'xfrac-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'xfrac-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'xfrac-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'xfrac-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xfrac-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xfrac-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xfrac-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xfrac-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'xfrac-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'xfrac-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'xfrac-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xfrac-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xfrac-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'xfrac-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'xfrac-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'xfrac-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'xfrac-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'xfrac-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xfrac-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'xfrac-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xfrac-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'xfrac-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'xfrac-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'xfrac-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'xfrac-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'xfrac-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'xfrac-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'xfrac-cmd', + score: 0.0063276692758974925, + }, + ], + shortvrb: [ + { + caption: '\\MakeShortVerb{}', + snippet: '\\MakeShortVerb{$1}', + meta: 'shortvrb-cmd', + score: 0.0002890733176655595, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'shortvrb-cmd', + score: 0.009278344180101056, + }, + ], + animate: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'animate-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'animate-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'animate-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'animate-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'animate-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'animate-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'animate-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'animate-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'animate-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'animate-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'animate-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'animate-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'animate-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'animate-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'animate-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'animate-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'animate-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'animate-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'animate-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'animate-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'animate-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'animate-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'animate-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'animate-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'animate-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'animate-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'animate-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'animate-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'animate-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'animate-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'animate-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'animate-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'animate-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'animate-cmd', + score: 0.008565354665444157, + }, + ], + euscript: [ + { + caption: '\\mathscr{}', + snippet: '\\mathscr{$1}', + meta: 'euscript-cmd', + score: 0.025302230226027712, + }, + { + caption: '\\mathcal{}', + snippet: '\\mathcal{$1}', + meta: 'euscript-cmd', + score: 0.35084018920966636, + }, + ], + hhline: [ + { + caption: '\\hhline{}', + snippet: '\\hhline{$1}', + meta: 'hhline-cmd', + score: 0.0004816338278157677, + }, + ], + subfiles: [ + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'subfiles-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'subfiles-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\subfile{}', + snippet: '\\subfile{$1}', + meta: 'subfiles-cmd', + score: 0.03337062633525651, + }, + { + caption: '\\endverbatim', + snippet: '\\endverbatim', + meta: 'subfiles-cmd', + score: 0.0022216421267780076, + }, + { + caption: '\\verbatim', + snippet: '\\verbatim', + meta: 'subfiles-cmd', + score: 0.0072203369120285256, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'subfiles-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'subfiles-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\par', + snippet: '\\par', + meta: 'subfiles-cmd', + score: 0.413853376001159, + }, + { + caption: '\\verbatiminput{}', + snippet: '\\verbatiminput{$1}', + meta: 'subfiles-cmd', + score: 0.0024547099784948665, + }, + { + caption: '\\verbatiminput', + snippet: '\\verbatiminput', + meta: 'subfiles-cmd', + score: 0.0024547099784948665, + }, + ], + accents: [ + { + caption: '\\underaccent{}{}', + snippet: '\\underaccent{$1}{$2}', + meta: 'accents-cmd', + score: 0.00109513727836357, + }, + ], + theorem: [ + { + caption: '\\theorembodyfont{}', + snippet: '\\theorembodyfont{$1}', + meta: 'theorem-cmd', + score: 0.00047103366488576113, + }, + ], + metalogo: [ + { + caption: '\\XeTeX', + snippet: '\\XeTeX', + meta: 'metalogo-cmd', + score: 0.0010635559050357936, + }, + { + caption: '\\TeX', + snippet: '\\TeX', + meta: 'metalogo-cmd', + score: 0.02873756018238537, + }, + { + caption: '\\TeX{}', + snippet: '\\TeX{$1}', + meta: 'metalogo-cmd', + score: 0.02873756018238537, + }, + { + caption: '\\LaTeX', + snippet: '\\LaTeX', + meta: 'metalogo-cmd', + score: 0.2334089308452787, + }, + { + caption: '\\LaTeX{}', + snippet: '\\LaTeX{$1}', + meta: 'metalogo-cmd', + score: 0.2334089308452787, + }, + { + caption: '\\XeLaTeX', + snippet: '\\XeLaTeX', + meta: 'metalogo-cmd', + score: 0.002009786035379175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'metalogo-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'metalogo-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'metalogo-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'metalogo-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'metalogo-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'metalogo-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'metalogo-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'metalogo-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'metalogo-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'metalogo-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'metalogo-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'metalogo-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'metalogo-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'metalogo-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'metalogo-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'metalogo-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'metalogo-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'metalogo-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'metalogo-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'metalogo-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'metalogo-cmd', + score: 0.004719094298848707, + }, + ], + bookmark: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\pdfbookmark[]{}{}', + snippet: '\\pdfbookmark[$1]{$2}{$3}', + meta: 'bookmark-cmd', + score: 0.006492248863367502, + }, + { + caption: '\\bookmarkget{}', + snippet: '\\bookmarkget{$1}', + meta: 'bookmark-cmd', + score: 0.00026847053008917257, + }, + { + caption: '\\bookmarksetup{}', + snippet: '\\bookmarksetup{$1}', + meta: 'bookmark-cmd', + score: 0.001134118016265821, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'bookmark-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bookmark-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bookmark-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'bookmark-cmd', + score: 0.00047530324346933345, + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'bookmark-cmd', + score: 0.0005277905480209891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'bookmark-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'bookmark-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'bookmark-cmd', + score: 0.0002854206807593436, + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'bookmark-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'bookmark-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'bookmark-cmd', + score: 0.010515056688180681, + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'bookmark-cmd', + score: 0.008041789461944983, + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'bookmark-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'bookmark-cmd', + score: 0.0032990580087398644, + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'bookmark-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'bookmark-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\nameref{}', + snippet: '\\nameref{$1}', + meta: 'bookmark-cmd', + score: 0.009472569279662113, + }, + { + caption: '\\pdfbookmark[]{}{}', + snippet: '\\pdfbookmark[$1]{$2}{$3}', + meta: 'bookmark-cmd', + score: 0.006492248863367502, + }, + { + caption: '\\figureautorefname', + snippet: '\\figureautorefname', + meta: 'bookmark-cmd', + score: 0.00014582556188448738, + }, + { + caption: '\\figureautorefname{}', + snippet: '\\figureautorefname{$1}', + meta: 'bookmark-cmd', + score: 0.00014582556188448738, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'bookmark-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bookmark-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bookmark-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\footnoteautorefname', + snippet: '\\footnoteautorefname', + meta: 'bookmark-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\roman{}', + snippet: '\\roman{$1}', + meta: 'bookmark-cmd', + score: 0.005553384455935491, + }, + { + caption: '\\roman', + snippet: '\\roman', + meta: 'bookmark-cmd', + score: 0.005553384455935491, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'bookmark-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\MakeLowercase{}', + snippet: '\\MakeLowercase{$1}', + meta: 'bookmark-cmd', + score: 0.017289599800633146, + }, + { + caption: '\\textunderscore', + snippet: '\\textunderscore', + meta: 'bookmark-cmd', + score: 0.001509072212764015, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'bookmark-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\begin{}', + snippet: '\\begin{$1}', + meta: 'bookmark-cmd', + score: 7.849662248028187, + }, + { + caption: '\\begin{}[]', + snippet: '\\begin{$1}[$2]', + meta: 'bookmark-cmd', + score: 7.849662248028187, + }, + { + caption: '\\begin{}{}', + snippet: '\\begin{$1}{$2}', + meta: 'bookmark-cmd', + score: 7.849662248028187, + }, + { + caption: '\\FancyVerbLineautorefname', + snippet: '\\FancyVerbLineautorefname', + meta: 'bookmark-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\hyperlink{}{}', + snippet: '\\hyperlink{$1}{$2}', + meta: 'bookmark-cmd', + score: 0.00978652043902115, + }, + { + caption: '\\tableautorefname', + snippet: '\\tableautorefname', + meta: 'bookmark-cmd', + score: 0.00012704528567339081, + }, + { + caption: '\\tableautorefname{}', + snippet: '\\tableautorefname{$1}', + meta: 'bookmark-cmd', + score: 0.00012704528567339081, + }, + { + caption: '\\equationautorefname', + snippet: '\\equationautorefname', + meta: 'bookmark-cmd', + score: 0.00018777198999871106, + }, + { + caption: '\\equationautorefname{}', + snippet: '\\equationautorefname{$1}', + meta: 'bookmark-cmd', + score: 0.00018777198999871106, + }, + { + caption: '\\chapterautorefname', + snippet: '\\chapterautorefname', + meta: 'bookmark-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\TeX', + snippet: '\\TeX', + meta: 'bookmark-cmd', + score: 0.02873756018238537, + }, + { + caption: '\\TeX{}', + snippet: '\\TeX{$1}', + meta: 'bookmark-cmd', + score: 0.02873756018238537, + }, + { + caption: '\\protect', + snippet: '\\protect', + meta: 'bookmark-cmd', + score: 0.0200686676229443, + }, + { + caption: '\\appendixautorefname', + snippet: '\\appendixautorefname', + meta: 'bookmark-cmd', + score: 7.950698053641679e-5, + }, + { + caption: '\\appendixautorefname{}', + snippet: '\\appendixautorefname{$1}', + meta: 'bookmark-cmd', + score: 7.950698053641679e-5, + }, + { + caption: '\\newlabel{}{}', + snippet: '\\newlabel{$1}{$2}', + meta: 'bookmark-cmd', + score: 0.00029737672328168955, + }, + { + caption: '\\texorpdfstring{}{}', + snippet: '\\texorpdfstring{$1}{$2}', + meta: 'bookmark-cmd', + score: 0.0073781967296121, + }, + { + caption: '\\refstepcounter{}', + snippet: '\\refstepcounter{$1}', + meta: 'bookmark-cmd', + score: 0.002140559856649122, + }, + { + caption: '\\alph', + snippet: '\\alph', + meta: 'bookmark-cmd', + score: 0.01034327266194849, + }, + { + caption: '\\alph{}', + snippet: '\\alph{$1}', + meta: 'bookmark-cmd', + score: 0.01034327266194849, + }, + { + caption: '\\pageref{}', + snippet: '\\pageref{$1}', + meta: 'bookmark-cmd', + score: 0.019788865471151957, + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'bookmark-cmd', + score: 3.800886892251021, + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'bookmark-cmd', + score: 3.800886892251021, + }, + { + caption: '\\LaTeX', + snippet: '\\LaTeX', + meta: 'bookmark-cmd', + score: 0.2334089308452787, + }, + { + caption: '\\LaTeX{}', + snippet: '\\LaTeX{$1}', + meta: 'bookmark-cmd', + score: 0.2334089308452787, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\itemautorefname', + snippet: '\\itemautorefname', + meta: 'bookmark-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'bookmark-cmd', + score: 1.2569477427490174, + }, + { + caption: '\\sectionautorefname', + snippet: '\\sectionautorefname', + meta: 'bookmark-cmd', + score: 0.0019832324299155183, + }, + { + caption: '\\sectionautorefname{}', + snippet: '\\sectionautorefname{$1}', + meta: 'bookmark-cmd', + score: 0.0019832324299155183, + }, + { + caption: '\\LaTeXe', + snippet: '\\LaTeXe', + meta: 'bookmark-cmd', + score: 0.007928096378157487, + }, + { + caption: '\\LaTeXe{}', + snippet: '\\LaTeXe{$1}', + meta: 'bookmark-cmd', + score: 0.007928096378157487, + }, + { + caption: '\\footref{}', + snippet: '\\footref{$1}', + meta: 'bookmark-cmd', + score: 0.0003680857021151614, + }, + { + caption: '\\footref', + snippet: '\\footref', + meta: 'bookmark-cmd', + score: 0.0003680857021151614, + }, + { + caption: '\\hypertarget{}{}', + snippet: '\\hypertarget{$1}{$2}', + meta: 'bookmark-cmd', + score: 0.009652820108904094, + }, + { + caption: '\\theoremautorefname', + snippet: '\\theoremautorefname', + meta: 'bookmark-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'bookmark-cmd', + score: 0.7504160124360846, + }, + { + caption: '\\subparagraphautorefname', + snippet: '\\subparagraphautorefname', + meta: 'bookmark-cmd', + score: 0.0005446476945175932, + }, + { + caption: '\\url{}', + snippet: '\\url{$1}', + meta: 'bookmark-cmd', + score: 0.13586474005868793, + }, + { + caption: '\\author{}', + snippet: '\\author{$1}', + meta: 'bookmark-cmd', + score: 0.8973590434087177, + }, + { + caption: '\\author[]{}', + snippet: '\\author[$1]{$2}', + meta: 'bookmark-cmd', + score: 0.8973590434087177, + }, + { + caption: '\\href{}{}', + snippet: '\\href{$1}{$2}', + meta: 'bookmark-cmd', + score: 0.27111130260612365, + }, + { + caption: '\\Roman{}', + snippet: '\\Roman{$1}', + meta: 'bookmark-cmd', + score: 0.0038703587462843594, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bookmark-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\autoref{}', + snippet: '\\autoref{$1}', + meta: 'bookmark-cmd', + score: 0.03741172773691362, + }, + { + caption: '\\nolinkurl{}', + snippet: '\\nolinkurl{$1}', + meta: 'bookmark-cmd', + score: 0.0004995635515943437, + }, + { + caption: '\\end{}', + snippet: '\\end{$1}', + meta: 'bookmark-cmd', + score: 7.847906405228455, + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'bookmark-cmd', + score: 0.0174633138331273, + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'bookmark-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'bookmark-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\partautorefname', + snippet: '\\partautorefname', + meta: 'bookmark-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\Itemautorefname{}', + snippet: '\\Itemautorefname{$1}', + meta: 'bookmark-cmd', + score: 6.006262128895586e-5, + }, + { + caption: '\\halign{}', + snippet: '\\halign{$1}', + meta: 'bookmark-cmd', + score: 0.00017906650306643613, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'bookmark-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\ref{}', + snippet: '\\ref{$1}', + meta: 'bookmark-cmd', + score: 1.4380093454211778, + }, + { + caption: '\\Alph{}', + snippet: '\\Alph{$1}', + meta: 'bookmark-cmd', + score: 0.002233258780143355, + }, + { + caption: '\\Alph', + snippet: '\\Alph', + meta: 'bookmark-cmd', + score: 0.002233258780143355, + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'bookmark-cmd', + score: 0.047007158741781095, + }, + { + caption: '\\MP', + snippet: '\\MP', + meta: 'bookmark-cmd', + score: 0.00018344383742255004, + }, + { + caption: '\\MP{}', + snippet: '\\MP{$1}', + meta: 'bookmark-cmd', + score: 0.00018344383742255004, + }, + { + caption: '\\paragraphautorefname', + snippet: '\\paragraphautorefname', + meta: 'bookmark-cmd', + score: 0.0005446476945175932, + }, + { + caption: '\\citeN{}', + snippet: '\\citeN{$1}', + meta: 'bookmark-cmd', + score: 0.0018503938529945614, + }, + { + caption: '\\citeN', + snippet: '\\citeN', + meta: 'bookmark-cmd', + score: 0.0018503938529945614, + }, + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'bookmark-cmd', + score: 0.07503475348393239, + }, + { + caption: '\\subsectionautorefname', + snippet: '\\subsectionautorefname', + meta: 'bookmark-cmd', + score: 0.0012546605780895737, + }, + { + caption: '\\subsectionautorefname{}', + snippet: '\\subsectionautorefname{$1}', + meta: 'bookmark-cmd', + score: 0.0012546605780895737, + }, + { + caption: '\\hyperref[]{}', + snippet: '\\hyperref[$1]{$2}', + meta: 'bookmark-cmd', + score: 0.004515152477030062, + }, + { + caption: '\\arabic{}', + snippet: '\\arabic{$1}', + meta: 'bookmark-cmd', + score: 0.02445837629741638, + }, + { + caption: '\\arabic', + snippet: '\\arabic', + meta: 'bookmark-cmd', + score: 0.02445837629741638, + }, + { + caption: '\\newline', + snippet: '\\newline', + meta: 'bookmark-cmd', + score: 0.3311721696201715, + }, + { + caption: '\\hypersetup{}', + snippet: '\\hypersetup{$1}', + meta: 'bookmark-cmd', + score: 0.06967310843464661, + }, + { + caption: '\\subsubsectionautorefname', + snippet: '\\subsubsectionautorefname', + meta: 'bookmark-cmd', + score: 0.0012064581899162352, + }, + { + caption: '\\subsubsectionautorefname{}', + snippet: '\\subsubsectionautorefname{$1}', + meta: 'bookmark-cmd', + score: 0.0012064581899162352, + }, + { + caption: '\\title{}', + snippet: '\\title{$1}', + meta: 'bookmark-cmd', + score: 0.9202908262245683, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bookmark-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bookmark-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bookmark-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bookmark-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bookmark-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bookmark-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bookmark-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bookmark-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'bookmark-cmd', + score: 0.0058342578961340175, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'bookmark-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'bookmark-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bookmark-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bookmark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bookmark-cmd', + score: 0.00530510025314411, + }, + ], + anysize: [ + { + caption: '\\marginsize{}{}{}{}', + snippet: '\\marginsize{$1}{$2}{$3}{$4}', + meta: 'anysize-cmd', + score: 0.0012034744434699038, + }, + ], + diagbox: [ + { + caption: '\\diagbox[]{}{}', + snippet: '\\diagbox[$1]{$2}{$3}', + meta: 'diagbox-cmd', + score: 2.2176553306779127e-5, + }, + { + caption: '\\backslashbox{}{}', + snippet: '\\backslashbox{$1}{$2}', + meta: 'diagbox-cmd', + score: 0.0005060776550832729, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'diagbox-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\Line', + snippet: '\\Line', + meta: 'diagbox-cmd', + score: 0.0006078790177929149, + }, + { + caption: '\\polygon', + snippet: '\\polygon', + meta: 'diagbox-cmd', + score: 0.0008987552240147395, + }, + { + caption: '\\line', + snippet: '\\line', + meta: 'diagbox-cmd', + score: 0.014519741542622297, + }, + { + caption: '\\polyline', + snippet: '\\polyline', + meta: 'diagbox-cmd', + score: 0.00022468880600368487, + }, + { + caption: '\\vector', + snippet: '\\vector', + meta: 'diagbox-cmd', + score: 0.002970308722584179, + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'diagbox-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'diagbox-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'diagbox-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'diagbox-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'diagbox-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'diagbox-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'diagbox-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'diagbox-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'diagbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'diagbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'diagbox-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'diagbox-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'diagbox-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'diagbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'diagbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'diagbox-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'diagbox-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'diagbox-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'diagbox-cmd', + score: 0.028955796305270766, + }, + ], + commath: [ + { + caption: '\\dod{}{}', + snippet: '\\dod{$1}{$2}', + meta: 'commath-cmd', + score: 7.950032807135384e-5, + }, + { + caption: '\\dpd{}{}', + snippet: '\\dpd{$1}{$2}', + meta: 'commath-cmd', + score: 0.00022966761442835552, + }, + { + caption: '\\dpd[]{}{}', + snippet: '\\dpd[$1]{$2}{$3}', + meta: 'commath-cmd', + score: 0.00022966761442835552, + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'commath-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'commath-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'commath-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'commath-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'commath-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'commath-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'commath-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'commath-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'commath-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'commath-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'commath-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'commath-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'commath-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'commath-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'commath-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'commath-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'commath-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'commath-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'commath-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'commath-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'commath-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'commath-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'commath-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'commath-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'commath-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'commath-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'commath-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'commath-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'commath-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'commath-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'commath-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'commath-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'commath-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'commath-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'commath-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'commath-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'commath-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'commath-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'commath-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'commath-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'commath-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'commath-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'commath-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'commath-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'commath-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'commath-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'commath-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'commath-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'commath-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'commath-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'commath-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'commath-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'commath-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'commath-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'commath-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'commath-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'commath-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'commath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'commath-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'commath-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'commath-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'commath-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'commath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'commath-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'commath-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'commath-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'commath-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'commath-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'commath-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'commath-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'commath-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'commath-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'commath-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'commath-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'commath-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'commath-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'commath-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'commath-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'commath-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'commath-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'commath-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'commath-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'commath-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'commath-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'commath-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'commath-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'commath-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'commath-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'commath-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'commath-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'commath-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'commath-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'commath-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'commath-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'commath-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'commath-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'commath-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'commath-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'commath-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'commath-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'commath-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'commath-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'commath-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'commath-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'commath-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'commath-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'commath-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'commath-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'commath-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'commath-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'commath-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'commath-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'commath-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'commath-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'commath-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'commath-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'commath-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'commath-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'commath-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'commath-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'commath-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'commath-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'commath-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'commath-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'commath-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'commath-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'commath-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'commath-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'commath-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'commath-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'commath-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'commath-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'commath-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'commath-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'commath-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'commath-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'commath-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'commath-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'commath-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'commath-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'commath-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'commath-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'commath-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'commath-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'commath-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'commath-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'commath-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'commath-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'commath-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'commath-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'commath-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'commath-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'commath-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'commath-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'commath-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'commath-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'commath-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'commath-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'commath-cmd', + score: 0.0063276692758974925, + }, + ], + breqn: [ + { + caption: '\\biggl', + snippet: '\\biggl', + meta: 'breqn-cmd', + score: 0.0016066581118686831, + }, + { + caption: '\\biggl[]', + snippet: '\\biggl[$1]', + meta: 'breqn-cmd', + score: 0.0016066581118686831, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'breqn-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'breqn-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\end{}', + snippet: '\\end{$1}', + meta: 'breqn-cmd', + score: 7.847906405228455, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'breqn-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'breqn-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'breqn-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'breqn-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'breqn-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'breqn-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'breqn-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'breqn-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'breqn-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'breqn-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'breqn-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'breqn-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'breqn-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'breqn-cmd', + score: 0.2864294797053033, + }, + ], + ClearSans: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'ClearSans-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ClearSans-cmd', + score: 0.008565354665444157, + }, + ], + ccicons: [ + { + caption: '\\ccbynd', + snippet: '\\ccbynd', + meta: 'ccicons-cmd', + score: 0.0002103469673225986, + }, + { + caption: '\\ccbysa', + snippet: '\\ccbysa', + meta: 'ccicons-cmd', + score: 0.00016986782584471025, + }, + ], + varioref: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'varioref-cmd', + score: 0.008565354665444157, + }, + ], + SIunits: [ + { + caption: '\\micro', + snippet: '\\micro', + meta: 'SIunits-cmd', + score: 0.011051971930487929, + }, + { + caption: '\\meter', + snippet: '\\meter', + meta: 'SIunits-cmd', + score: 0.012499244923238213, + }, + { + caption: '\\cdot', + snippet: '\\cdot', + meta: 'SIunits-cmd', + score: 0.23029085545522762, + }, + { + caption: '\\degreecelsius', + snippet: '\\degreecelsius', + meta: 'SIunits-cmd', + score: 0.002130669712103909, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'SIunits-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'SIunits-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'SIunits-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'SIunits-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'SIunits-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'SIunits-cmd', + score: 0.0063276692758974925, + }, + ], + alltt: [ + { + caption: '\\par', + snippet: '\\par', + meta: 'alltt-cmd', + score: 0.413853376001159, + }, + ], + fancyvrb: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'fancyvrb-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'fancyvrb-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\refstepcounter{}', + snippet: '\\refstepcounter{$1}', + meta: 'fancyvrb-cmd', + score: 0.002140559856649122, + }, + { + caption: '\\VerbatimEnvironment', + snippet: '\\VerbatimEnvironment', + meta: 'fancyvrb-cmd', + score: 4.5350034239275855e-5, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'fancyvrb-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\fvset{}', + snippet: '\\fvset{$1}', + meta: 'fancyvrb-cmd', + score: 0.00015476887282479622, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'fancyvrb-cmd', + score: 0.00037306820619479756, + }, + ], + textgreek: [ + { + caption: '\\temp', + snippet: '\\temp', + meta: 'textgreek-cmd', + score: 0.0003566413345844499, + }, + { + caption: '\\temp{}', + snippet: '\\temp{$1}', + meta: 'textgreek-cmd', + score: 0.0003566413345844499, + }, + ], + endnotes: [ + { + caption: '\\endnote', + snippet: '\\endnote', + meta: 'endnotes-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\theendnotes', + snippet: '\\theendnotes', + meta: 'endnotes-cmd', + score: 0.0002788252334941383, + }, + ], + leading: [ + { + caption: '\\leading{}', + snippet: '\\leading{$1}', + meta: 'leading-cmd', + score: 0.00029077374894594517, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'leading-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'leading-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'leading-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'leading-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'leading-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'leading-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'leading-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'leading-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'leading-cmd', + score: 0.028955796305270766, + }, + ], + esvect: [ + { + caption: '\\vv', + snippet: '\\vv', + meta: 'esvect-cmd', + score: 0.003087420708479709, + }, + { + caption: '\\vv{}', + snippet: '\\vv{$1}', + meta: 'esvect-cmd', + score: 0.003087420708479709, + }, + ], + lettrine: [ + { + caption: '\\LettrineFontHook', + snippet: '\\LettrineFontHook', + meta: 'lettrine-cmd', + score: 9.103413871235853e-5, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'lettrine-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'lettrine-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'lettrine-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\lettrine[]{}{}', + snippet: '\\lettrine[$1]{$2}{$3}', + meta: 'lettrine-cmd', + score: 0.0028028146688245602, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'lettrine-cmd', + score: 0.00037306820619479756, + }, + ], + pgfopts: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfopts-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfopts-cmd', + score: 0.021170869458413965, + }, + ], + tabulary: [ + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'tabulary-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'tabulary-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tabulary-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tabulary-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'tabulary-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'tabulary-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'tabulary-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'tabulary-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'tabulary-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tabulary-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'tabulary-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'tabulary-cmd', + score: 0.018615449342361392, + }, + ], + grffile: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'grffile-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'grffile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'grffile-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'grffile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'grffile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'grffile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'grffile-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'grffile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'grffile-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'grffile-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'grffile-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'grffile-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'grffile-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'grffile-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'grffile-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'grffile-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'grffile-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'grffile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'grffile-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'grffile-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'grffile-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'grffile-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'grffile-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'grffile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'grffile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'grffile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'grffile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'grffile-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'grffile-cmd', + score: 0.021170869458413965, + }, + ], + pgfgantt: [ + { + caption: '\\gantttitlecalendar{}', + snippet: '\\gantttitlecalendar{$1}', + meta: 'pgfgantt-cmd', + score: 0.00027821409061195467, + }, + { + caption: '\\ganttset{}', + snippet: '\\ganttset{$1}', + meta: 'pgfgantt-cmd', + score: 0.0002492292297037303, + }, + { + caption: '\\gantttitlelist[]{}{}', + snippet: '\\gantttitlelist[$1]{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.00046430963549633653, + }, + { + caption: '\\gantttitlelist{}{}', + snippet: '\\gantttitlelist{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.00046430963549633653, + }, + { + caption: '\\ganttlink[]{}{}', + snippet: '\\ganttlink[$1]{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.0011494045501518014, + }, + { + caption: '\\newganttchartelement{}{}', + snippet: '\\newganttchartelement{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.00023651453263545777, + }, + { + caption: '\\gantttitle{}{}', + snippet: '\\gantttitle{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.001804531670553746, + }, + { + caption: '\\gantttitle[]{}{}', + snippet: '\\gantttitle[$1]{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.001804531670553746, + }, + { + caption: '\\setganttlinklabel{}{}', + snippet: '\\setganttlinklabel{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 9.045112044064169e-5, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfgantt-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfgantt-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfgantt-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfgantt-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfgantt-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfgantt-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfgantt-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfgantt-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfgantt-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfgantt-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfgantt-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfgantt-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfgantt-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfgantt-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfgantt-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfgantt-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfgantt-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfgantt-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfgantt-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfgantt-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfgantt-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfgantt-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfgantt-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfgantt-cmd', + score: 0.2864294797053033, + }, + ], + circuitikz: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'circuitikz-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'circuitikz-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'circuitikz-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'circuitikz-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'circuitikz-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'circuitikz-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'circuitikz-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'circuitikz-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'circuitikz-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'circuitikz-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'circuitikz-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'circuitikz-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'circuitikz-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'circuitikz-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'circuitikz-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'circuitikz-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'circuitikz-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'circuitikz-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'circuitikz-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'circuitikz-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'circuitikz-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'circuitikz-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'circuitikz-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'circuitikz-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'circuitikz-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'circuitikz-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'circuitikz-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'circuitikz-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'circuitikz-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'circuitikz-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'circuitikz-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'circuitikz-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'circuitikz-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'circuitikz-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'circuitikz-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'circuitikz-cmd', + score: 0.2864294797053033, + }, + ], + hypcap: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypcap-cmd', + score: 0.008565354665444157, + }, + ], + 'scrlayer-scrpage': [ + { + caption: '\\lofoot{}', + snippet: '\\lofoot{$1}', + meta: 'scrlayer-scrpage-cmd', + score: 0.00011911213812243537, + }, + { + caption: '\\rofoot{}', + snippet: '\\rofoot{$1}', + meta: 'scrlayer-scrpage-cmd', + score: 0.00021082185485863327, + }, + { + caption: '\\clearpairofpagestyles', + snippet: '\\clearpairofpagestyles', + meta: 'scrlayer-scrpage-cmd', + score: 8.874602750594376e-5, + }, + { + caption: '\\ihead{}', + snippet: '\\ihead{$1}', + meta: 'scrlayer-scrpage-cmd', + score: 0.0004507603139230655, + }, + { + caption: '\\ihead[]{}', + snippet: '\\ihead[$1]{$2}', + meta: 'scrlayer-scrpage-cmd', + score: 0.0004507603139230655, + }, + { + caption: '\\cofoot{}', + snippet: '\\cofoot{$1}', + meta: 'scrlayer-scrpage-cmd', + score: 0.00021082185485863327, + }, + { + caption: '\\cfoot{}', + snippet: '\\cfoot{$1}', + meta: 'scrlayer-scrpage-cmd', + score: 0.013411641301057813, + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'scrlayer-scrpage-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'scrlayer-scrpage-cmd', + score: 0.1789117552185788, + }, + { + caption: '\\addtokomafont{}{}', + snippet: '\\addtokomafont{$1}{$2}', + meta: 'scrlayer-scrpage-cmd', + score: 0.0008555564394100388, + }, + { + caption: '\\setkomafont{}{}', + snippet: '\\setkomafont{$1}{$2}', + meta: 'scrlayer-scrpage-cmd', + score: 0.012985816912639263, + }, + { + caption: '\\KOMAoptions{}', + snippet: '\\KOMAoptions{$1}', + meta: 'scrlayer-scrpage-cmd', + score: 0.000396664302361659, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'scrlayer-scrpage-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\automark[]{}', + snippet: '\\automark[$1]{$2}', + meta: 'scrlayer-scrpage-cmd', + score: 0.0006703031783997437, + }, + { + caption: '\\automark{}', + snippet: '\\automark{$1}', + meta: 'scrlayer-scrpage-cmd', + score: 0.0006703031783997437, + }, + { + caption: '\\pagemark', + snippet: '\\pagemark', + meta: 'scrlayer-scrpage-cmd', + score: 0.0017520841736604843, + }, + ], + amsgen: [ + { + caption: '\\do', + snippet: '\\do', + meta: 'amsgen-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'amsgen-cmd', + score: 0.0063276692758974925, + }, + ], + tipa: [ + { + caption: '\\textipa{}', + snippet: '\\textipa{$1}', + meta: 'tipa-cmd', + score: 0.0028202799587687334, + }, + ], + appendixnumberbeamer: [ + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'appendixnumberbeamer-cmd', + score: 0.047007158741781095, + }, + { + caption: '\\inserttotalframenumber', + snippet: '\\inserttotalframenumber', + meta: 'appendixnumberbeamer-cmd', + score: 0.0008756113669543194, + }, + ], + totcount: [ + { + caption: '\\totvalue{}', + snippet: '\\totvalue{$1}', + meta: 'totcount-cmd', + score: 0.000325977535138643, + }, + { + caption: '\\newtotcounter{}', + snippet: '\\newtotcounter{$1}', + meta: 'totcount-cmd', + score: 0.004398151085448998, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'totcount-cmd', + score: 0.00037306820619479756, + }, + ], + atbegshi: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'atbegshi-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'atbegshi-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'atbegshi-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'atbegshi-cmd', + score: 0.00047530324346933345, + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'atbegshi-cmd', + score: 0.0005277905480209891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'atbegshi-cmd', + score: 0.008565354665444157, + }, + ], + environ: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'environ-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'environ-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'environ-cmd', + score: 0.021170869458413965, + }, + ], + arydshln: [ + { + caption: '\\hdashline', + snippet: '\\hdashline', + meta: 'arydshln-cmd', + score: 3.1727559255976046e-5, + }, + { + caption: '\\arrayrulecolor{}', + snippet: '\\arrayrulecolor{$1}', + meta: 'arydshln-cmd', + score: 0.008538501902241319, + }, + { + caption: '\\arrayrulecolor[]{}', + snippet: '\\arrayrulecolor[$1]{$2}', + meta: 'arydshln-cmd', + score: 0.008538501902241319, + }, + { + caption: '\\hline', + snippet: '\\hline', + meta: 'arydshln-cmd', + score: 1.3209538327406387, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'arydshln-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\cline{}', + snippet: '\\cline{$1}', + meta: 'arydshln-cmd', + score: 0.07276573550543858, + }, + ], + fp: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'fp-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'fp-cmd', + score: 0.021170869458413965, + }, + ], + here: [ + { + caption: '\\listof{}{}', + snippet: '\\listof{$1}{$2}', + meta: 'here-cmd', + score: 0.0009837365348002915, + }, + { + caption: '\\floatplacement{}{}', + snippet: '\\floatplacement{$1}{$2}', + meta: 'here-cmd', + score: 0.0005815474978918903, + }, + { + caption: '\\restylefloat{}', + snippet: '\\restylefloat{$1}', + meta: 'here-cmd', + score: 0.0008866338267686714, + }, + { + caption: '\\floatstyle{}', + snippet: '\\floatstyle{$1}', + meta: 'here-cmd', + score: 0.0015470917047414941, + }, + { + caption: '\\floatname{}{}', + snippet: '\\floatname{$1}{$2}', + meta: 'here-cmd', + score: 0.0011934321931750752, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'here-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'here-cmd', + score: 1.2569477427490174, + }, + { + caption: '\\newfloat{}{}{}', + snippet: '\\newfloat{$1}{$2}{$3}', + meta: 'here-cmd', + score: 0.0012745874472536625, + }, + { + caption: '\\newfloat', + snippet: '\\newfloat', + meta: 'here-cmd', + score: 0.0012745874472536625, + }, + { + caption: '\\newfloat{}', + snippet: '\\newfloat{$1}', + meta: 'here-cmd', + score: 0.0012745874472536625, + }, + ], + layout: [ + { + caption: '\\layout', + snippet: '\\layout', + meta: 'layout-cmd', + score: 0.0003951770756385293, + }, + { + caption: '\\layout{}', + snippet: '\\layout{$1}', + meta: 'layout-cmd', + score: 0.0003951770756385293, + }, + ], + multibib: [ + { + caption: '\\newcites{}{}', + snippet: '\\newcites{$1}{$2}', + meta: 'multibib-cmd', + score: 0.0024438508435048224, + }, + { + caption: '\\bibliography{}', + snippet: '\\bibliography{$1}', + meta: 'multibib-cmd', + score: 0.2659628337907604, + }, + ], + tgpagella: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgpagella-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgpagella-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgpagella-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgpagella-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgpagella-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgpagella-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgpagella-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgpagella-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tgpagella-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tgpagella-cmd', + score: 0.021170869458413965, + }, + ], + minitoc: [ + { + caption: '\\addstarredchapter{}', + snippet: '\\addstarredchapter{$1}', + meta: 'minitoc-cmd', + score: 0.0009796486230293261, + }, + { + caption: '\\minitoc', + snippet: '\\minitoc', + meta: 'minitoc-cmd', + score: 0.001626371504530358, + }, + { + caption: '\\dominitoc', + snippet: '\\dominitoc', + meta: 'minitoc-cmd', + score: 0.0006984399207241325, + }, + { + caption: '\\mtcaddchapter', + snippet: '\\mtcaddchapter', + meta: 'minitoc-cmd', + score: 9.045112044064169e-5, + }, + { + caption: '\\listoffigures', + snippet: '\\listoffigures', + meta: 'minitoc-cmd', + score: 0.03447318897846567, + }, + { + caption: '\\listoftables', + snippet: '\\listoftables', + meta: 'minitoc-cmd', + score: 0.02104656820469027, + }, + { + caption: '\\tableofcontents', + snippet: '\\tableofcontents', + meta: 'minitoc-cmd', + score: 0.13360595130994957, + }, + { + caption: '\\adjustmtc', + snippet: '\\adjustmtc', + meta: 'minitoc-cmd', + score: 0.00015075186740106945, + }, + { + caption: '\\section{}', + snippet: '\\section{$1}', + meta: 'minitoc-cmd', + score: 3.0952612541683835, + }, + ], + nameref: [ + { + caption: '\\nameref{}', + snippet: '\\nameref{$1}', + meta: 'nameref-cmd', + score: 0.009472569279662113, + }, + { + caption: '\\protect', + snippet: '\\protect', + meta: 'nameref-cmd', + score: 0.0200686676229443, + }, + { + caption: '\\ref{}', + snippet: '\\ref{$1}', + meta: 'nameref-cmd', + score: 1.4380093454211778, + }, + { + caption: '\\pageref{}', + snippet: '\\pageref{$1}', + meta: 'nameref-cmd', + score: 0.019788865471151957, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'nameref-cmd', + score: 1.897791904799601, + }, + { + caption: '\\thepage', + snippet: '\\thepage', + meta: 'nameref-cmd', + score: 0.0591555998103519, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'nameref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'nameref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'nameref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'nameref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'nameref-cmd', + score: 0.07503475348393239, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'nameref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'nameref-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'nameref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'nameref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'nameref-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'nameref-cmd', + score: 0.008565354665444157, + }, + ], + ntheorem: [ + { + caption: '\\theoremclass{}', + snippet: '\\theoremclass{$1}', + meta: 'ntheorem-cmd', + score: 0.0001448542182198375, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'ntheorem-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\theoremstyle{}', + snippet: '\\theoremstyle{$1}', + meta: 'ntheorem-cmd', + score: 0.02533412165007986, + }, + { + caption: '\\newshadedtheorem{}{}', + snippet: '\\newshadedtheorem{$1}{$2}', + meta: 'ntheorem-cmd', + score: 0.0001632850673327423, + }, + { + caption: '\\newtheorem{}[]{}', + snippet: '\\newtheorem{$1}[$2]{$3}', + meta: 'ntheorem-cmd', + score: 0.215689795055434, + }, + { + caption: '\\newtheorem{}{}', + snippet: '\\newtheorem{$1}{$2}', + meta: 'ntheorem-cmd', + score: 0.215689795055434, + }, + { + caption: '\\newtheorem{}{}[]', + snippet: '\\newtheorem{$1}{$2}[$3]', + meta: 'ntheorem-cmd', + score: 0.215689795055434, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'ntheorem-cmd', + score: 1.897791904799601, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'ntheorem-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'ntheorem-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'ntheorem-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'ntheorem-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'ntheorem-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'ntheorem-cmd', + score: 0.0018957469739775527, + }, + ], + tabto: [ + { + caption: '\\tab', + snippet: '\\tab', + meta: 'tabto-cmd', + score: 0.016398493343291305, + }, + { + caption: '\\tab{}', + snippet: '\\tab{$1}', + meta: 'tabto-cmd', + score: 0.016398493343291305, + }, + { + caption: '\\NumTabs{}', + snippet: '\\NumTabs{$1}', + meta: 'tabto-cmd', + score: 0.00011350525217178113, + }, + { + caption: '\\tabto{}{}', + snippet: '\\tabto{$1}{$2}', + meta: 'tabto-cmd', + score: 0.002119919034744357, + }, + { + caption: '\\tabto{}', + snippet: '\\tabto{$1}', + meta: 'tabto-cmd', + score: 0.002119919034744357, + }, + ], + emptypage: [ + { + caption: '\\cleardoublepage', + snippet: '\\cleardoublepage', + meta: 'emptypage-cmd', + score: 0.044016804142963585, + }, + ], + abntex2abrev: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'abntex2abrev-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'abntex2abrev-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'abntex2abrev-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'abntex2abrev-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'abntex2abrev-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'abntex2abrev-cmd', + score: 0.0018957469739775527, + }, + ], + scrhack: [ + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'scrhack-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'scrhack-cmd', + score: 0.1789117552185788, + }, + { + caption: '\\addtokomafont{}{}', + snippet: '\\addtokomafont{$1}{$2}', + meta: 'scrhack-cmd', + score: 0.0008555564394100388, + }, + { + caption: '\\setkomafont{}{}', + snippet: '\\setkomafont{$1}{$2}', + meta: 'scrhack-cmd', + score: 0.012985816912639263, + }, + { + caption: '\\KOMAoptions{}', + snippet: '\\KOMAoptions{$1}', + meta: 'scrhack-cmd', + score: 0.000396664302361659, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'scrhack-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\xpatchcmd{}{}{}{}{}', + snippet: '\\xpatchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'scrhack-cmd', + score: 0.0019344877752147675, + }, + { + caption: '\\xpatchcmd', + snippet: '\\xpatchcmd', + meta: 'scrhack-cmd', + score: 0.0019344877752147675, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'scrhack-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'scrhack-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'scrhack-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'scrhack-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'scrhack-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'scrhack-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'scrhack-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'scrhack-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'scrhack-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'scrhack-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'scrhack-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'scrhack-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'scrhack-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'scrhack-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'scrhack-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'scrhack-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'scrhack-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'scrhack-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'scrhack-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'scrhack-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'scrhack-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'scrhack-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'scrhack-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'scrhack-cmd', + score: 0.2864294797053033, + }, + ], + nth: [ + { + caption: '\\nth{}', + snippet: '\\nth{$1}', + meta: 'nth-cmd', + score: 0.0006155314043974968, + }, + { + caption: '\\thesection', + snippet: '\\thesection', + meta: 'nth-cmd', + score: 0.011068945893347528, + }, + { + caption: '\\thesection{}', + snippet: '\\thesection{$1}', + meta: 'nth-cmd', + score: 0.011068945893347528, + }, + ], + showkeys: [ + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'showkeys-cmd', + score: 1.897791904799601, + }, + ], + fncychap: [ + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'fncychap-cmd', + score: 0.047007158741781095, + }, + { + caption: '\\ChTitleVar{}', + snippet: '\\ChTitleVar{$1}', + meta: 'fncychap-cmd', + score: 0.00047530324346933345, + }, + { + caption: '\\thechapter', + snippet: '\\thechapter', + meta: 'fncychap-cmd', + score: 0.011821300392639589, + }, + ], + ae: [ + { + caption: '\\sfdefault', + snippet: '\\sfdefault', + meta: 'ae-cmd', + score: 0.008427383388519996, + }, + { + caption: '\\sfdefault{}', + snippet: '\\sfdefault{$1}', + meta: 'ae-cmd', + score: 0.008427383388519996, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'ae-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'ae-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'ae-cmd', + score: 0.021170869458413965, + }, + ], + asymptote: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'asymptote-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'asymptote-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'asymptote-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'asymptote-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'asymptote-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'asymptote-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'asymptote-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'asymptote-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'asymptote-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'asymptote-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'asymptote-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'asymptote-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'asymptote-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'asymptote-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'asymptote-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'asymptote-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'asymptote-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'asymptote-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'asymptote-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'asymptote-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'asymptote-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'asymptote-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'asymptote-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'asymptote-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'asymptote-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'asymptote-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'asymptote-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'asymptote-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'asymptote-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'asymptote-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'asymptote-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'asymptote-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'asymptote-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'asymptote-cmd', + score: 0.008565354665444157, + }, + ], + truncate: [ + { + caption: '\\selectfont', + snippet: '\\selectfont', + meta: 'truncate-cmd', + score: 0.04598628699063736, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'truncate-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'truncate-cmd', + score: 0.021170869458413965, + }, + ], + xpatch: [ + { + caption: '\\xpatchcmd{}{}{}{}{}', + snippet: '\\xpatchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'xpatch-cmd', + score: 0.0019344877752147675, + }, + { + caption: '\\xpatchcmd', + snippet: '\\xpatchcmd', + meta: 'xpatch-cmd', + score: 0.0019344877752147675, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'xpatch-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'xpatch-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'xpatch-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'xpatch-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'xpatch-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'xpatch-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'xpatch-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'xpatch-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'xpatch-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'xpatch-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'xpatch-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'xpatch-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'xpatch-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'xpatch-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'xpatch-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'xpatch-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xpatch-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'xpatch-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'xpatch-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'xpatch-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'xpatch-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xpatch-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xpatch-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xpatch-cmd', + score: 0.2864294797053033, + }, + ], + totpages: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'totpages-cmd', + score: 0.00037306820619479756, + }, + ], + fourier: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'fourier-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'fourier-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'fourier-cmd', + score: 0.021170869458413965, + }, + ], + scrbase: [ + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'scrbase-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'scrbase-cmd', + score: 0.1789117552185788, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'scrbase-cmd', + score: 0.00037306820619479756, + }, + ], + svg: [ + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'svg-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'svg-cmd', + score: 0.1789117552185788, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'svg-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'svg-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'svg-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'svg-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'svg-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'svg-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'svg-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'svg-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'svg-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'svg-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'svg-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'svg-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'svg-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'svg-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'svg-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'svg-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'svg-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'svg-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'svg-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'svg-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'svg-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'svg-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'svg-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'svg-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'svg-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'svg-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'svg-cmd', + score: 0.008565354665444157, + }, + ], + etex: [ + { + caption: '\\reserveinserts{}', + snippet: '\\reserveinserts{$1}', + meta: 'etex-cmd', + score: 0.0018653410309739879, + }, + { + caption: '\\newtoks', + snippet: '\\newtoks', + meta: 'etex-cmd', + score: 0.00031058155311734754, + }, + ], + linguex: [ + { + caption: '\\Last[]', + snippet: '\\Last[$1]', + meta: 'linguex-cmd', + score: 0.0008163755131430334, + }, + { + caption: '\\Last', + snippet: '\\Last', + meta: 'linguex-cmd', + score: 0.0008163755131430334, + }, + { + caption: '\\Next', + snippet: '\\Next', + meta: 'linguex-cmd', + score: 0.0018776636802289772, + }, + { + caption: '\\Next[]', + snippet: '\\Next[$1]', + meta: 'linguex-cmd', + score: 0.0018776636802289772, + }, + { + caption: '\\LLast[]', + snippet: '\\LLast[$1]', + meta: 'linguex-cmd', + score: 0.00016327510262860667, + }, + { + caption: '\\LLast', + snippet: '\\LLast', + meta: 'linguex-cmd', + score: 0.00016327510262860667, + }, + { + caption: '\\NNext[]', + snippet: '\\NNext[$1]', + meta: 'linguex-cmd', + score: 0.0004490065322286684, + }, + { + caption: '\\NNext', + snippet: '\\NNext', + meta: 'linguex-cmd', + score: 0.0004490065322286684, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'linguex-cmd', + score: 1.897791904799601, + }, + { + caption: '\\xspace', + snippet: '\\xspace', + meta: 'linguex-cmd', + score: 0.07560370351316588, + }, + ], + adforn: [ + { + caption: '\\adforn{}', + snippet: '\\adforn{$1}', + meta: 'adforn-cmd', + score: 0.0003148505561835075, + }, + { + caption: '\\ding{}', + snippet: '\\ding{$1}', + meta: 'adforn-cmd', + score: 0.009992300665793867, + }, + ], + bigstrut: [ + { + caption: '\\bigstrut', + snippet: '\\bigstrut', + meta: 'bigstrut-cmd', + score: 0.005498219710082848, + }, + ], + standalone: [ + { + caption: '\\renewcommand{}{}', + snippet: '\\renewcommand{$1}{$2}', + meta: 'standalone-cmd', + score: 0.3267437011085663, + }, + { + caption: '\\renewcommand', + snippet: '\\renewcommand', + meta: 'standalone-cmd', + score: 0.3267437011085663, + }, + { + caption: '\\currfiledir', + snippet: '\\currfiledir', + meta: 'standalone-cmd', + score: 0.0002459788020229296, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'standalone-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'standalone-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'standalone-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'standalone-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'standalone-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'standalone-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'standalone-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'standalone-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'standalone-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'standalone-cmd', + score: 0.021170869458413965, + }, + ], + ifsym: [ + { + caption: '\\Letter', + snippet: '\\Letter', + meta: 'ifsym-cmd', + score: 0.0012281130571092198, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'ifsym-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'ifsym-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'ifsym-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'ifsym-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'ifsym-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'ifsym-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'ifsym-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'ifsym-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'ifsym-cmd', + score: 0.028955796305270766, + }, + ], + newtxtext: [ + { + caption: '\\textsc{}', + snippet: '\\textsc{$1}', + meta: 'newtxtext-cmd', + score: 0.6926466355384758, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'newtxtext-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'newtxtext-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'newtxtext-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'newtxtext-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'newtxtext-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'newtxtext-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'newtxtext-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'newtxtext-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'newtxtext-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'newtxtext-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'newtxtext-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'newtxtext-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'newtxtext-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'newtxtext-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'newtxtext-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'newtxtext-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'newtxtext-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'newtxtext-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'newtxtext-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'newtxtext-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'newtxtext-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'newtxtext-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'newtxtext-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'newtxtext-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'newtxtext-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'newtxtext-cmd', + score: 0.008565354665444157, + }, + ], + silence: [ + { + caption: '\\WarningsOff[]', + snippet: '\\WarningsOff[$1]', + meta: 'silence-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\WarningFilter{}{}', + snippet: '\\WarningFilter{$1}{$2}', + meta: 'silence-cmd', + score: 0.0010293824370507024, + }, + ], + numprint: [ + { + caption: '\\textcelsius', + snippet: '\\textcelsius', + meta: 'numprint-cmd', + score: 0.00012244782670334462, + }, + { + caption: '\\pm', + snippet: '\\pm', + meta: 'numprint-cmd', + score: 0.15663535405975132, + }, + { + caption: '\\npdecimalsign{}', + snippet: '\\npdecimalsign{$1}', + meta: 'numprint-cmd', + score: 8.401009062000455e-6, + }, + { + caption: '\\npthousandsep{}', + snippet: '\\npthousandsep{$1}', + meta: 'numprint-cmd', + score: 8.401009062000455e-6, + }, + { + caption: '\\np{}', + snippet: '\\np{$1}', + meta: 'numprint-cmd', + score: 0.0001782233963311367, + }, + { + caption: '\\np', + snippet: '\\np', + meta: 'numprint-cmd', + score: 0.0001782233963311367, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'numprint-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'numprint-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'numprint-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'numprint-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'numprint-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'numprint-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'numprint-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'numprint-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'numprint-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'numprint-cmd', + score: 0.018615449342361392, + }, + ], + srcltx: [ + { + caption: '\\bibliography{}', + snippet: '\\bibliography{$1}', + meta: 'srcltx-cmd', + score: 0.2659628337907604, + }, + { + caption: '\\input{}', + snippet: '\\input{$1}', + meta: 'srcltx-cmd', + score: 0.4966021927742672, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'srcltx-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'srcltx-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'srcltx-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'srcltx-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'srcltx-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'srcltx-cmd', + score: 0.0018957469739775527, + }, + ], + ctable: [ + { + caption: '\\tmark[]', + snippet: '\\tmark[$1]', + meta: 'ctable-cmd', + score: 0.004423748442334348, + }, + { + caption: '\\ctable[]{}{}{}', + snippet: '\\ctable[$1]{$2}{$3}{$4}', + meta: 'ctable-cmd', + score: 0.0007377841391165772, + }, + { + caption: '\\let', + snippet: '\\let', + meta: 'ctable-cmd', + score: 0.03789745970461662, + }, + { + caption: '\\write', + snippet: '\\write', + meta: 'ctable-cmd', + score: 0.0008038857295393196, + }, + { + caption: '\\tabularxcolumn[]{}', + snippet: '\\tabularxcolumn[$1]{$2}', + meta: 'ctable-cmd', + score: 0.00048507499766588637, + }, + { + caption: '\\tabularxcolumn', + snippet: '\\tabularxcolumn', + meta: 'ctable-cmd', + score: 0.00048507499766588637, + }, + { + caption: '\\tabularx{}{}', + snippet: '\\tabularx{$1}{$2}', + meta: 'ctable-cmd', + score: 0.0005861357565780464, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'ctable-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'ctable-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'ctable-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'ctable-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'ctable-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'ctable-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'ctable-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'ctable-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'ctable-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'ctable-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ctable-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'ctable-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'ctable-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'ctable-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'ctable-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'ctable-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'ctable-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'ctable-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'ctable-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'ctable-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'ctable-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\specialrule{}{}{}', + snippet: '\\specialrule{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.004974385202605165, + }, + { + caption: '\\cmidrule', + snippet: '\\cmidrule', + meta: 'ctable-cmd', + score: 0.01894952272365088, + }, + { + caption: '\\cmidrule{}', + snippet: '\\cmidrule{$1}', + meta: 'ctable-cmd', + score: 0.01894952272365088, + }, + { + caption: '\\bottomrule', + snippet: '\\bottomrule', + meta: 'ctable-cmd', + score: 0.04533364657852219, + }, + { + caption: '\\midrule', + snippet: '\\midrule', + meta: 'ctable-cmd', + score: 0.07098077735912875, + }, + { + caption: '\\addlinespace', + snippet: '\\addlinespace', + meta: 'ctable-cmd', + score: 0.005865460617491447, + }, + { + caption: '\\addlinespace[]', + snippet: '\\addlinespace[$1]', + meta: 'ctable-cmd', + score: 0.005865460617491447, + }, + { + caption: '\\toprule', + snippet: '\\toprule', + meta: 'ctable-cmd', + score: 0.059857788139528495, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'ctable-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'ctable-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'ctable-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ctable-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'ctable-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'ctable-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'ctable-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'ctable-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ctable-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'ctable-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'ctable-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'ctable-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'ctable-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'ctable-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'ctable-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'ctable-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'ctable-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'ctable-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'ctable-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'ctable-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'ctable-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'ctable-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'ctable-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'ctable-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'ctable-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'ctable-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'ctable-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'ctable-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ctable-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'ctable-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'ctable-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'ctable-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'ctable-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'ctable-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'ctable-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'ctable-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'ctable-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'ctable-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ctable-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'ctable-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'ctable-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'ctable-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'ctable-cmd', + score: 0.2864294797053033, + }, + ], + bbding: [ + { + caption: '\\HandRight', + snippet: '\\HandRight', + meta: 'bbding-cmd', + score: 9.986169155719329e-5, + }, + { + caption: '\\XSolidBrush', + snippet: '\\XSolidBrush', + meta: 'bbding-cmd', + score: 0.0003502234425563509, + }, + { + caption: '\\Checkmark', + snippet: '\\Checkmark', + meta: 'bbding-cmd', + score: 0.0010506703276690528, + }, + ], + endfloat: [ + { + caption: '\\DeclareDelayedFloatFlavor{}{}', + snippet: '\\DeclareDelayedFloatFlavor{$1}{$2}', + meta: 'endfloat-cmd', + score: 0.00012872796177294446, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'endfloat-cmd', + score: 0.00037306820619479756, + }, + ], + centernot: [ + { + caption: '\\centernot', + snippet: '\\centernot', + meta: 'centernot-cmd', + score: 0.0002513707969474898, + }, + ], + tikzpagenodes: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzpagenodes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzpagenodes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikzpagenodes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikzpagenodes-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikzpagenodes-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikzpagenodes-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikzpagenodes-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzpagenodes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikzpagenodes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikzpagenodes-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikzpagenodes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikzpagenodes-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\checkoddpage', + snippet: '\\checkoddpage', + meta: 'tikzpagenodes-cmd', + score: 0.00028672585452906425, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikzpagenodes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tikzpagenodes-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tikzpagenodes-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikzpagenodes-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzpagenodes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tikzpagenodes-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikzpagenodes-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikzpagenodes-cmd', + score: 0.2864294797053033, + }, + ], + xargs: [ + { + caption: '\\newcommandx{}[][]{}', + snippet: '\\newcommandx{$1}[$2][$3]{$4}', + meta: 'xargs-cmd', + score: 0.0001110821063389004, + }, + ], + morefloats: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'morefloats-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'morefloats-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'morefloats-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'morefloats-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'morefloats-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'morefloats-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'morefloats-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'morefloats-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'morefloats-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'morefloats-cmd', + score: 0.021170869458413965, + }, + ], + background: [ + { + caption: '\\BgThispage', + snippet: '\\BgThispage', + meta: 'background-cmd', + score: 0.0003956357273698423, + }, + { + caption: '\\backgroundsetup{}', + snippet: '\\backgroundsetup{$1}', + meta: 'background-cmd', + score: 0.0004910777123492879, + }, + ], + bibunits: [ + { + caption: '\\bibliography{}', + snippet: '\\bibliography{$1}', + meta: 'bibunits-cmd', + score: 0.2659628337907604, + }, + ], + moresize: [ + { + caption: '\\Huge', + snippet: '\\Huge', + meta: 'moresize-cmd', + score: 0.04725806985998919, + }, + ], + pgfpages: [ + { + caption: '\\pgfpagesphysicalpageoptions{}', + snippet: '\\pgfpagesphysicalpageoptions{$1}', + meta: 'pgfpages-cmd', + score: 0.00045967325420052095, + }, + { + caption: '\\pgfpageslogicalpageoptions{}{}', + snippet: '\\pgfpageslogicalpageoptions{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.00045967325420052095, + }, + { + caption: '\\pgfpageoptionborder{}', + snippet: '\\pgfpageoptionborder{$1}', + meta: 'pgfpages-cmd', + score: 0.0009193465084010419, + }, + { + caption: '\\pgfpageoptionborder', + snippet: '\\pgfpageoptionborder', + meta: 'pgfpages-cmd', + score: 0.0009193465084010419, + }, + { + caption: '\\pgfpagesdeclarelayout{}{}{}', + snippet: '\\pgfpagesdeclarelayout{$1}{$2}{$3}', + meta: 'pgfpages-cmd', + score: 0.00045967325420052095, + }, + { + caption: '\\pgfpagesuselayout{}', + snippet: '\\pgfpagesuselayout{$1}', + meta: 'pgfpages-cmd', + score: 0.0006090132461062934, + }, + { + caption: '\\pgfpagesuselayout{}[]', + snippet: '\\pgfpagesuselayout{$1}[$2]', + meta: 'pgfpages-cmd', + score: 0.0006090132461062934, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'pgfpages-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfpages-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfpages-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'pgfpages-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'pgfpages-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfpages-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfpages-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfpages-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfpages-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfpages-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfpages-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfpages-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfpages-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfpages-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfpages-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfpages-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfpages-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfpages-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfpages-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfpages-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfpages-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfpages-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfpages-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfpages-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfpages-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfpages-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfpages-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfpages-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfpages-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfpages-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfpages-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfpages-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfpages-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfpages-cmd', + score: 0.2864294797053033, + }, + ], + ctex: [ + { + caption: '\\CTeX', + snippet: '\\CTeX', + meta: 'ctex-cmd', + score: 0.0005884706823906032, + }, + { + caption: '\\selectfont', + snippet: '\\selectfont', + meta: 'ctex-cmd', + score: 0.04598628699063736, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'ctex-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'ctex-cmd', + score: 0.2864294797053033, + }, + ], + algcompatible: [ + { + caption: '\\algrenewcommand', + snippet: '\\algrenewcommand', + meta: 'algcompatible-cmd', + score: 0.0019861803661869416, + }, + { + caption: '\\Statex', + snippet: '\\Statex', + meta: 'algcompatible-cmd', + score: 0.008622777195102994, + }, + { + caption: '\\BState{}', + snippet: '\\BState{$1}', + meta: 'algcompatible-cmd', + score: 0.0008685861525307122, + }, + { + caption: '\\BState', + snippet: '\\BState', + meta: 'algcompatible-cmd', + score: 0.0008685861525307122, + }, + { + caption: '\\algloopdefx{}[][]{}', + snippet: '\\algloopdefx{$1}[$2][$3]{$4}', + meta: 'algcompatible-cmd', + score: 0.00025315185701145097, + }, + { + caption: '\\algnewcommand', + snippet: '\\algnewcommand', + meta: 'algcompatible-cmd', + score: 0.0030209395012065327, + }, + { + caption: '\\algnewcommand{}[]{}', + snippet: '\\algnewcommand{$1}[$2]{$3}', + meta: 'algcompatible-cmd', + score: 0.0030209395012065327, + }, + { + caption: '\\Comment{}', + snippet: '\\Comment{$1}', + meta: 'algcompatible-cmd', + score: 0.005178604573219454, + }, + { + caption: '\\algblockdefx{}{}[]', + snippet: '\\algblockdefx{$1}{$2}[$3]', + meta: 'algcompatible-cmd', + score: 0.00025315185701145097, + }, + { + caption: '\\algrenewtext{}{}', + snippet: '\\algrenewtext{$1}{$2}', + meta: 'algcompatible-cmd', + score: 0.0024415580558825975, + }, + { + caption: '\\algrenewtext{}[]{}', + snippet: '\\algrenewtext{$1}[$2]{$3}', + meta: 'algcompatible-cmd', + score: 0.0024415580558825975, + }, + { + caption: '\\algblock{}{}', + snippet: '\\algblock{$1}{$2}', + meta: 'algcompatible-cmd', + score: 0.0007916858220314837, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'algcompatible-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\algdef{}[]{}{}{}{}', + snippet: '\\algdef{$1}[$2]{$3}{$4}{$5}{$6}', + meta: 'algcompatible-cmd', + score: 0.0003102486920966127, + }, + { + caption: '\\algdef{}[]{}{}[]{}{}', + snippet: '\\algdef{$1}[$2]{$3}{$4}[$5]{$6}{$7}', + meta: 'algcompatible-cmd', + score: 0.0003102486920966127, + }, + { + caption: '\\algdef{}[]{}[]{}', + snippet: '\\algdef{$1}[$2]{$3}[$4]{$5}', + meta: 'algcompatible-cmd', + score: 0.0003102486920966127, + }, + { + caption: '\\algtext{}', + snippet: '\\algtext{$1}', + meta: 'algcompatible-cmd', + score: 0.0005463612015579842, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'algcompatible-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'algcompatible-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'algcompatible-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'algcompatible-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'algcompatible-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'algcompatible-cmd', + score: 0.0018957469739775527, + }, + ], + draftwatermark: [ + { + caption: '\\SetWatermarkScale{}', + snippet: '\\SetWatermarkScale{$1}', + meta: 'draftwatermark-cmd', + score: 0.0013776850432469145, + }, + { + caption: '\\SetWatermarkText{}', + snippet: '\\SetWatermarkText{$1}', + meta: 'draftwatermark-cmd', + score: 0.0017209596079747669, + }, + { + caption: '\\SetWatermarkColor[]{}', + snippet: '\\SetWatermarkColor[$1]{$2}', + meta: 'draftwatermark-cmd', + score: 0.0007061648188687239, + }, + { + caption: '\\SetWatermarkFontSize{}', + snippet: '\\SetWatermarkFontSize{$1}', + meta: 'draftwatermark-cmd', + score: 0.0005747853176838451, + }, + { + caption: '\\SetWatermarkLightness{}', + snippet: '\\SetWatermarkLightness{$1}', + meta: 'draftwatermark-cmd', + score: 0.0005747853176838451, + }, + { + caption: '\\SetWatermarkAngle{}', + snippet: '\\SetWatermarkAngle{$1}', + meta: 'draftwatermark-cmd', + score: 0.0005747853176838451, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'draftwatermark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'draftwatermark-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'draftwatermark-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'draftwatermark-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'draftwatermark-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'draftwatermark-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'draftwatermark-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'draftwatermark-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'draftwatermark-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'draftwatermark-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'draftwatermark-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'draftwatermark-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'draftwatermark-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'draftwatermark-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'draftwatermark-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'draftwatermark-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'draftwatermark-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'draftwatermark-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'draftwatermark-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'draftwatermark-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'draftwatermark-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'draftwatermark-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'draftwatermark-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'draftwatermark-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'draftwatermark-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'draftwatermark-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'draftwatermark-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'draftwatermark-cmd', + score: 0.004719094298848707, + }, + ], + eqparbox: [ + { + caption: '\\eqparbox{}{}', + snippet: '\\eqparbox{$1}{$2}', + meta: 'eqparbox-cmd', + score: 2.9423534119530166e-5, + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'eqparbox-cmd', + score: 3.800886892251021, + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'eqparbox-cmd', + score: 3.800886892251021, + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'eqparbox-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'eqparbox-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'eqparbox-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'eqparbox-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'eqparbox-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'eqparbox-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'eqparbox-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'eqparbox-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'eqparbox-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'eqparbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'eqparbox-cmd', + score: 0.021170869458413965, + }, + ], + nowidow: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'nowidow-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'nowidow-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'nowidow-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'nowidow-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'nowidow-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'nowidow-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'nowidow-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'nowidow-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'nowidow-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'nowidow-cmd', + score: 0.021170869458413965, + }, + ], + stackrel: [ + { + caption: '\\stackrel{}{}', + snippet: '\\stackrel{$1}{$2}', + meta: 'stackrel-cmd', + score: 0.009911875742973681, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'stackrel-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'stackrel-cmd', + score: 0.002958865219480927, + }, + ], + threeparttablex: [ + { + caption: '\\item', + snippet: '\\item', + meta: 'threeparttablex-cmd', + score: 3.800886892251021, + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'threeparttablex-cmd', + score: 3.800886892251021, + }, + { + caption: '\\insertTableNotes', + snippet: '\\insertTableNotes', + meta: 'threeparttablex-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\tnotex{}', + snippet: '\\tnotex{$1}', + meta: 'threeparttablex-cmd', + score: 0.0021491972748178554, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'threeparttablex-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'threeparttablex-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'threeparttablex-cmd', + score: 3.800886892251021, + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'threeparttablex-cmd', + score: 3.800886892251021, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'threeparttablex-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'threeparttablex-cmd', + score: 0.021170869458413965, + }, + ], + mathdesign: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mathdesign-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mathdesign-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mathdesign-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'mathdesign-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'mathdesign-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'mathdesign-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'mathdesign-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'mathdesign-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'mathdesign-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'mathdesign-cmd', + score: 0.00037306820619479756, + }, + ], + 'pst-node': [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pst-node-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pst-node-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pst-node-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pst-node-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pst-node-cmd', + score: 0.0005786730478266738, + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pst-node-cmd', + score: 0.006520475264573554, + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pst-node-cmd', + score: 0.006520475264573554, + }, + ], + varwidth: [ + { + caption: '\\par', + snippet: '\\par', + meta: 'varwidth-cmd', + score: 0.413853376001159, + }, + ], + schemabloc: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'schemabloc-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'schemabloc-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'schemabloc-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'schemabloc-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'schemabloc-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'schemabloc-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'schemabloc-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'schemabloc-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'schemabloc-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'schemabloc-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'schemabloc-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'schemabloc-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'schemabloc-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'schemabloc-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'schemabloc-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'schemabloc-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'schemabloc-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'schemabloc-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'schemabloc-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'schemabloc-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'schemabloc-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'schemabloc-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'schemabloc-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'schemabloc-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'schemabloc-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'schemabloc-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'schemabloc-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'schemabloc-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'schemabloc-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'schemabloc-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'schemabloc-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'schemabloc-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'schemabloc-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'schemabloc-cmd', + score: 0.2864294797053033, + }, + ], + bigints: [ + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'bigints-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'bigints-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'bigints-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'bigints-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'bigints-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'bigints-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'bigints-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'bigints-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'bigints-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'bigints-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'bigints-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'bigints-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'bigints-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'bigints-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'bigints-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'bigints-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'bigints-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'bigints-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'bigints-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'bigints-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'bigints-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'bigints-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'bigints-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'bigints-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'bigints-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'bigints-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'bigints-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'bigints-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'bigints-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'bigints-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'bigints-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'bigints-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'bigints-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'bigints-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'bigints-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'bigints-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'bigints-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'bigints-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'bigints-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'bigints-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'bigints-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'bigints-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'bigints-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'bigints-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'bigints-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'bigints-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'bigints-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'bigints-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'bigints-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'bigints-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'bigints-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'bigints-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'bigints-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'bigints-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'bigints-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'bigints-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'bigints-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'bigints-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'bigints-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'bigints-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'bigints-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'bigints-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'bigints-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'bigints-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'bigints-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'bigints-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'bigints-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'bigints-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'bigints-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'bigints-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'bigints-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'bigints-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'bigints-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'bigints-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'bigints-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'bigints-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'bigints-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'bigints-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'bigints-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'bigints-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'bigints-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'bigints-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'bigints-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'bigints-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'bigints-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'bigints-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'bigints-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'bigints-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'bigints-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'bigints-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'bigints-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'bigints-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'bigints-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'bigints-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'bigints-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'bigints-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'bigints-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'bigints-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'bigints-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'bigints-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'bigints-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'bigints-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'bigints-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'bigints-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'bigints-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'bigints-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'bigints-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'bigints-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'bigints-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'bigints-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'bigints-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'bigints-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'bigints-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'bigints-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'bigints-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'bigints-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'bigints-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'bigints-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'bigints-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'bigints-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'bigints-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'bigints-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'bigints-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'bigints-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'bigints-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'bigints-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'bigints-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'bigints-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'bigints-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'bigints-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'bigints-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'bigints-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'bigints-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'bigints-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'bigints-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'bigints-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'bigints-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'bigints-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'bigints-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'bigints-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'bigints-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'bigints-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'bigints-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'bigints-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'bigints-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'bigints-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'bigints-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'bigints-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'bigints-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bigints-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'bigints-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'bigints-cmd', + score: 0.0063276692758974925, + }, + ], + classicthesis: [ + { + caption: '\\marginpar{}', + snippet: '\\marginpar{$1}', + meta: 'classicthesis-cmd', + score: 0.003400158497921723, + }, + { + caption: '\\marginpar', + snippet: '\\marginpar', + meta: 'classicthesis-cmd', + score: 0.003400158497921723, + }, + { + caption: '\\cftsecleader', + snippet: '\\cftsecleader', + meta: 'classicthesis-cmd', + score: 0.0011340882025681251, + }, + { + caption: '\\cftsubsecleader', + snippet: '\\cftsubsecleader', + meta: 'classicthesis-cmd', + score: 1.0644172549700836e-5, + }, + { + caption: '\\spacedlowsmallcaps{}', + snippet: '\\spacedlowsmallcaps{$1}', + meta: 'classicthesis-cmd', + score: 0.002677188251799468, + }, + { + caption: '\\sectionmark', + snippet: '\\sectionmark', + meta: 'classicthesis-cmd', + score: 0.005008938879210868, + }, + { + caption: '\\chaptermark', + snippet: '\\chaptermark', + meta: 'classicthesis-cmd', + score: 0.005924520024686584, + }, + { + caption: '\\chaptermark{}', + snippet: '\\chaptermark{$1}', + meta: 'classicthesis-cmd', + score: 0.005924520024686584, + }, + { + caption: '\\part{}', + snippet: '\\part{$1}', + meta: 'classicthesis-cmd', + score: 0.022180129487444723, + }, + { + caption: '\\tocEntry{}', + snippet: '\\tocEntry{$1}', + meta: 'classicthesis-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\graffito{}', + snippet: '\\graffito{$1}', + meta: 'classicthesis-cmd', + score: 1.1006799670632527e-5, + }, + { + caption: '\\chapter{}', + snippet: '\\chapter{$1}', + meta: 'classicthesis-cmd', + score: 0.422097569591803, + }, + { + caption: '\\spacedallcaps{}', + snippet: '\\spacedallcaps{$1}', + meta: 'classicthesis-cmd', + score: 0.0015281000475958944, + }, + { + caption: '\\cftchapleader', + snippet: '\\cftchapleader', + meta: 'classicthesis-cmd', + score: 1.0644172549700836e-5, + }, + { + caption: '\\myVersion', + snippet: '\\myVersion', + meta: 'classicthesis-cmd', + score: 0.00018029288638573757, + }, + { + caption: '\\ctparttext{}', + snippet: '\\ctparttext{$1}', + meta: 'classicthesis-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'classicthesis-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'classicthesis-cmd', + score: 0.1789117552185788, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'classicthesis-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\specialrule{}{}{}', + snippet: '\\specialrule{$1}{$2}{$3}', + meta: 'classicthesis-cmd', + score: 0.004974385202605165, + }, + { + caption: '\\cmidrule', + snippet: '\\cmidrule', + meta: 'classicthesis-cmd', + score: 0.01894952272365088, + }, + { + caption: '\\cmidrule{}', + snippet: '\\cmidrule{$1}', + meta: 'classicthesis-cmd', + score: 0.01894952272365088, + }, + { + caption: '\\bottomrule', + snippet: '\\bottomrule', + meta: 'classicthesis-cmd', + score: 0.04533364657852219, + }, + { + caption: '\\midrule', + snippet: '\\midrule', + meta: 'classicthesis-cmd', + score: 0.07098077735912875, + }, + { + caption: '\\addlinespace', + snippet: '\\addlinespace', + meta: 'classicthesis-cmd', + score: 0.005865460617491447, + }, + { + caption: '\\addlinespace[]', + snippet: '\\addlinespace[$1]', + meta: 'classicthesis-cmd', + score: 0.005865460617491447, + }, + { + caption: '\\toprule', + snippet: '\\toprule', + meta: 'classicthesis-cmd', + score: 0.059857788139528495, + }, + { + caption: '\\titleclass{}{}[]', + snippet: '\\titleclass{$1}{$2}[$3]', + meta: 'classicthesis-cmd', + score: 0.00028979763314974667, + }, + { + caption: '\\titlelabel{}', + snippet: '\\titlelabel{$1}', + meta: 'classicthesis-cmd', + score: 6.40387839367932e-6, + }, + { + caption: '\\thetitle', + snippet: '\\thetitle', + meta: 'classicthesis-cmd', + score: 0.0015531478302713473, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'classicthesis-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'classicthesis-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\titleformat{}{}{}{}{}[]', + snippet: '\\titleformat{$1}{$2}{$3}{$4}{$5}[$6]', + meta: 'classicthesis-cmd', + score: 0.03475519439740096, + }, + { + caption: '\\titleformat{}[]{}{}{}{}', + snippet: '\\titleformat{$1}[$2]{$3}{$4}{$5}{$6}', + meta: 'classicthesis-cmd', + score: 0.03475519439740096, + }, + { + caption: '\\titleformat{}{}', + snippet: '\\titleformat{$1}{$2}', + meta: 'classicthesis-cmd', + score: 0.03475519439740096, + }, + { + caption: '\\titleformat{}{}{}{}{}', + snippet: '\\titleformat{$1}{$2}{$3}{$4}{$5}', + meta: 'classicthesis-cmd', + score: 0.03475519439740096, + }, + { + caption: '\\titlespacing{}{}{}{}', + snippet: '\\titlespacing{$1}{$2}{$3}{$4}', + meta: 'classicthesis-cmd', + score: 0.023062744385192156, + }, + { + caption: '\\markboth{}{}', + snippet: '\\markboth{$1}{$2}', + meta: 'classicthesis-cmd', + score: 0.038323601301945065, + }, + { + caption: '\\markboth{}', + snippet: '\\markboth{$1}', + meta: 'classicthesis-cmd', + score: 0.038323601301945065, + }, + { + caption: '\\markright{}', + snippet: '\\markright{$1}', + meta: 'classicthesis-cmd', + score: 0.007138622674767024, + }, + { + caption: '\\markright{}{}', + snippet: '\\markright{$1}{$2}', + meta: 'classicthesis-cmd', + score: 0.007138622674767024, + }, + { + caption: '\\filleft', + snippet: '\\filleft', + meta: 'classicthesis-cmd', + score: 7.959989906732799e-5, + }, + { + caption: '\\filcenter', + snippet: '\\filcenter', + meta: 'classicthesis-cmd', + score: 0.0004835660211260246, + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'classicthesis-cmd', + score: 0.2253056071787701, + }, + { + caption: '\\cleardoublepage', + snippet: '\\cleardoublepage', + meta: 'classicthesis-cmd', + score: 0.044016804142963585, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'classicthesis-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\chaptertitlename', + snippet: '\\chaptertitlename', + meta: 'classicthesis-cmd', + score: 0.0016985007766926272, + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'classicthesis-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\filright', + snippet: '\\filright', + meta: 'classicthesis-cmd', + score: 7.959989906732799e-5, + }, + { + caption: '\\titlerule', + snippet: '\\titlerule', + meta: 'classicthesis-cmd', + score: 0.019273712561461216, + }, + { + caption: '\\titlerule[]{}', + snippet: '\\titlerule[$1]{$2}', + meta: 'classicthesis-cmd', + score: 0.019273712561461216, + }, + { + caption: '\\addtokomafont{}{}', + snippet: '\\addtokomafont{$1}{$2}', + meta: 'classicthesis-cmd', + score: 0.0008555564394100388, + }, + { + caption: '\\setkomafont{}{}', + snippet: '\\setkomafont{$1}{$2}', + meta: 'classicthesis-cmd', + score: 0.012985816912639263, + }, + { + caption: '\\KOMAoptions{}', + snippet: '\\KOMAoptions{$1}', + meta: 'classicthesis-cmd', + score: 0.000396664302361659, + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'classicthesis-cmd', + score: 2.341195220791228, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'classicthesis-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'classicthesis-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'classicthesis-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'classicthesis-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'classicthesis-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'classicthesis-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'classicthesis-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'classicthesis-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'classicthesis-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\lsstyle', + snippet: '\\lsstyle', + meta: 'classicthesis-cmd', + score: 0.0023367519914345774, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'classicthesis-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\DisableLigatures[]{}', + snippet: '\\DisableLigatures[$1]{$2}', + meta: 'classicthesis-cmd', + score: 0.0009805246614299932, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'classicthesis-cmd', + score: 0.00021116765384691477, + }, + ], + expl3: [ + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'expl3-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'expl3-cmd', + score: 0.2864294797053033, + }, + ], + 'pst-plot': [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pst-plot-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pst-plot-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pst-plot-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pst-plot-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pst-plot-cmd', + score: 0.0005786730478266738, + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pst-plot-cmd', + score: 0.006520475264573554, + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pst-plot-cmd', + score: 0.006520475264573554, + }, + ], + chemarrow: [ + { + caption: '\\chemarrow', + snippet: '\\chemarrow', + meta: 'chemarrow-cmd', + score: 0.0005176077206367611, + }, + ], + prettyref: [ + { + caption: '\\newrefformat{}{}', + snippet: '\\newrefformat{$1}{$2}', + meta: 'prettyref-cmd', + score: 0.001373625900102228, + }, + { + caption: '\\prettyref{}', + snippet: '\\prettyref{$1}', + meta: 'prettyref-cmd', + score: 0.005783541047730358, + }, + ], + versions: [ + { + caption: '\\includeversion{}', + snippet: '\\includeversion{$1}', + meta: 'versions-cmd', + score: 0.0028410409433993543, + }, + { + caption: '\\excludeversion{}', + snippet: '\\excludeversion{$1}', + meta: 'versions-cmd', + score: 0.001742562336270228, + }, + { + caption: '\\processifversion{}{}', + snippet: '\\processifversion{$1}{$2}', + meta: 'versions-cmd', + score: 0.0022991412707353805, + }, + ], + contour: [ + { + caption: '\\contour{}{}', + snippet: '\\contour{$1}{$2}', + meta: 'contour-cmd', + score: 0.0008245159401597211, + }, + { + caption: '\\contourlength{}', + snippet: '\\contourlength{$1}', + meta: 'contour-cmd', + score: 8.130187059343861e-5, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'contour-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'contour-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'contour-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'contour-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'contour-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'contour-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'contour-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'contour-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'contour-cmd', + score: 0.008565354665444157, + }, + ], + xintexpr: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xintexpr-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xintexpr-cmd', + score: 0.021170869458413965, + }, + ], + tocstyle: [ + { + caption: '\\usetocstyle{}', + snippet: '\\usetocstyle{$1}', + meta: 'tocstyle-cmd', + score: 3.2405622997778076e-6, + }, + ], + bigdelim: [ + { + caption: '\\multirow{}{}{}', + snippet: '\\multirow{$1}{$2}{$3}', + meta: 'bigdelim-cmd', + score: 0.07525389638751734, + }, + { + caption: '\\multirow{}[]{}{}', + snippet: '\\multirow{$1}[$2]{$3}{$4}', + meta: 'bigdelim-cmd', + score: 0.07525389638751734, + }, + ], + eulervm: [ + { + caption: '\\big', + snippet: '\\big', + meta: 'eulervm-cmd', + score: 0.05613164277964739, + }, + ], + xr: [ + { + caption: '\\externaldocument{}', + snippet: '\\externaldocument{$1}', + meta: 'xr-cmd', + score: 0.0008648763879096798, + }, + ], + yhmath: [ + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'yhmath-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'yhmath-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'yhmath-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'yhmath-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'yhmath-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'yhmath-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'yhmath-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'yhmath-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'yhmath-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'yhmath-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'yhmath-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'yhmath-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'yhmath-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'yhmath-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'yhmath-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'yhmath-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'yhmath-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'yhmath-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'yhmath-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'yhmath-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'yhmath-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'yhmath-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'yhmath-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'yhmath-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'yhmath-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'yhmath-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'yhmath-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'yhmath-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'yhmath-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'yhmath-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'yhmath-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'yhmath-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'yhmath-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'yhmath-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'yhmath-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'yhmath-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'yhmath-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'yhmath-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'yhmath-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'yhmath-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'yhmath-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'yhmath-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'yhmath-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'yhmath-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'yhmath-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'yhmath-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'yhmath-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'yhmath-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'yhmath-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'yhmath-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'yhmath-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'yhmath-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'yhmath-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'yhmath-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'yhmath-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'yhmath-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'yhmath-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'yhmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'yhmath-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'yhmath-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'yhmath-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'yhmath-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'yhmath-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'yhmath-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'yhmath-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'yhmath-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'yhmath-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'yhmath-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'yhmath-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'yhmath-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'yhmath-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'yhmath-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'yhmath-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'yhmath-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'yhmath-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'yhmath-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'yhmath-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'yhmath-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'yhmath-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'yhmath-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'yhmath-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'yhmath-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'yhmath-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'yhmath-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'yhmath-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'yhmath-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'yhmath-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'yhmath-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'yhmath-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'yhmath-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'yhmath-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'yhmath-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'yhmath-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'yhmath-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'yhmath-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'yhmath-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'yhmath-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'yhmath-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'yhmath-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'yhmath-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'yhmath-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'yhmath-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'yhmath-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'yhmath-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'yhmath-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'yhmath-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'yhmath-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'yhmath-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'yhmath-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'yhmath-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'yhmath-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'yhmath-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'yhmath-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'yhmath-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'yhmath-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'yhmath-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'yhmath-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'yhmath-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'yhmath-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'yhmath-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'yhmath-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'yhmath-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'yhmath-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'yhmath-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'yhmath-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'yhmath-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'yhmath-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'yhmath-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'yhmath-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'yhmath-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'yhmath-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'yhmath-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'yhmath-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'yhmath-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'yhmath-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'yhmath-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'yhmath-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'yhmath-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'yhmath-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'yhmath-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'yhmath-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'yhmath-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'yhmath-cmd', + score: 0.0063276692758974925, + }, + ], + XCharter: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'XCharter-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'XCharter-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'XCharter-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'XCharter-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'XCharter-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'XCharter-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'XCharter-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'XCharter-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'XCharter-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'XCharter-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'XCharter-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'XCharter-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'XCharter-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'XCharter-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'XCharter-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'XCharter-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'XCharter-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'XCharter-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'XCharter-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'XCharter-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'XCharter-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'XCharter-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'XCharter-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'XCharter-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'XCharter-cmd', + score: 0.008565354665444157, + }, + ], + 'tikz-feynman': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikz-feynman-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-feynman-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-feynman-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikz-feynman-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikz-feynman-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikz-feynman-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikz-feynman-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikz-feynman-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-feynman-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikz-feynman-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-feynman-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikz-feynman-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-feynman-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-feynman-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-feynman-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikz-feynman-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-feynman-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-feynman-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-feynman-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-feynman-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-feynman-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-feynman-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-feynman-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tikz-feynman-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-feynman-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-feynman-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tikz-feynman-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tikz-feynman-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tikz-feynman-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tikz-feynman-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tikz-feynman-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-feynman-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tikz-feynman-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tikz-feynman-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-feynman-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tikz-feynman-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tikz-feynman-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikz-feynman-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikz-feynman-cmd', + score: 0.2864294797053033, + }, + ], + easylist: [ + { + caption: '\\ListProperties', + snippet: '\\ListProperties', + meta: 'easylist-cmd', + score: 5.7747123038330224e-5, + }, + ], + hologo: [ + { + caption: '\\hologo{}', + snippet: '\\hologo{$1}', + meta: 'hologo-cmd', + score: 0.00028086100750460613, + }, + ], + cases: [ + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'cases-cmd', + score: 0.002995924112493351, + }, + ], + xint: [ + { + caption: '\\xintSgnFork{}', + snippet: '\\xintSgnFork{$1}', + meta: 'xint-cmd', + score: 0.0005720629946669665, + }, + { + caption: '\\xintCmp{}{}', + snippet: '\\xintCmp{$1}{$2}', + meta: 'xint-cmd', + score: 0.0002860314973334833, + }, + { + caption: '\\xintOdd{}', + snippet: '\\xintOdd{$1}', + meta: 'xint-cmd', + score: 0.0002860314973334833, + }, + { + caption: '\\xintGeq', + snippet: '\\xintGeq', + meta: 'xint-cmd', + score: 0.0002860314973334833, + }, + ], + inputenx: [ + { + caption: '\\inputencoding{}', + snippet: '\\inputencoding{$1}', + meta: 'inputenx-cmd', + score: 0.0002447047447770061, + }, + ], + vwcol: [ + { + caption: '\\selectfont', + snippet: '\\selectfont', + meta: 'vwcol-cmd', + score: 0.04598628699063736, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'vwcol-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\justifying', + snippet: '\\justifying', + meta: 'vwcol-cmd', + score: 0.010373702256548788, + }, + { + caption: '\\justifying{}', + snippet: '\\justifying{$1}', + meta: 'vwcol-cmd', + score: 0.010373702256548788, + }, + { + caption: '\\RaggedRight', + snippet: '\\RaggedRight', + meta: 'vwcol-cmd', + score: 0.001021021782267457, + }, + { + caption: '\\Centering', + snippet: '\\Centering', + meta: 'vwcol-cmd', + score: 0.00037395241488843035, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'vwcol-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'vwcol-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'vwcol-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'vwcol-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'vwcol-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'vwcol-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'vwcol-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'vwcol-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'vwcol-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'vwcol-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'vwcol-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'vwcol-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'vwcol-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'vwcol-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'vwcol-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'vwcol-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'vwcol-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'vwcol-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'vwcol-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'vwcol-cmd', + score: 0.021170869458413965, + }, + ], + multimedia: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'multimedia-cmd', + score: 0.00037306820619479756, + }, + ], + sgame: [ + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'sgame-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'sgame-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'sgame-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'sgame-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'sgame-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'sgame-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'sgame-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'sgame-cmd', + score: 0.2864294797053033, + }, + ], + bussproofs: [ + { + caption: '\\makeatletter', + snippet: '\\makeatletter', + meta: 'bussproofs-cmd', + score: 0.041979363643201636, + }, + { + caption: '\\makeatother', + snippet: '\\makeatother', + meta: 'bussproofs-cmd', + score: 0.03923442255397878, + }, + ], + titlepic: [ + { + caption: '\\titlepic{}', + snippet: '\\titlepic{$1}', + meta: 'titlepic-cmd', + score: 0.00020896323441399082, + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'titlepic-cmd', + score: 0.7504160124360846, + }, + ], + paracol: [ + { + caption: '\\switchcolumn', + snippet: '\\switchcolumn', + meta: 'paracol-cmd', + score: 0.0008273060639466222, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'paracol-cmd', + score: 0.008565354665444157, + }, + ], + polyglossia: [ + { + caption: '\\markboth{}{}', + snippet: '\\markboth{$1}{$2}', + meta: 'polyglossia-cmd', + score: 0.038323601301945065, + }, + { + caption: '\\markboth{}', + snippet: '\\markboth{$1}', + meta: 'polyglossia-cmd', + score: 0.038323601301945065, + }, + { + caption: '\\normalfont', + snippet: '\\normalfont', + meta: 'polyglossia-cmd', + score: 0.06871177093091137, + }, + { + caption: '\\normalfont{}', + snippet: '\\normalfont{$1}', + meta: 'polyglossia-cmd', + score: 0.06871177093091137, + }, + { + caption: '\\setdefaultlanguage{}', + snippet: '\\setdefaultlanguage{$1}', + meta: 'polyglossia-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'polyglossia-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'polyglossia-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'polyglossia-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'polyglossia-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'polyglossia-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'polyglossia-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'polyglossia-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'polyglossia-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'polyglossia-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'polyglossia-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'polyglossia-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'polyglossia-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'polyglossia-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'polyglossia-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'polyglossia-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'polyglossia-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'polyglossia-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'polyglossia-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'polyglossia-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'polyglossia-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'polyglossia-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'polyglossia-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'polyglossia-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'polyglossia-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'polyglossia-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'polyglossia-cmd', + score: 0.2864294797053033, + }, + ], + 'zref-user': [ + { + caption: '\\zlabel{}', + snippet: '\\zlabel{$1}', + meta: 'zref-user-cmd', + score: 0.0005277905480209891, + }, + { + caption: '\\zref', + snippet: '\\zref', + meta: 'zref-user-cmd', + score: 0.002193637536912482, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-user-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-user-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-user-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'zref-user-cmd', + score: 0.0058342578961340175, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'zref-user-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-user-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-user-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'zref-user-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-user-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-user-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'zref-user-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'zref-user-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-user-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-user-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-user-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'zref-user-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-user-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-user-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-user-cmd', + score: 0.002958865219480927, + }, + ], + 'zref-abspage': [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-abspage-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'zref-abspage-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'zref-abspage-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'zref-abspage-cmd', + score: 0.00047530324346933345, + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'zref-abspage-cmd', + score: 0.0005277905480209891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-abspage-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-abspage-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-abspage-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-abspage-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'zref-abspage-cmd', + score: 0.0058342578961340175, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'zref-abspage-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-abspage-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-abspage-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'zref-abspage-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-abspage-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-abspage-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'zref-abspage-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'zref-abspage-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-abspage-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-abspage-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-abspage-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'zref-abspage-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-abspage-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-abspage-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-abspage-cmd', + score: 0.002958865219480927, + }, + ], + quotchap: [ + { + caption: '\\chapter{}', + snippet: '\\chapter{$1}', + meta: 'quotchap-cmd', + score: 0.422097569591803, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'quotchap-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'quotchap-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\qauthor{}', + snippet: '\\qauthor{$1}', + meta: 'quotchap-cmd', + score: 0.002335082759143631, + }, + ], + misccorr: [ + { + caption: '\\subsection{}', + snippet: '\\subsection{$1}', + meta: 'misccorr-cmd', + score: 1.3890912739512353, + }, + { + caption: '\\section{}', + snippet: '\\section{$1}', + meta: 'misccorr-cmd', + score: 3.0952612541683835, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'misccorr-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\makelabel', + snippet: '\\makelabel', + meta: 'misccorr-cmd', + score: 5.739925426740175e-5, + }, + { + caption: '\\makelabel{}', + snippet: '\\makelabel{$1}', + meta: 'misccorr-cmd', + score: 5.739925426740175e-5, + }, + { + caption: '\\makelabel[]{}', + snippet: '\\makelabel[$1]{$2}', + meta: 'misccorr-cmd', + score: 5.739925426740175e-5, + }, + { + caption: '\\frak{}', + snippet: '\\frak{$1}', + meta: 'misccorr-cmd', + score: 0.0017966000518546787, + }, + { + caption: '\\checkmark', + snippet: '\\checkmark', + meta: 'misccorr-cmd', + score: 0.025060530944368123, + }, + { + caption: '\\bold', + snippet: '\\bold', + meta: 'misccorr-cmd', + score: 0.0014358547624941567, + }, + { + caption: '\\bold{}', + snippet: '\\bold{$1}', + meta: 'misccorr-cmd', + score: 0.0014358547624941567, + }, + { + caption: '\\Bbb{}', + snippet: '\\Bbb{$1}', + meta: 'misccorr-cmd', + score: 0.0006671850995492977, + }, + { + caption: '\\Bbb', + snippet: '\\Bbb', + meta: 'misccorr-cmd', + score: 0.0006671850995492977, + }, + ], + academicons: [ + { + caption: '\\aiResearchGateSquare', + snippet: '\\aiResearchGateSquare', + meta: 'academicons-cmd', + score: 0.0005747853176838451, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'academicons-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'academicons-cmd', + score: 0.2864294797053033, + }, + ], + tasks: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tasks-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tasks-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tasks-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tasks-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tasks-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tasks-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tasks-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tasks-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tasks-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tasks-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tasks-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tasks-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tasks-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'tasks-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'tasks-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'tasks-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'tasks-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'tasks-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'tasks-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'tasks-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'tasks-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'tasks-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'tasks-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'tasks-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'tasks-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'tasks-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'tasks-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'tasks-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'tasks-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tasks-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'tasks-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'tasks-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'tasks-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'tasks-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tasks-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tasks-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tasks-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tasks-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tasks-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tasks-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tasks-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tasks-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tasks-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tasks-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tasks-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tasks-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tasks-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tasks-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tasks-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tasks-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tasks-cmd', + score: 0.2864294797053033, + }, + ], + 'pstricks-add': [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pstricks-add-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pstricks-add-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pstricks-add-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pstricks-add-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pstricks-add-cmd', + score: 0.0005786730478266738, + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pstricks-add-cmd', + score: 0.006520475264573554, + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pstricks-add-cmd', + score: 0.006520475264573554, + }, + ], + extramarks: [ + { + caption: '\\leftmark', + snippet: '\\leftmark', + meta: 'extramarks-cmd', + score: 0.01094124445235767, + }, + { + caption: '\\extramarks{}{}', + snippet: '\\extramarks{$1}{$2}', + meta: 'extramarks-cmd', + score: 0.0003269562507660904, + }, + { + caption: '\\markboth{}{}', + snippet: '\\markboth{$1}{$2}', + meta: 'extramarks-cmd', + score: 0.038323601301945065, + }, + { + caption: '\\markboth{}', + snippet: '\\markboth{$1}', + meta: 'extramarks-cmd', + score: 0.038323601301945065, + }, + { + caption: '\\markright{}', + snippet: '\\markright{$1}', + meta: 'extramarks-cmd', + score: 0.007138622674767024, + }, + { + caption: '\\markright{}{}', + snippet: '\\markright{$1}{$2}', + meta: 'extramarks-cmd', + score: 0.007138622674767024, + }, + { + caption: '\\rightmark', + snippet: '\\rightmark', + meta: 'extramarks-cmd', + score: 0.008472328846194114, + }, + ], + calrsfs: [ + { + caption: '\\mathcal{}', + snippet: '\\mathcal{$1}', + meta: 'calrsfs-cmd', + score: 0.35084018920966636, + }, + ], + newlfont: [ + { + caption: '\\em', + snippet: '\\em', + meta: 'newlfont-cmd', + score: 0.10357353994640862, + }, + ], + mdwtab: [ + { + caption: '\\cline{}', + snippet: '\\cline{$1}', + meta: 'mdwtab-cmd', + score: 0.07276573550543858, + }, + { + caption: '\\hline', + snippet: '\\hline', + meta: 'mdwtab-cmd', + score: 1.3209538327406387, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'mdwtab-cmd', + score: 0.5473606021405326, + }, + ], + mdwmath: [ + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'mdwmath-cmd', + score: 0.04318078602869565, + }, + ], + wallpaper: [ + { + caption: '\\CenterWallPaper{}{}', + snippet: '\\CenterWallPaper{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.00042983945496357105, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'wallpaper-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'wallpaper-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'wallpaper-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'wallpaper-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'wallpaper-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'wallpaper-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'wallpaper-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'wallpaper-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'wallpaper-cmd', + score: 0.00047530324346933345, + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'wallpaper-cmd', + score: 0.0005277905480209891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'wallpaper-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\AddToShipoutPictureFG{}', + snippet: '\\AddToShipoutPictureFG{$1}', + meta: 'wallpaper-cmd', + score: 0.000325977535138643, + }, + { + caption: '\\AddToShipoutPictureBG{}', + snippet: '\\AddToShipoutPictureBG{$1}', + meta: 'wallpaper-cmd', + score: 0.0008957666085644653, + }, + { + caption: '\\AtPageUpperLeft{}', + snippet: '\\AtPageUpperLeft{$1}', + meta: 'wallpaper-cmd', + score: 0.0003608141410278152, + }, + { + caption: '\\LenToUnit{}', + snippet: '\\LenToUnit{$1}', + meta: 'wallpaper-cmd', + score: 0.0007216282820556304, + }, + { + caption: '\\AddToShipoutPicture{}', + snippet: '\\AddToShipoutPicture{$1}', + meta: 'wallpaper-cmd', + score: 0.0017658629469099734, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'wallpaper-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'wallpaper-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'wallpaper-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'wallpaper-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'wallpaper-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'wallpaper-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'wallpaper-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'wallpaper-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'wallpaper-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'wallpaper-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'wallpaper-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'wallpaper-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'wallpaper-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'wallpaper-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'wallpaper-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'wallpaper-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'wallpaper-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'wallpaper-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'wallpaper-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'wallpaper-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'wallpaper-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'wallpaper-cmd', + score: 0.008565354665444157, + }, + ], + newunicodechar: [ + { + caption: '\\newunicodechar{}{}', + snippet: '\\newunicodechar{$1}{$2}', + meta: 'newunicodechar-cmd', + score: 8.718084183564492e-5, + }, + ], + thmtools: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'thmtools-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\listtheoremname', + snippet: '\\listtheoremname', + meta: 'thmtools-cmd', + score: 1.9443373798666845e-5, + }, + { + caption: '\\thmtformatoptarg', + snippet: '\\thmtformatoptarg', + meta: 'thmtools-cmd', + score: 6.353668036093916e-5, + }, + { + caption: '\\listoftheorems[]', + snippet: '\\listoftheorems[$1]', + meta: 'thmtools-cmd', + score: 1.9443373798666845e-5, + }, + { + caption: '\\declaretheoremstyle[]{}', + snippet: '\\declaretheoremstyle[$1]{$2}', + meta: 'thmtools-cmd', + score: 0.0001168034231635369, + }, + { + caption: '\\declaretheorem[]{}', + snippet: '\\declaretheorem[$1]{$2}', + meta: 'thmtools-cmd', + score: 0.0004904790216915127, + }, + { + caption: '\\theoremstyle{}', + snippet: '\\theoremstyle{$1}', + meta: 'thmtools-cmd', + score: 0.02533412165007986, + }, + { + caption: '\\proof{}', + snippet: '\\proof{$1}', + meta: 'thmtools-cmd', + score: 0.000701497773639073, + }, + { + caption: '\\proof', + snippet: '\\proof', + meta: 'thmtools-cmd', + score: 0.000701497773639073, + }, + { + caption: '\\newtheorem{}[]{}', + snippet: '\\newtheorem{$1}[$2]{$3}', + meta: 'thmtools-cmd', + score: 0.215689795055434, + }, + { + caption: '\\newtheorem{}{}', + snippet: '\\newtheorem{$1}{$2}', + meta: 'thmtools-cmd', + score: 0.215689795055434, + }, + { + caption: '\\newtheorem{}{}[]', + snippet: '\\newtheorem{$1}{$2}[$3]', + meta: 'thmtools-cmd', + score: 0.215689795055434, + }, + { + caption: '\\endproof', + snippet: '\\endproof', + meta: 'thmtools-cmd', + score: 0.0006133100544751855, + }, + { + caption: '\\endproof{}', + snippet: '\\endproof{$1}', + meta: 'thmtools-cmd', + score: 0.0006133100544751855, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'thmtools-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'thmtools-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'thmtools-cmd', + score: 0.008565354665444157, + }, + ], + nccmath: [ + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'nccmath-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'nccmath-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'nccmath-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'nccmath-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'nccmath-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'nccmath-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'nccmath-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'nccmath-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'nccmath-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'nccmath-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'nccmath-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'nccmath-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'nccmath-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'nccmath-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'nccmath-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'nccmath-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'nccmath-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'nccmath-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'nccmath-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'nccmath-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'nccmath-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'nccmath-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'nccmath-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'nccmath-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'nccmath-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'nccmath-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'nccmath-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'nccmath-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'nccmath-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'nccmath-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'nccmath-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'nccmath-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'nccmath-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'nccmath-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'nccmath-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'nccmath-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'nccmath-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'nccmath-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'nccmath-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'nccmath-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'nccmath-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'nccmath-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'nccmath-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'nccmath-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'nccmath-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'nccmath-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'nccmath-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'nccmath-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'nccmath-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'nccmath-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'nccmath-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'nccmath-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'nccmath-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'nccmath-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'nccmath-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'nccmath-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'nccmath-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'nccmath-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'nccmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'nccmath-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'nccmath-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'nccmath-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'nccmath-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'nccmath-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'nccmath-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'nccmath-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'nccmath-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'nccmath-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'nccmath-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'nccmath-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'nccmath-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'nccmath-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'nccmath-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'nccmath-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'nccmath-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'nccmath-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'nccmath-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'nccmath-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'nccmath-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'nccmath-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'nccmath-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'nccmath-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'nccmath-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'nccmath-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'nccmath-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'nccmath-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'nccmath-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'nccmath-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'nccmath-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'nccmath-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'nccmath-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'nccmath-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'nccmath-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'nccmath-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'nccmath-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'nccmath-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'nccmath-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'nccmath-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'nccmath-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'nccmath-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'nccmath-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'nccmath-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'nccmath-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'nccmath-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'nccmath-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'nccmath-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'nccmath-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'nccmath-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'nccmath-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'nccmath-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'nccmath-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'nccmath-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'nccmath-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'nccmath-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'nccmath-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'nccmath-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'nccmath-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'nccmath-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'nccmath-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'nccmath-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'nccmath-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'nccmath-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'nccmath-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'nccmath-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'nccmath-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'nccmath-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'nccmath-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'nccmath-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'nccmath-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'nccmath-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'nccmath-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'nccmath-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'nccmath-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'nccmath-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'nccmath-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'nccmath-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'nccmath-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'nccmath-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'nccmath-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'nccmath-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'nccmath-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'nccmath-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'nccmath-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'nccmath-cmd', + score: 0.0063276692758974925, + }, + ], + scrtime: [ + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'scrtime-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'scrtime-cmd', + score: 0.1789117552185788, + }, + { + caption: '\\addtokomafont{}{}', + snippet: '\\addtokomafont{$1}{$2}', + meta: 'scrtime-cmd', + score: 0.0008555564394100388, + }, + { + caption: '\\setkomafont{}{}', + snippet: '\\setkomafont{$1}{$2}', + meta: 'scrtime-cmd', + score: 0.012985816912639263, + }, + { + caption: '\\KOMAoptions{}', + snippet: '\\KOMAoptions{$1}', + meta: 'scrtime-cmd', + score: 0.000396664302361659, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'scrtime-cmd', + score: 0.00037306820619479756, + }, + ], + luainputenc: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'luainputenc-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'luainputenc-cmd', + score: 0.008565354665444157, + }, + ], + curve2e: [ + { + caption: '\\polyline', + snippet: '\\polyline', + meta: 'curve2e-cmd', + score: 0.00022468880600368487, + }, + { + caption: '\\put', + snippet: '\\put', + meta: 'curve2e-cmd', + score: 0.0406766030275089, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'curve2e-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'curve2e-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'curve2e-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'curve2e-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'curve2e-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'curve2e-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'curve2e-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'curve2e-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\Line', + snippet: '\\Line', + meta: 'curve2e-cmd', + score: 0.0006078790177929149, + }, + { + caption: '\\polygon', + snippet: '\\polygon', + meta: 'curve2e-cmd', + score: 0.0008987552240147395, + }, + { + caption: '\\line', + snippet: '\\line', + meta: 'curve2e-cmd', + score: 0.014519741542622297, + }, + { + caption: '\\polyline', + snippet: '\\polyline', + meta: 'curve2e-cmd', + score: 0.00022468880600368487, + }, + { + caption: '\\vector', + snippet: '\\vector', + meta: 'curve2e-cmd', + score: 0.002970308722584179, + }, + ], + couriers: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'couriers-cmd', + score: 0.00037306820619479756, + }, + ], + caption3: [ + { + caption: '\\DeclareCaptionJustification{}{}', + snippet: '\\DeclareCaptionJustification{$1}{$2}', + meta: 'caption3-cmd', + score: 0.0001872850414971473, + }, + { + caption: '\\DeclareCaptionLabelSeparator{}{}', + snippet: '\\DeclareCaptionLabelSeparator{$1}{$2}', + meta: 'caption3-cmd', + score: 0.0003890810058478364, + }, + { + caption: '\\DeclareCaptionFormat{}{}', + snippet: '\\DeclareCaptionFormat{$1}{$2}', + meta: 'caption3-cmd', + score: 0.0004717618449370015, + }, + { + caption: '\\DeclareCaptionFont{}{}', + snippet: '\\DeclareCaptionFont{$1}{$2}', + meta: 'caption3-cmd', + score: 5.0133404990680195e-5, + }, + { + caption: '\\DeclareCaptionSubType[]{}', + snippet: '\\DeclareCaptionSubType[$1]{$2}', + meta: 'caption3-cmd', + score: 0.0001872850414971473, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'caption3-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'caption3-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'caption3-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'caption3-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'caption3-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\DeclareCaptionType{}[][]', + snippet: '\\DeclareCaptionType{$1}[$2][$3]', + meta: 'caption3-cmd', + score: 0.00015256647321237863, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'caption3-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'caption3-cmd', + score: 0.2253056071787701, + }, + { + caption: '\\footnotemark[]', + snippet: '\\footnotemark[$1]', + meta: 'caption3-cmd', + score: 0.021473212893597875, + }, + { + caption: '\\footnotemark', + snippet: '\\footnotemark', + meta: 'caption3-cmd', + score: 0.021473212893597875, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'caption3-cmd', + score: 0.00037306820619479756, + }, + ], + gauss: [ + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'gauss-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'gauss-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'gauss-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'gauss-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'gauss-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'gauss-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'gauss-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'gauss-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'gauss-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'gauss-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'gauss-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'gauss-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'gauss-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'gauss-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'gauss-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'gauss-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'gauss-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'gauss-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'gauss-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'gauss-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'gauss-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'gauss-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'gauss-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'gauss-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'gauss-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'gauss-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'gauss-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'gauss-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'gauss-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'gauss-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'gauss-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'gauss-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'gauss-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'gauss-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'gauss-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'gauss-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'gauss-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'gauss-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'gauss-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'gauss-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'gauss-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'gauss-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'gauss-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'gauss-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'gauss-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'gauss-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'gauss-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'gauss-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'gauss-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'gauss-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'gauss-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'gauss-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'gauss-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'gauss-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'gauss-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'gauss-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'gauss-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'gauss-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'gauss-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'gauss-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'gauss-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'gauss-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'gauss-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'gauss-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'gauss-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'gauss-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'gauss-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'gauss-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'gauss-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'gauss-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'gauss-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'gauss-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'gauss-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'gauss-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'gauss-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'gauss-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'gauss-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'gauss-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'gauss-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'gauss-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'gauss-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'gauss-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'gauss-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'gauss-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'gauss-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'gauss-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'gauss-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'gauss-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'gauss-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'gauss-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'gauss-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'gauss-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'gauss-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'gauss-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'gauss-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'gauss-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'gauss-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'gauss-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'gauss-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'gauss-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'gauss-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'gauss-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'gauss-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'gauss-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'gauss-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'gauss-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'gauss-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'gauss-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'gauss-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'gauss-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'gauss-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'gauss-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'gauss-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'gauss-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'gauss-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'gauss-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'gauss-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'gauss-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'gauss-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'gauss-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'gauss-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'gauss-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'gauss-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'gauss-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'gauss-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'gauss-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'gauss-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'gauss-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'gauss-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'gauss-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'gauss-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'gauss-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'gauss-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'gauss-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'gauss-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'gauss-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'gauss-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'gauss-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'gauss-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'gauss-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'gauss-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'gauss-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'gauss-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'gauss-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'gauss-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'gauss-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'gauss-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'gauss-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'gauss-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'gauss-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'gauss-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'gauss-cmd', + score: 0.0063276692758974925, + }, + ], + fancyref: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'fancyref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'fancyref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'fancyref-cmd', + score: 0.008565354665444157, + }, + ], + eufrak: [ + { + caption: '\\mathfrak{}', + snippet: '\\mathfrak{$1}', + meta: 'eufrak-cmd', + score: 0.025213895825856578, + }, + { + caption: '\\mathfrak', + snippet: '\\mathfrak', + meta: 'eufrak-cmd', + score: 0.025213895825856578, + }, + ], + fixme: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'fixme-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'fixme-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'fixme-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'fixme-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'fixme-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'fixme-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\endverbatim', + snippet: '\\endverbatim', + meta: 'fixme-cmd', + score: 0.0022216421267780076, + }, + { + caption: '\\verbatim', + snippet: '\\verbatim', + meta: 'fixme-cmd', + score: 0.0072203369120285256, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'fixme-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'fixme-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\par', + snippet: '\\par', + meta: 'fixme-cmd', + score: 0.413853376001159, + }, + { + caption: '\\verbatiminput{}', + snippet: '\\verbatiminput{$1}', + meta: 'fixme-cmd', + score: 0.0024547099784948665, + }, + { + caption: '\\verbatiminput', + snippet: '\\verbatiminput', + meta: 'fixme-cmd', + score: 0.0024547099784948665, + }, + ], + 'pgf-umlsd': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-umlsd-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgf-umlsd-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgf-umlsd-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgf-umlsd-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgf-umlsd-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgf-umlsd-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-umlsd-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgf-umlsd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgf-umlsd-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'pgf-umlsd-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgf-umlsd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgf-umlsd-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgf-umlsd-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgf-umlsd-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgf-umlsd-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-umlsd-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgf-umlsd-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgf-umlsd-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgf-umlsd-cmd', + score: 0.2864294797053033, + }, + ], + tgadventor: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgadventor-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgadventor-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgadventor-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgadventor-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgadventor-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgadventor-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgadventor-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgadventor-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tgadventor-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tgadventor-cmd', + score: 0.021170869458413965, + }, + ], + fancyheadings: [ + { + caption: '\\lhead{}', + snippet: '\\lhead{$1}', + meta: 'fancyheadings-cmd', + score: 0.05268978171228714, + }, + { + caption: '\\chaptermark', + snippet: '\\chaptermark', + meta: 'fancyheadings-cmd', + score: 0.005924520024686584, + }, + { + caption: '\\chaptermark{}', + snippet: '\\chaptermark{$1}', + meta: 'fancyheadings-cmd', + score: 0.005924520024686584, + }, + { + caption: '\\fancypagestyle{}{}', + snippet: '\\fancypagestyle{$1}{$2}', + meta: 'fancyheadings-cmd', + score: 0.009430919590937878, + }, + { + caption: '\\footrule', + snippet: '\\footrule', + meta: 'fancyheadings-cmd', + score: 0.0010032754348913366, + }, + { + caption: '\\footrule{}', + snippet: '\\footrule{$1}', + meta: 'fancyheadings-cmd', + score: 0.0010032754348913366, + }, + { + caption: '\\fancyfoot[]{}', + snippet: '\\fancyfoot[$1]{$2}', + meta: 'fancyheadings-cmd', + score: 0.024973618823189894, + }, + { + caption: '\\fancyfoot{}', + snippet: '\\fancyfoot{$1}', + meta: 'fancyheadings-cmd', + score: 0.024973618823189894, + }, + { + caption: '\\fancyfootoffset[]{}', + snippet: '\\fancyfootoffset[$1]{$2}', + meta: 'fancyheadings-cmd', + score: 0.0015373246231684555, + }, + { + caption: '\\fancyfootoffset{}', + snippet: '\\fancyfootoffset{$1}', + meta: 'fancyheadings-cmd', + score: 0.0015373246231684555, + }, + { + caption: '\\footruleskip', + snippet: '\\footruleskip', + meta: 'fancyheadings-cmd', + score: 0.000830117957327721, + }, + { + caption: '\\fancyheadoffset[]{}', + snippet: '\\fancyheadoffset[$1]{$2}', + meta: 'fancyheadings-cmd', + score: 0.0016786568695309166, + }, + { + caption: '\\fancyheadoffset{}', + snippet: '\\fancyheadoffset{$1}', + meta: 'fancyheadings-cmd', + score: 0.0016786568695309166, + }, + { + caption: '\\iffloatpage{}{}', + snippet: '\\iffloatpage{$1}{$2}', + meta: 'fancyheadings-cmd', + score: 6.606286310833368e-5, + }, + { + caption: '\\cfoot{}', + snippet: '\\cfoot{$1}', + meta: 'fancyheadings-cmd', + score: 0.013411641301057813, + }, + { + caption: '\\subsectionmark', + snippet: '\\subsectionmark', + meta: 'fancyheadings-cmd', + score: 3.1153423008593836e-5, + }, + { + caption: '\\footrulewidth', + snippet: '\\footrulewidth', + meta: 'fancyheadings-cmd', + score: 0.011424740897486949, + }, + { + caption: '\\fancyhfoffset[]{}', + snippet: '\\fancyhfoffset[$1]{$2}', + meta: 'fancyheadings-cmd', + score: 3.741978601121172e-5, + }, + { + caption: '\\rhead{}', + snippet: '\\rhead{$1}', + meta: 'fancyheadings-cmd', + score: 0.022782817416731292, + }, + { + caption: '\\fancyplain{}{}', + snippet: '\\fancyplain{$1}{$2}', + meta: 'fancyheadings-cmd', + score: 0.007402339896386138, + }, + { + caption: '\\rfoot{}', + snippet: '\\rfoot{$1}', + meta: 'fancyheadings-cmd', + score: 0.013393817825547868, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'fancyheadings-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\plainheadrulewidth', + snippet: '\\plainheadrulewidth', + meta: 'fancyheadings-cmd', + score: 6.2350576842596716e-6, + }, + { + caption: '\\baselinestretch', + snippet: '\\baselinestretch', + meta: 'fancyheadings-cmd', + score: 0.03225350148161425, + }, + { + caption: '\\lfoot{}', + snippet: '\\lfoot{$1}', + meta: 'fancyheadings-cmd', + score: 0.00789399846642229, + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'fancyheadings-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'fancyheadings-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\fancyhf{}', + snippet: '\\fancyhf{$1}', + meta: 'fancyheadings-cmd', + score: 0.02314618933449356, + }, + { + caption: '\\sectionmark', + snippet: '\\sectionmark', + meta: 'fancyheadings-cmd', + score: 0.005008938879210868, + }, + { + caption: '\\fancyhead[]{}', + snippet: '\\fancyhead[$1]{$2}', + meta: 'fancyheadings-cmd', + score: 0.039101068064744296, + }, + { + caption: '\\fancyhead{}', + snippet: '\\fancyhead{$1}', + meta: 'fancyheadings-cmd', + score: 0.039101068064744296, + }, + { + caption: '\\nouppercase{}', + snippet: '\\nouppercase{$1}', + meta: 'fancyheadings-cmd', + score: 0.006416387071584083, + }, + { + caption: '\\nouppercase', + snippet: '\\nouppercase', + meta: 'fancyheadings-cmd', + score: 0.006416387071584083, + }, + { + caption: '\\headrule', + snippet: '\\headrule', + meta: 'fancyheadings-cmd', + score: 0.0008327432627715623, + }, + { + caption: '\\headrule{}', + snippet: '\\headrule{$1}', + meta: 'fancyheadings-cmd', + score: 0.0008327432627715623, + }, + { + caption: '\\chead{}', + snippet: '\\chead{$1}', + meta: 'fancyheadings-cmd', + score: 0.00755042164734884, + }, + { + caption: '\\headrulewidth', + snippet: '\\headrulewidth', + meta: 'fancyheadings-cmd', + score: 0.02268137935335823, + }, + ], + 'tikz-3dplot': [ + { + caption: '\\tdplotsetmaincoords{}{}', + snippet: '\\tdplotsetmaincoords{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.00021728148272883815, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-3dplot-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'tikz-3dplot-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-3dplot-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-3dplot-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikz-3dplot-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikz-3dplot-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikz-3dplot-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-3dplot-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-3dplot-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-3dplot-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikz-3dplot-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-3dplot-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tikz-3dplot-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tikz-3dplot-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-3dplot-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-3dplot-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tikz-3dplot-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikz-3dplot-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikz-3dplot-cmd', + score: 0.2864294797053033, + }, + ], + ltxtable: [ + { + caption: '\\let', + snippet: '\\let', + meta: 'ltxtable-cmd', + score: 0.03789745970461662, + }, + { + caption: '\\write', + snippet: '\\write', + meta: 'ltxtable-cmd', + score: 0.0008038857295393196, + }, + { + caption: '\\tabularxcolumn[]{}', + snippet: '\\tabularxcolumn[$1]{$2}', + meta: 'ltxtable-cmd', + score: 0.00048507499766588637, + }, + { + caption: '\\tabularxcolumn', + snippet: '\\tabularxcolumn', + meta: 'ltxtable-cmd', + score: 0.00048507499766588637, + }, + { + caption: '\\tabularx{}{}', + snippet: '\\tabularx{$1}{$2}', + meta: 'ltxtable-cmd', + score: 0.0005861357565780464, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'ltxtable-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'ltxtable-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'ltxtable-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'ltxtable-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'ltxtable-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'ltxtable-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ltxtable-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'ltxtable-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'ltxtable-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\endhead', + snippet: '\\endhead', + meta: 'ltxtable-cmd', + score: 0.0023853501147448834, + }, + { + caption: '\\endfoot', + snippet: '\\endfoot', + meta: 'ltxtable-cmd', + score: 0.00044045261916551967, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'ltxtable-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'ltxtable-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\nopagebreak', + snippet: '\\nopagebreak', + meta: 'ltxtable-cmd', + score: 9.952664522415981e-5, + }, + { + caption: '\\endfirsthead', + snippet: '\\endfirsthead', + meta: 'ltxtable-cmd', + score: 0.0016148498709822416, + }, + { + caption: '\\endlastfoot', + snippet: '\\endlastfoot', + meta: 'ltxtable-cmd', + score: 0.00044045261916551967, + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'ltxtable-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\tablename', + snippet: '\\tablename', + meta: 'ltxtable-cmd', + score: 0.0029238994233674776, + }, + { + caption: '\\pagebreak', + snippet: '\\pagebreak', + meta: 'ltxtable-cmd', + score: 0.0313525090421608, + }, + ], + pict2e: [ + { + caption: '\\Line', + snippet: '\\Line', + meta: 'pict2e-cmd', + score: 0.0006078790177929149, + }, + { + caption: '\\polygon', + snippet: '\\polygon', + meta: 'pict2e-cmd', + score: 0.0008987552240147395, + }, + { + caption: '\\line', + snippet: '\\line', + meta: 'pict2e-cmd', + score: 0.014519741542622297, + }, + { + caption: '\\polyline', + snippet: '\\polyline', + meta: 'pict2e-cmd', + score: 0.00022468880600368487, + }, + { + caption: '\\vector', + snippet: '\\vector', + meta: 'pict2e-cmd', + score: 0.002970308722584179, + }, + ], + ltablex: [ + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'ltablex-cmd', + score: 1.2569477427490174, + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'ltablex-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'ltablex-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'ltablex-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'ltablex-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'ltablex-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ltablex-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'ltablex-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'ltablex-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\let', + snippet: '\\let', + meta: 'ltablex-cmd', + score: 0.03789745970461662, + }, + { + caption: '\\write', + snippet: '\\write', + meta: 'ltablex-cmd', + score: 0.0008038857295393196, + }, + { + caption: '\\tabularxcolumn[]{}', + snippet: '\\tabularxcolumn[$1]{$2}', + meta: 'ltablex-cmd', + score: 0.00048507499766588637, + }, + { + caption: '\\tabularxcolumn', + snippet: '\\tabularxcolumn', + meta: 'ltablex-cmd', + score: 0.00048507499766588637, + }, + { + caption: '\\tabularx{}{}', + snippet: '\\tabularx{$1}{$2}', + meta: 'ltablex-cmd', + score: 0.0005861357565780464, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'ltablex-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\endhead', + snippet: '\\endhead', + meta: 'ltablex-cmd', + score: 0.0023853501147448834, + }, + { + caption: '\\endfoot', + snippet: '\\endfoot', + meta: 'ltablex-cmd', + score: 0.00044045261916551967, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'ltablex-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'ltablex-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\nopagebreak', + snippet: '\\nopagebreak', + meta: 'ltablex-cmd', + score: 9.952664522415981e-5, + }, + { + caption: '\\endfirsthead', + snippet: '\\endfirsthead', + meta: 'ltablex-cmd', + score: 0.0016148498709822416, + }, + { + caption: '\\endlastfoot', + snippet: '\\endlastfoot', + meta: 'ltablex-cmd', + score: 0.00044045261916551967, + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'ltablex-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\tablename', + snippet: '\\tablename', + meta: 'ltablex-cmd', + score: 0.0029238994233674776, + }, + { + caption: '\\pagebreak', + snippet: '\\pagebreak', + meta: 'ltablex-cmd', + score: 0.0313525090421608, + }, + ], + amsopn: [ + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'amsopn-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'amsopn-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'amsopn-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'amsopn-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'amsopn-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'amsopn-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'amsopn-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'amsopn-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'amsopn-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'amsopn-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'amsopn-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'amsopn-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'amsopn-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'amsopn-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'amsopn-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'amsopn-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'amsopn-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'amsopn-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'amsopn-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'amsopn-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'amsopn-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'amsopn-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'amsopn-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'amsopn-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'amsopn-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'amsopn-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'amsopn-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'amsopn-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'amsopn-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'amsopn-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'amsopn-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'amsopn-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'amsopn-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'amsopn-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'amsopn-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'amsopn-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'amsopn-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'amsopn-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'amsopn-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'amsopn-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'amsopn-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'amsopn-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'amsopn-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'amsopn-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'amsopn-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'amsopn-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'amsopn-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'amsopn-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'amsopn-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'amsopn-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'amsopn-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'amsopn-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'amsopn-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'amsopn-cmd', + score: 0.0063276692758974925, + }, + ], + topcoman: [ + { + caption: '\\listing{}', + snippet: '\\listing{$1}', + meta: 'topcoman-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\micro', + snippet: '\\micro', + meta: 'topcoman-cmd', + score: 0.011051971930487929, + }, + { + caption: '\\gradi', + snippet: '\\gradi', + meta: 'topcoman-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\unit[]{}', + snippet: '\\unit[$1]{$2}', + meta: 'topcoman-cmd', + score: 0.028299796173135428, + }, + { + caption: '\\unit{}', + snippet: '\\unit{$1}', + meta: 'topcoman-cmd', + score: 0.028299796173135428, + }, + { + caption: '\\ped{}', + snippet: '\\ped{$1}', + meta: 'topcoman-cmd', + score: 0.0007129548652040002, + }, + { + caption: '\\ohm', + snippet: '\\ohm', + meta: 'topcoman-cmd', + score: 0.0038146685721293138, + }, + { + caption: '\\gei', + snippet: '\\gei', + meta: 'topcoman-cmd', + score: 0.00023765162173466673, + }, + ], + topfront: [ + { + caption: '\\corsodilaurea{}', + snippet: '\\corsodilaurea{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\NomeQuartoTomo{}', + snippet: '\\NomeQuartoTomo{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\ciclodidottorato{}', + snippet: '\\ciclodidottorato{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\CorsoDiLaureaIn{}', + snippet: '\\CorsoDiLaureaIn{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\ateneo{}', + snippet: '\\ateneo{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\retrofrontespizio{}', + snippet: '\\retrofrontespizio{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\InName{}', + snippet: '\\InName{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\secondocandidato{}', + snippet: '\\secondocandidato{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\NomeMonografia{}', + snippet: '\\NomeMonografia{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\NomeTutoreAziendale{}', + snippet: '\\NomeTutoreAziendale{$1}', + meta: 'topfront-cmd', + score: 0.00047530324346933345, + }, + { + caption: '\\TutorName{}', + snippet: '\\TutorName{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\NomeDissertazione{}', + snippet: '\\NomeDissertazione{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\sedutadilaurea{}', + snippet: '\\sedutadilaurea{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\logosede{}', + snippet: '\\logosede{$1}', + meta: 'topfront-cmd', + score: 0.00047530324346933345, + }, + { + caption: '\\TesiDiLaurea{}', + snippet: '\\TesiDiLaurea{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\NomeTerzoTomo{}', + snippet: '\\NomeTerzoTomo{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\AdvisorName{}', + snippet: '\\AdvisorName{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\facolta[]{}', + snippet: '\\facolta[$1]{$2}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\CycleName{}', + snippet: '\\CycleName{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\NomePrimoTomo{}', + snippet: '\\NomePrimoTomo{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\candidato{}', + snippet: '\\candidato{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\NomeSecondoTomo{}', + snippet: '\\NomeSecondoTomo{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\titolo{}', + snippet: '\\titolo{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\CandidateName{}', + snippet: '\\CandidateName{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\secondorelatore{}', + snippet: '\\secondorelatore{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\FacoltaDi{}', + snippet: '\\FacoltaDi{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\nomeateneo{}', + snippet: '\\nomeateneo{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\DottoratoIn{}', + snippet: '\\DottoratoIn{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\sottotitolo{}', + snippet: '\\sottotitolo{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\relatore{}', + snippet: '\\relatore{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\tutoreaziendale{}', + snippet: '\\tutoreaziendale{$1}', + meta: 'topfront-cmd', + score: 0.00023765162173466673, + }, + ], + mathspec: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'mathspec-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'mathspec-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'mathspec-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'mathspec-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'mathspec-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'mathspec-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'mathspec-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'mathspec-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'mathspec-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'mathspec-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'mathspec-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'mathspec-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'mathspec-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'mathspec-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'mathspec-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'mathspec-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'mathspec-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'mathspec-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'mathspec-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mathspec-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'mathspec-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'mathspec-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'mathspec-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'mathspec-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mathspec-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'mathspec-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'mathspec-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'mathspec-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mathspec-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'mathspec-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'mathspec-cmd', + score: 0.0063276692758974925, + }, + ], + overpic: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'overpic-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'overpic-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'overpic-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'overpic-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'overpic-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'overpic-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'overpic-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'overpic-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'overpic-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'overpic-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'overpic-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'overpic-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'overpic-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'overpic-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'overpic-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'overpic-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'overpic-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'overpic-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'overpic-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'overpic-cmd', + score: 0.004719094298848707, + }, + ], + 'tkz-euclide': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tkz-euclide-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-euclide-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-euclide-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-euclide-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tkz-euclide-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tkz-euclide-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tkz-euclide-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\reserveinserts{}', + snippet: '\\reserveinserts{$1}', + meta: 'tkz-euclide-cmd', + score: 0.0018653410309739879, + }, + { + caption: '\\newtoks', + snippet: '\\newtoks', + meta: 'tkz-euclide-cmd', + score: 0.00031058155311734754, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-euclide-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-euclide-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tkz-euclide-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tkz-euclide-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tkz-euclide-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tkz-euclide-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tkz-euclide-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tkz-euclide-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tkz-euclide-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-euclide-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tkz-euclide-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tkz-euclide-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tkz-euclide-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tkz-euclide-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tkz-euclide-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tkz-euclide-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-euclide-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-euclide-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tkz-euclide-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tkz-euclide-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tkz-euclide-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tkz-euclide-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tkz-euclide-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tkz-euclide-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tkz-euclide-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tkz-euclide-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-euclide-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tkz-euclide-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tkz-euclide-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tkz-euclide-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tkz-euclide-cmd', + score: 0.2864294797053033, + }, + ], + morewrites: [ + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'morewrites-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'morewrites-cmd', + score: 0.2864294797053033, + }, + ], + pgflibraryshapes: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgflibraryshapes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgflibraryshapes-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgflibraryshapes-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgflibraryshapes-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgflibraryshapes-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgflibraryshapes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgflibraryshapes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgflibraryshapes-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgflibraryshapes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgflibraryshapes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgflibraryshapes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgflibraryshapes-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgflibraryshapes-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgflibraryshapes-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgflibraryshapes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgflibraryshapes-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgflibraryshapes-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgflibraryshapes-cmd', + score: 0.2864294797053033, + }, + ], + pdfcolparallel: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pdfcolparallel-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'pdfcolparallel-cmd', + score: 0.0058342578961340175, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'pdfcolparallel-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pdfcolparallel-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pdfcolparallel-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\ParallelRText{}', + snippet: '\\ParallelRText{$1}', + meta: 'pdfcolparallel-cmd', + score: 0.0005986518360651812, + }, + { + caption: '\\ParallelLText{}', + snippet: '\\ParallelLText{$1}', + meta: 'pdfcolparallel-cmd', + score: 0.0005986518360651812, + }, + { + caption: '\\ParallelPar', + snippet: '\\ParallelPar', + meta: 'pdfcolparallel-cmd', + score: 0.0005986518360651812, + }, + ], + aeguill: [ + { + caption: '\\guillemotleft', + snippet: '\\guillemotleft', + meta: 'aeguill-cmd', + score: 9.764370963946686e-5, + }, + { + caption: '\\guillemotright', + snippet: '\\guillemotright', + meta: 'aeguill-cmd', + score: 9.764370963946686e-5, + }, + { + caption: '\\sfdefault', + snippet: '\\sfdefault', + meta: 'aeguill-cmd', + score: 0.008427383388519996, + }, + { + caption: '\\sfdefault{}', + snippet: '\\sfdefault{$1}', + meta: 'aeguill-cmd', + score: 0.008427383388519996, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'aeguill-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'aeguill-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'aeguill-cmd', + score: 0.021170869458413965, + }, + ], + changes: [ + { + caption: '\\selectfont', + snippet: '\\selectfont', + meta: 'changes-cmd', + score: 0.04598628699063736, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'changes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'changes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'changes-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'changes-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'changes-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'changes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'changes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'changes-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'changes-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'changes-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'changes-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'changes-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'changes-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'changes-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'changes-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'changes-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'changes-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'changes-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'changes-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'changes-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'changes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'changes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'changes-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'changes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'changes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'changes-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'changes-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'changes-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'changes-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'changes-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'changes-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'changes-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'changes-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'changes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'changes-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'changes-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'changes-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'changes-cmd', + score: 0.2864294797053033, + }, + ], + droidmono: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'droidmono-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'droidmono-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'droidmono-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'droidmono-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'droidmono-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'droidmono-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\scshape', + snippet: '\\scshape', + meta: 'droidmono-cmd', + score: 0.05364108855914402, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'droidmono-cmd', + score: 0.00037306820619479756, + }, + ], + tgheros: [ + { + caption: '\\sfdefault', + snippet: '\\sfdefault', + meta: 'tgheros-cmd', + score: 0.008427383388519996, + }, + { + caption: '\\sfdefault{}', + snippet: '\\sfdefault{$1}', + meta: 'tgheros-cmd', + score: 0.008427383388519996, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgheros-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgheros-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgheros-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgheros-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgheros-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgheros-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgheros-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgheros-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tgheros-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tgheros-cmd', + score: 0.021170869458413965, + }, + ], + har2nat: [ + { + caption: '\\citeasnoun{}', + snippet: '\\citeasnoun{$1}', + meta: 'har2nat-cmd', + score: 0.010452591644582749, + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'har2nat-cmd', + score: 2.341195220791228, + }, + { + caption: '\\citealt{}', + snippet: '\\citealt{$1}', + meta: 'har2nat-cmd', + score: 0.007302105441724955, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'har2nat-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'har2nat-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\textsuperscript{}', + snippet: '\\textsuperscript{$1}', + meta: 'har2nat-cmd', + score: 0.05216393882408519, + }, + { + caption: '\\nocite{}', + snippet: '\\nocite{$1}', + meta: 'har2nat-cmd', + score: 0.04990693820960752, + }, + { + caption: '\\bibname', + snippet: '\\bibname', + meta: 'har2nat-cmd', + score: 0.007599529252128519, + }, + { + caption: '\\bibname{}', + snippet: '\\bibname{$1}', + meta: 'har2nat-cmd', + score: 0.007599529252128519, + }, + { + caption: '\\bibpunct', + snippet: '\\bibpunct', + meta: 'har2nat-cmd', + score: 0.001148574749873469, + }, + { + caption: '\\bibpunct{}{}{}{}{}{}', + snippet: '\\bibpunct{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'har2nat-cmd', + score: 0.001148574749873469, + }, + { + caption: '\\bibpunct[]{}{}{}{}{}{}', + snippet: '\\bibpunct[$1]{$2}{$3}{$4}{$5}{$6}{$7}', + meta: 'har2nat-cmd', + score: 0.001148574749873469, + }, + { + caption: '\\citepalias{}', + snippet: '\\citepalias{$1}', + meta: 'har2nat-cmd', + score: 0.00032712684909035603, + }, + { + caption: '\\citepalias[][]{}', + snippet: '\\citepalias[$1][$2]{$3}', + meta: 'har2nat-cmd', + score: 0.00032712684909035603, + }, + { + caption: '\\makeindex', + snippet: '\\makeindex', + meta: 'har2nat-cmd', + score: 0.010304996748556729, + }, + { + caption: '\\citep{}', + snippet: '\\citep{$1}', + meta: 'har2nat-cmd', + score: 0.2941882834697057, + }, + { + caption: '\\bibsection', + snippet: '\\bibsection', + meta: 'har2nat-cmd', + score: 0.00038872734530908233, + }, + { + caption: '\\bibsection{}', + snippet: '\\bibsection{$1}', + meta: 'har2nat-cmd', + score: 0.00038872734530908233, + }, + { + caption: '\\refname', + snippet: '\\refname', + meta: 'har2nat-cmd', + score: 0.006490238196722249, + }, + { + caption: '\\refname{}', + snippet: '\\refname{$1}', + meta: 'har2nat-cmd', + score: 0.006490238196722249, + }, + { + caption: '\\citealp{}', + snippet: '\\citealp{$1}', + meta: 'har2nat-cmd', + score: 0.005275912376595364, + }, + { + caption: '\\citealp[]{}', + snippet: '\\citealp[$1]{$2}', + meta: 'har2nat-cmd', + score: 0.005275912376595364, + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'har2nat-cmd', + score: 2.341195220791228, + }, + { + caption: '\\citetalias{}', + snippet: '\\citetalias{$1}', + meta: 'har2nat-cmd', + score: 0.001419571355756266, + }, + { + caption: '\\bibitem{}', + snippet: '\\bibitem{$1}', + meta: 'har2nat-cmd', + score: 0.3689547570562042, + }, + { + caption: '\\bibitem[]{}', + snippet: '\\bibitem[$1]{$2}', + meta: 'har2nat-cmd', + score: 0.3689547570562042, + }, + { + caption: '\\citet{}', + snippet: '\\citet{$1}', + meta: 'har2nat-cmd', + score: 0.09046048561361801, + }, + { + caption: '\\defcitealias{}{}', + snippet: '\\defcitealias{$1}{$2}', + meta: 'har2nat-cmd', + score: 0.00042021825647418025, + }, + { + caption: '\\aftergroup', + snippet: '\\aftergroup', + meta: 'har2nat-cmd', + score: 0.002020423627422133, + }, + { + caption: '\\setcitestyle{}', + snippet: '\\setcitestyle{$1}', + meta: 'har2nat-cmd', + score: 0.0015840652870152204, + }, + { + caption: '\\citeyearpar{}', + snippet: '\\citeyearpar{$1}', + meta: 'har2nat-cmd', + score: 0.001877888310324327, + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'har2nat-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'har2nat-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\newblock', + snippet: '\\newblock', + meta: 'har2nat-cmd', + score: 0.03684301726876973, + }, + { + caption: '\\newblock{}', + snippet: '\\newblock{$1}', + meta: 'har2nat-cmd', + score: 0.03684301726876973, + }, + { + caption: '\\bibnumfmt', + snippet: '\\bibnumfmt', + meta: 'har2nat-cmd', + score: 0.000353353600267394, + }, + { + caption: '\\citeyear{}', + snippet: '\\citeyear{$1}', + meta: 'har2nat-cmd', + score: 0.01091041305836494, + }, + { + caption: '\\citeauthor{}', + snippet: '\\citeauthor{$1}', + meta: 'har2nat-cmd', + score: 0.01359248786373484, + }, + { + caption: '\\let', + snippet: '\\let', + meta: 'har2nat-cmd', + score: 0.03789745970461662, + }, + ], + 'matlab-prettifier': [ + { + caption: '\\mlttfamily', + snippet: '\\mlttfamily', + meta: 'matlab-prettifier-cmd', + score: 0.000856282742498241, + }, + { + caption: '\\vskip', + snippet: '\\vskip', + meta: 'matlab-prettifier-cmd', + score: 0.05143052892347224, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'matlab-prettifier-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'matlab-prettifier-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'matlab-prettifier-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\thelstlisting', + snippet: '\\thelstlisting', + meta: 'matlab-prettifier-cmd', + score: 0.00012774128088872144, + }, + { + caption: '\\lstinputlisting[]{}', + snippet: '\\lstinputlisting[$1]{$2}', + meta: 'matlab-prettifier-cmd', + score: 0.011660477607086044, + }, + { + caption: '\\lstinputlisting{}', + snippet: '\\lstinputlisting{$1}', + meta: 'matlab-prettifier-cmd', + score: 0.011660477607086044, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'matlab-prettifier-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'matlab-prettifier-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\lstinline', + snippet: '\\lstinline', + meta: 'matlab-prettifier-cmd', + score: 0.005972262850694285, + }, + { + caption: '\\lstinline{}', + snippet: '\\lstinline{$1}', + meta: 'matlab-prettifier-cmd', + score: 0.005972262850694285, + }, + { + caption: '\\lstlistoflistings', + snippet: '\\lstlistoflistings', + meta: 'matlab-prettifier-cmd', + score: 0.005279080363360602, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'matlab-prettifier-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'matlab-prettifier-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'matlab-prettifier-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'matlab-prettifier-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'matlab-prettifier-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'matlab-prettifier-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'matlab-prettifier-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'matlab-prettifier-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'matlab-prettifier-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'matlab-prettifier-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'matlab-prettifier-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'matlab-prettifier-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'matlab-prettifier-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'matlab-prettifier-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'matlab-prettifier-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'matlab-prettifier-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'matlab-prettifier-cmd', + score: 0.2864294797053033, + }, + ], + datetime2: [ + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'datetime2-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'datetime2-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'datetime2-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'datetime2-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'datetime2-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'datetime2-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'datetime2-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'datetime2-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'datetime2-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'datetime2-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'datetime2-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'datetime2-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'datetime2-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'datetime2-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'datetime2-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'datetime2-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'datetime2-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'datetime2-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'datetime2-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'datetime2-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'datetime2-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'datetime2-cmd', + score: 0.008565354665444157, + }, + ], + lapdf: [ + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'lapdf-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'lapdf-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'lapdf-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'lapdf-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'lapdf-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'lapdf-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'lapdf-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'lapdf-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'lapdf-cmd', + score: 0.028955796305270766, + }, + ], + nccbbb: [ + { + caption: '\\bbbe', + snippet: '\\bbbe', + meta: 'nccbbb-cmd', + score: 0.0013332214754983353, + }, + { + caption: '\\bbbe[]', + snippet: '\\bbbe[$1]', + meta: 'nccbbb-cmd', + score: 0.0013332214754983353, + }, + { + caption: '\\bbbr', + snippet: '\\bbbr', + meta: 'nccbbb-cmd', + score: 0.0015739010274051707, + }, + ], + tgbonum: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgbonum-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgbonum-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgbonum-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgbonum-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgbonum-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgbonum-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgbonum-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgbonum-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tgbonum-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tgbonum-cmd', + score: 0.021170869458413965, + }, + ], + 'thm-restate': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'thm-restate-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\listtheoremname', + snippet: '\\listtheoremname', + meta: 'thm-restate-cmd', + score: 1.9443373798666845e-5, + }, + { + caption: '\\thmtformatoptarg', + snippet: '\\thmtformatoptarg', + meta: 'thm-restate-cmd', + score: 6.353668036093916e-5, + }, + { + caption: '\\listoftheorems[]', + snippet: '\\listoftheorems[$1]', + meta: 'thm-restate-cmd', + score: 1.9443373798666845e-5, + }, + { + caption: '\\declaretheoremstyle[]{}', + snippet: '\\declaretheoremstyle[$1]{$2}', + meta: 'thm-restate-cmd', + score: 0.0001168034231635369, + }, + { + caption: '\\declaretheorem[]{}', + snippet: '\\declaretheorem[$1]{$2}', + meta: 'thm-restate-cmd', + score: 0.0004904790216915127, + }, + { + caption: '\\theoremstyle{}', + snippet: '\\theoremstyle{$1}', + meta: 'thm-restate-cmd', + score: 0.02533412165007986, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'thm-restate-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'thm-restate-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'thm-restate-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\proof{}', + snippet: '\\proof{$1}', + meta: 'thm-restate-cmd', + score: 0.000701497773639073, + }, + { + caption: '\\proof', + snippet: '\\proof', + meta: 'thm-restate-cmd', + score: 0.000701497773639073, + }, + { + caption: '\\newtheorem{}[]{}', + snippet: '\\newtheorem{$1}[$2]{$3}', + meta: 'thm-restate-cmd', + score: 0.215689795055434, + }, + { + caption: '\\newtheorem{}{}', + snippet: '\\newtheorem{$1}{$2}', + meta: 'thm-restate-cmd', + score: 0.215689795055434, + }, + { + caption: '\\newtheorem{}{}[]', + snippet: '\\newtheorem{$1}{$2}[$3]', + meta: 'thm-restate-cmd', + score: 0.215689795055434, + }, + { + caption: '\\endproof', + snippet: '\\endproof', + meta: 'thm-restate-cmd', + score: 0.0006133100544751855, + }, + { + caption: '\\endproof{}', + snippet: '\\endproof{$1}', + meta: 'thm-restate-cmd', + score: 0.0006133100544751855, + }, + ], + 'biblatex-chicago': [ + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'biblatex-chicago-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'biblatex-chicago-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'biblatex-chicago-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'biblatex-chicago-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'biblatex-chicago-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'biblatex-chicago-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'biblatex-chicago-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'biblatex-chicago-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'biblatex-chicago-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'biblatex-chicago-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'biblatex-chicago-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'biblatex-chicago-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'biblatex-chicago-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'biblatex-chicago-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'biblatex-chicago-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'biblatex-chicago-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'biblatex-chicago-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'biblatex-chicago-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'biblatex-chicago-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'biblatex-chicago-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'biblatex-chicago-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'biblatex-chicago-cmd', + score: 0.008565354665444157, + }, + ], + pseudocode: [ + { + caption: '\\shadowbox{}', + snippet: '\\shadowbox{$1}', + meta: 'pseudocode-cmd', + score: 0.00107667147399019, + }, + { + caption: '\\doublebox', + snippet: '\\doublebox', + meta: 'pseudocode-cmd', + score: 0.00015142240898356106, + }, + { + caption: '\\VerbatimEnvironment', + snippet: '\\VerbatimEnvironment', + meta: 'pseudocode-cmd', + score: 4.5350034239275855e-5, + }, + { + caption: '\\thisfancypage{}{}', + snippet: '\\thisfancypage{$1}{$2}', + meta: 'pseudocode-cmd', + score: 0.00015142240898356106, + }, + { + caption: '\\TheSbox', + snippet: '\\TheSbox', + meta: 'pseudocode-cmd', + score: 4.5350034239275855e-5, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'pseudocode-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'pseudocode-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'pseudocode-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'pseudocode-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'pseudocode-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'pseudocode-cmd', + score: 0.0018957469739775527, + }, + ], + imakeidx: [ + { + caption: '\\makeindex', + snippet: '\\makeindex', + meta: 'imakeidx-cmd', + score: 0.010304996748556729, + }, + { + caption: '\\printindex', + snippet: '\\printindex', + meta: 'imakeidx-cmd', + score: 0.004417016910870522, + }, + { + caption: '\\index{}', + snippet: '\\index{$1}', + meta: 'imakeidx-cmd', + score: 0.013774721817648336, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'imakeidx-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'imakeidx-cmd', + score: 0.008565354665444157, + }, + ], + uri: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'uri-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'uri-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'uri-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'uri-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'uri-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'uri-cmd', + score: 0.0002854206807593436, + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'uri-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'uri-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'uri-cmd', + score: 0.010515056688180681, + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'uri-cmd', + score: 0.008041789461944983, + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'uri-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'uri-cmd', + score: 0.0032990580087398644, + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'uri-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'uri-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'uri-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'uri-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'uri-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'uri-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'uri-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'uri-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'uri-cmd', + score: 0.021170869458413965, + }, + ], + tocvsec2: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'tocvsec2-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'tocvsec2-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'tocvsec2-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'tocvsec2-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'tocvsec2-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'tocvsec2-cmd', + score: 0.0018957469739775527, + }, + ], + graphbox: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'graphbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'graphbox-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'graphbox-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'graphbox-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'graphbox-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'graphbox-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'graphbox-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'graphbox-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'graphbox-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'graphbox-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'graphbox-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'graphbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'graphbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'graphbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'graphbox-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'graphbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'graphbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'graphbox-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'graphbox-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'graphbox-cmd', + score: 0.008565354665444157, + }, + ], + limap: [ + { + caption: '\\MapContinuing{}', + snippet: '\\MapContinuing{$1}', + meta: 'limap-cmd', + score: 7.216282820556303e-5, + }, + { + caption: '\\MapTextFraction{}', + snippet: '\\MapTextFraction{$1}', + meta: 'limap-cmd', + score: 7.216282820556303e-5, + }, + { + caption: '\\MapBlockLabelFont{}', + snippet: '\\MapBlockLabelFont{$1}', + meta: 'limap-cmd', + score: 7.216282820556303e-5, + }, + { + caption: '\\Block{}', + snippet: '\\Block{$1}', + meta: 'limap-cmd', + score: 0.011618215341095648, + }, + { + caption: '\\MapRuleWidth{}', + snippet: '\\MapRuleWidth{$1}', + meta: 'limap-cmd', + score: 7.216282820556303e-5, + }, + { + caption: '\\MapTitleFraction{}', + snippet: '\\MapTitleFraction{$1}', + meta: 'limap-cmd', + score: 7.216282820556303e-5, + }, + { + caption: '\\MapContinued{}', + snippet: '\\MapContinued{$1}', + meta: 'limap-cmd', + score: 7.216282820556303e-5, + }, + { + caption: '\\WideBlock{}', + snippet: '\\WideBlock{$1}', + meta: 'limap-cmd', + score: 0.002453536158989143, + }, + { + caption: '\\MapParskip{}', + snippet: '\\MapParskip{$1}', + meta: 'limap-cmd', + score: 7.216282820556303e-5, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'limap-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'limap-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'limap-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'limap-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'limap-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'limap-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'limap-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'limap-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'limap-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'limap-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'limap-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'limap-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'limap-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'limap-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'limap-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'limap-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'limap-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'limap-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'limap-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'limap-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'limap-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'limap-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\specialrule{}{}{}', + snippet: '\\specialrule{$1}{$2}{$3}', + meta: 'limap-cmd', + score: 0.004974385202605165, + }, + { + caption: '\\cmidrule', + snippet: '\\cmidrule', + meta: 'limap-cmd', + score: 0.01894952272365088, + }, + { + caption: '\\cmidrule{}', + snippet: '\\cmidrule{$1}', + meta: 'limap-cmd', + score: 0.01894952272365088, + }, + { + caption: '\\bottomrule', + snippet: '\\bottomrule', + meta: 'limap-cmd', + score: 0.04533364657852219, + }, + { + caption: '\\midrule', + snippet: '\\midrule', + meta: 'limap-cmd', + score: 0.07098077735912875, + }, + { + caption: '\\addlinespace', + snippet: '\\addlinespace', + meta: 'limap-cmd', + score: 0.005865460617491447, + }, + { + caption: '\\addlinespace[]', + snippet: '\\addlinespace[$1]', + meta: 'limap-cmd', + score: 0.005865460617491447, + }, + { + caption: '\\toprule', + snippet: '\\toprule', + meta: 'limap-cmd', + score: 0.059857788139528495, + }, + { + caption: '\\endhead', + snippet: '\\endhead', + meta: 'limap-cmd', + score: 0.0023853501147448834, + }, + { + caption: '\\endfoot', + snippet: '\\endfoot', + meta: 'limap-cmd', + score: 0.00044045261916551967, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'limap-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'limap-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\nopagebreak', + snippet: '\\nopagebreak', + meta: 'limap-cmd', + score: 9.952664522415981e-5, + }, + { + caption: '\\endfirsthead', + snippet: '\\endfirsthead', + meta: 'limap-cmd', + score: 0.0016148498709822416, + }, + { + caption: '\\endlastfoot', + snippet: '\\endlastfoot', + meta: 'limap-cmd', + score: 0.00044045261916551967, + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'limap-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\tablename', + snippet: '\\tablename', + meta: 'limap-cmd', + score: 0.0029238994233674776, + }, + { + caption: '\\pagebreak', + snippet: '\\pagebreak', + meta: 'limap-cmd', + score: 0.0313525090421608, + }, + ], + tikzscale: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikzscale-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikzscale-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikzscale-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikzscale-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikzscale-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikzscale-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikzscale-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikzscale-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikzscale-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikzscale-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzscale-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikzscale-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikzscale-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikzscale-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikzscale-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikzscale-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikzscale-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikzscale-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikzscale-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzscale-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzscale-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'tikzscale-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'tikzscale-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'tikzscale-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'tikzscale-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'tikzscale-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'tikzscale-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'tikzscale-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'tikzscale-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'tikzscale-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'tikzscale-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'tikzscale-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'tikzscale-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'tikzscale-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'tikzscale-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'tikzscale-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'tikzscale-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikzscale-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'tikzscale-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'tikzscale-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'tikzscale-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'tikzscale-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikzscale-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikzscale-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikzscale-cmd', + score: 0.2864294797053033, + }, + ], + savesym: [ + { + caption: '\\savesymbol{}', + snippet: '\\savesymbol{$1}', + meta: 'savesym-cmd', + score: 6.662041157021826e-5, + }, + ], + subscript: [ + { + caption: '\\textsubscript{}', + snippet: '\\textsubscript{$1}', + meta: 'subscript-cmd', + score: 0.058405875394131175, + }, + ], + letterspace: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'letterspace-cmd', + score: 0.00037306820619479756, + }, + ], + mathastext: [ + { + caption: '\\Huge', + snippet: '\\Huge', + meta: 'mathastext-cmd', + score: 0.04725806985998919, + }, + { + caption: '\\sfdefault', + snippet: '\\sfdefault', + meta: 'mathastext-cmd', + score: 0.008427383388519996, + }, + { + caption: '\\sfdefault{}', + snippet: '\\sfdefault{$1}', + meta: 'mathastext-cmd', + score: 0.008427383388519996, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'mathastext-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\mathrm{}', + snippet: '\\mathrm{$1}', + meta: 'mathastext-cmd', + score: 0.19117752976172653, + }, + ], + movie15: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'movie15-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'movie15-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'movie15-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'movie15-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'movie15-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'movie15-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'movie15-cmd', + score: 0.0018957469739775527, + }, + ], + refstyle: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'refstyle-cmd', + score: 0.00037306820619479756, + }, + ], + 'pst-3d': [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pst-3d-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pst-3d-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pst-3d-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pst-3d-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pst-3d-cmd', + score: 0.0005786730478266738, + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pst-3d-cmd', + score: 0.006520475264573554, + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pst-3d-cmd', + score: 0.006520475264573554, + }, + ], + rotfloat: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'rotfloat-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\listof{}{}', + snippet: '\\listof{$1}{$2}', + meta: 'rotfloat-cmd', + score: 0.0009837365348002915, + }, + { + caption: '\\floatplacement{}{}', + snippet: '\\floatplacement{$1}{$2}', + meta: 'rotfloat-cmd', + score: 0.0005815474978918903, + }, + { + caption: '\\restylefloat{}', + snippet: '\\restylefloat{$1}', + meta: 'rotfloat-cmd', + score: 0.0008866338267686714, + }, + { + caption: '\\floatstyle{}', + snippet: '\\floatstyle{$1}', + meta: 'rotfloat-cmd', + score: 0.0015470917047414941, + }, + { + caption: '\\floatname{}{}', + snippet: '\\floatname{$1}{$2}', + meta: 'rotfloat-cmd', + score: 0.0011934321931750752, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rotfloat-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'rotfloat-cmd', + score: 1.2569477427490174, + }, + { + caption: '\\newfloat{}{}{}', + snippet: '\\newfloat{$1}{$2}{$3}', + meta: 'rotfloat-cmd', + score: 0.0012745874472536625, + }, + { + caption: '\\newfloat', + snippet: '\\newfloat', + meta: 'rotfloat-cmd', + score: 0.0012745874472536625, + }, + { + caption: '\\newfloat{}', + snippet: '\\newfloat{$1}', + meta: 'rotfloat-cmd', + score: 0.0012745874472536625, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rotfloat-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'rotfloat-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'rotfloat-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'rotfloat-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'rotfloat-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'rotfloat-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'rotfloat-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'rotfloat-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'rotfloat-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'rotfloat-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rotfloat-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'rotfloat-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'rotfloat-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'rotfloat-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'rotfloat-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'rotfloat-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'rotfloat-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'rotfloat-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'rotfloat-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'rotfloat-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'rotfloat-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'rotfloat-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'rotfloat-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'rotfloat-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'rotfloat-cmd', + score: 0.004719094298848707, + }, + ], + progressbar: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'progressbar-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'progressbar-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'progressbar-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'progressbar-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'progressbar-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'progressbar-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'progressbar-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'progressbar-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'progressbar-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'progressbar-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'progressbar-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'progressbar-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'progressbar-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'progressbar-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'progressbar-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'progressbar-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'progressbar-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'progressbar-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'progressbar-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'progressbar-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'progressbar-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'progressbar-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'progressbar-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'progressbar-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'progressbar-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'progressbar-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'progressbar-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'progressbar-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'progressbar-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'progressbar-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'progressbar-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'progressbar-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'progressbar-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'progressbar-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'progressbar-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'progressbar-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'progressbar-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'progressbar-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'progressbar-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'progressbar-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'progressbar-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'progressbar-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'progressbar-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'progressbar-cmd', + score: 0.008565354665444157, + }, + ], + pagecolor: [ + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pagecolor-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pagecolor-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pagecolor-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pagecolor-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pagecolor-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pagecolor-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pagecolor-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pagecolor-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pagecolor-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pagecolor-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pagecolor-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pagecolor-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pagecolor-cmd', + score: 0.021170869458413965, + }, + ], + gb4e: [ + { + caption: '\\ex', + snippet: '\\ex', + meta: 'gb4e-cmd', + score: 0.00916111174873264, + }, + ], + ESIEEcv: [ + { + caption: '\\let', + snippet: '\\let', + meta: 'ESIEEcv-cmd', + score: 0.03789745970461662, + }, + { + caption: '\\write', + snippet: '\\write', + meta: 'ESIEEcv-cmd', + score: 0.0008038857295393196, + }, + { + caption: '\\tabularxcolumn[]{}', + snippet: '\\tabularxcolumn[$1]{$2}', + meta: 'ESIEEcv-cmd', + score: 0.00048507499766588637, + }, + { + caption: '\\tabularxcolumn', + snippet: '\\tabularxcolumn', + meta: 'ESIEEcv-cmd', + score: 0.00048507499766588637, + }, + { + caption: '\\tabularx{}{}', + snippet: '\\tabularx{$1}{$2}', + meta: 'ESIEEcv-cmd', + score: 0.0005861357565780464, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'ESIEEcv-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'ESIEEcv-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'ESIEEcv-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'ESIEEcv-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'ESIEEcv-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'ESIEEcv-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ESIEEcv-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'ESIEEcv-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'ESIEEcv-cmd', + score: 0.018615449342361392, + }, + ], + ftnright: [ + { + caption: '\\footnotesize', + snippet: '\\footnotesize', + meta: 'ftnright-cmd', + score: 0.2038592081252624, + }, + { + caption: '\\footnotesize{}', + snippet: '\\footnotesize{$1}', + meta: 'ftnright-cmd', + score: 0.2038592081252624, + }, + ], + chemformula: [ + { + caption: '\\ch{}', + snippet: '\\ch{$1}', + meta: 'chemformula-cmd', + score: 0.0013276105116845872, + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'chemformula-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'chemformula-cmd', + score: 0.1789117552185788, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\nicefrac{}{}', + snippet: '\\nicefrac{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.0018011350423659288, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'chemformula-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'chemformula-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'chemformula-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'chemformula-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'chemformula-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'chemformula-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'chemformula-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'chemformula-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'chemformula-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'chemformula-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'chemformula-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'chemformula-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'chemformula-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'chemformula-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'chemformula-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'chemformula-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'chemformula-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'chemformula-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'chemformula-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'chemformula-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'chemformula-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'chemformula-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'chemformula-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'chemformula-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'chemformula-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'chemformula-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'chemformula-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'chemformula-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'chemformula-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'chemformula-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'chemformula-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'chemformula-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'chemformula-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'chemformula-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'chemformula-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'chemformula-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'chemformula-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'chemformula-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'chemformula-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'chemformula-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'chemformula-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'chemformula-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'chemformula-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'chemformula-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'chemformula-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'chemformula-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'chemformula-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'chemformula-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'chemformula-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'chemformula-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'chemformula-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'chemformula-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'chemformula-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemformula-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'chemformula-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'chemformula-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chemformula-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chemformula-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'chemformula-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'chemformula-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'chemformula-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'chemformula-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chemformula-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'chemformula-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemformula-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'chemformula-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chemformula-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chemformula-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'chemformula-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'chemformula-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chemformula-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chemformula-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'chemformula-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'chemformula-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'chemformula-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chemformula-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'chemformula-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemformula-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'chemformula-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'chemformula-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'chemformula-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'chemformula-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'chemformula-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'chemformula-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'chemformula-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'chemformula-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'chemformula-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'chemformula-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'chemformula-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'chemformula-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'chemformula-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'chemformula-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'chemformula-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'chemformula-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'chemformula-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'chemformula-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'chemformula-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'chemformula-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'chemformula-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'chemformula-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'chemformula-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'chemformula-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'chemformula-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'chemformula-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'chemformula-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'chemformula-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'chemformula-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'chemformula-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'chemformula-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'chemformula-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'chemformula-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'chemformula-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'chemformula-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'chemformula-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'chemformula-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'chemformula-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'chemformula-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'chemformula-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'chemformula-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'chemformula-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'chemformula-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'chemformula-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'chemformula-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'chemformula-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'chemformula-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'chemformula-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'chemformula-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'chemformula-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'chemformula-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'chemformula-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'chemformula-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'chemformula-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'chemformula-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'chemformula-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'chemformula-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'chemformula-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'chemformula-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'chemformula-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'chemformula-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'chemformula-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'chemformula-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'chemformula-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'chemformula-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'chemformula-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'chemformula-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'chemformula-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'chemformula-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'chemformula-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'chemformula-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'chemformula-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'chemformula-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'chemformula-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'chemformula-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'chemformula-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'chemformula-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'chemformula-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'chemformula-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'chemformula-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'chemformula-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'chemformula-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'chemformula-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'chemformula-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'chemformula-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'chemformula-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'chemformula-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'chemformula-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'chemformula-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'chemformula-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'chemformula-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'chemformula-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'chemformula-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'chemformula-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chemformula-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chemformula-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\sfrac{}{}', + snippet: '\\sfrac{$1}{$2}', + meta: 'chemformula-cmd', + score: 0.0030164694688453453, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemformula-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'chemformula-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'chemformula-cmd', + score: 0.0063276692758974925, + }, + ], + pgfautomata: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfautomata-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfautomata-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfautomata-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfautomata-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfautomata-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfautomata-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfautomata-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfautomata-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfautomata-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfautomata-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfautomata-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfautomata-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfautomata-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfautomata-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfautomata-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfautomata-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfautomata-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfautomata-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfautomata-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfautomata-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfautomata-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfautomata-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfautomata-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfautomata-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfautomata-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfautomata-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfautomata-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfautomata-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfautomata-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfautomata-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfautomata-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfautomata-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfautomata-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfautomata-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfautomata-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfautomata-cmd', + score: 0.2864294797053033, + }, + ], + pgfnodes: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfnodes-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfnodes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfnodes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfnodes-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfnodes-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfnodes-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfnodes-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfnodes-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfnodes-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfnodes-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfnodes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfnodes-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfnodes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfnodes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfnodes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfnodes-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfnodes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfnodes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfnodes-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfnodes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfnodes-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfnodes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfnodes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfnodes-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfnodes-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfnodes-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfnodes-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfnodes-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfnodes-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfnodes-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfnodes-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfnodes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfnodes-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfnodes-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfnodes-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfnodes-cmd', + score: 0.2864294797053033, + }, + ], + pgfarrows: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfarrows-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfarrows-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfarrows-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfarrows-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfarrows-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfarrows-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfarrows-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfarrows-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfarrows-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfarrows-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfarrows-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfarrows-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfarrows-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfarrows-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfarrows-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfarrows-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfarrows-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfarrows-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfarrows-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfarrows-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfarrows-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfarrows-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfarrows-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfarrows-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfarrows-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfarrows-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfarrows-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfarrows-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfarrows-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfarrows-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfarrows-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfarrows-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfarrows-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfarrows-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfarrows-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfarrows-cmd', + score: 0.2864294797053033, + }, + ], + 'pst-text': [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pst-text-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pst-text-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pst-text-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pst-text-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pst-text-cmd', + score: 0.0005786730478266738, + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pst-text-cmd', + score: 0.006520475264573554, + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pst-text-cmd', + score: 0.006520475264573554, + }, + ], + keystroke: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'keystroke-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'keystroke-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'keystroke-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'keystroke-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'keystroke-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'keystroke-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'keystroke-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'keystroke-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'keystroke-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'keystroke-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'keystroke-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'keystroke-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'keystroke-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'keystroke-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'keystroke-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'keystroke-cmd', + score: 0.004649150613625593, + }, + ], + currvita: [ + { + caption: '\\cvheadingfont', + snippet: '\\cvheadingfont', + meta: 'currvita-cmd', + score: 5.547871753177405e-5, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'currvita-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'currvita-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'currvita-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'currvita-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'currvita-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'currvita-cmd', + score: 0.0018957469739775527, + }, + ], + subfigmat: [ + { + caption: '\\subfigure[]{}', + snippet: '\\subfigure[$1]{$2}', + meta: 'subfigmat-cmd', + score: 0.037856842641104005, + }, + { + caption: '\\subref{}', + snippet: '\\subref{$1}', + meta: 'subfigmat-cmd', + score: 0.007192033516871399, + }, + { + caption: '\\subfigure[]{}', + snippet: '\\subfigure[$1]{$2}', + meta: 'subfigmat-cmd', + score: 0.037856842641104005, + }, + ], + boxhandler: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'boxhandler-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'boxhandler-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'boxhandler-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'boxhandler-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'boxhandler-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'boxhandler-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\pbox{}{}', + snippet: '\\pbox{$1}{$2}', + meta: 'boxhandler-cmd', + score: 0.0010883030320478486, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'boxhandler-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'boxhandler-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'boxhandler-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'boxhandler-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'boxhandler-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'boxhandler-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'boxhandler-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'boxhandler-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'boxhandler-cmd', + score: 0.028955796305270766, + }, + ], + media9: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'media9-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'media9-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'media9-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'media9-cmd', + score: 0.00047530324346933345, + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'media9-cmd', + score: 0.0005277905480209891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'media9-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'media9-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'media9-cmd', + score: 0.2864294797053033, + }, + ], + translator: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'translator-cmd', + score: 0.00037306820619479756, + }, + ], + german: [ + { + caption: '\\today', + snippet: '\\today', + meta: 'german-cmd', + score: 0.10733849317324783, + }, + ], + mhsetup: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mhsetup-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mhsetup-cmd', + score: 0.021170869458413965, + }, + ], + nomentbl: [ + { + caption: '\\nomenclature[]{}{}', + snippet: '\\nomenclature[$1]{$2}{$3}', + meta: 'nomentbl-cmd', + score: 0.016053526743355948, + }, + { + caption: '\\nomenclature{}{}', + snippet: '\\nomenclature{$1}{$2}', + meta: 'nomentbl-cmd', + score: 0.016053526743355948, + }, + { + caption: '\\nomlabel', + snippet: '\\nomlabel', + meta: 'nomentbl-cmd', + score: 6.353668036093916e-5, + }, + { + caption: '\\printnomenclature', + snippet: '\\printnomenclature', + meta: 'nomentbl-cmd', + score: 0.0014526113324237952, + }, + { + caption: '\\printnomenclature[]', + snippet: '\\printnomenclature[$1]', + meta: 'nomentbl-cmd', + score: 0.0014526113324237952, + }, + { + caption: '\\makenomenclature', + snippet: '\\makenomenclature', + meta: 'nomentbl-cmd', + score: 0.002310610204652063, + }, + { + caption: '\\nomgroup', + snippet: '\\nomgroup', + meta: 'nomentbl-cmd', + score: 0.0005549290951493257, + }, + { + caption: '\\nomgroup[]{}', + snippet: '\\nomgroup[$1]{$2}', + meta: 'nomentbl-cmd', + score: 0.0005549290951493257, + }, + { + caption: '\\nomname', + snippet: '\\nomname', + meta: 'nomentbl-cmd', + score: 0.0015092617929470952, + }, + { + caption: '\\nompreamble', + snippet: '\\nompreamble', + meta: 'nomentbl-cmd', + score: 2.4350510995473236e-5, + }, + { + caption: '\\nomentryend', + snippet: '\\nomentryend', + meta: 'nomentbl-cmd', + score: 0.000137692304514793, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'nomentbl-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'nomentbl-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'nomentbl-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'nomentbl-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'nomentbl-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'nomentbl-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'nomentbl-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'nomentbl-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'nomentbl-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'nomentbl-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'nomentbl-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'nomentbl-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'nomentbl-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'nomentbl-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'nomentbl-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'nomentbl-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'nomentbl-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'nomentbl-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'nomentbl-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'nomentbl-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'nomentbl-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'nomentbl-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'nomentbl-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\endhead', + snippet: '\\endhead', + meta: 'nomentbl-cmd', + score: 0.0023853501147448834, + }, + { + caption: '\\endfoot', + snippet: '\\endfoot', + meta: 'nomentbl-cmd', + score: 0.00044045261916551967, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'nomentbl-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'nomentbl-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\nopagebreak', + snippet: '\\nopagebreak', + meta: 'nomentbl-cmd', + score: 9.952664522415981e-5, + }, + { + caption: '\\endfirsthead', + snippet: '\\endfirsthead', + meta: 'nomentbl-cmd', + score: 0.0016148498709822416, + }, + { + caption: '\\endlastfoot', + snippet: '\\endlastfoot', + meta: 'nomentbl-cmd', + score: 0.00044045261916551967, + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'nomentbl-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\tablename', + snippet: '\\tablename', + meta: 'nomentbl-cmd', + score: 0.0029238994233674776, + }, + { + caption: '\\pagebreak', + snippet: '\\pagebreak', + meta: 'nomentbl-cmd', + score: 0.0313525090421608, + }, + ], + miller: [ + { + caption: '\\hkl', + snippet: '\\hkl', + meta: 'miller-cmd', + score: 0.0034259481311452946, + }, + { + caption: '\\hkl{}', + snippet: '\\hkl{$1}', + meta: 'miller-cmd', + score: 0.0034259481311452946, + }, + { + caption: '\\hkl[]', + snippet: '\\hkl[$1]', + meta: 'miller-cmd', + score: 0.0034259481311452946, + }, + ], + lpform: [ + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'lpform-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'lpform-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'lpform-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'lpform-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'lpform-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'lpform-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'lpform-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'lpform-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'lpform-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'lpform-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'lpform-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'lpform-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'lpform-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'lpform-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'lpform-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'lpform-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'lpform-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'lpform-cmd', + score: 0.009331077109224957, + }, + ], + xepersian: [ + { + caption: '\\settextfont[]{}', + snippet: '\\settextfont[$1]{$2}', + meta: 'xepersian-cmd', + score: 0.00015447355412753335, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'xepersian-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xepersian-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xepersian-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'xepersian-cmd', + score: 0.00047530324346933345, + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'xepersian-cmd', + score: 0.0005277905480209891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xepersian-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xepersian-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'xepersian-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'xepersian-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xepersian-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xepersian-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xepersian-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'xepersian-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xepersian-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xepersian-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'xepersian-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'xepersian-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'xepersian-cmd', + score: 0.0058342578961340175, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'xepersian-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xepersian-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xepersian-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xepersian-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xepersian-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'xepersian-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xepersian-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xepersian-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xepersian-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'xepersian-cmd', + score: 0.002958865219480927, + }, + ], + chapterbib: [ + { + caption: '\\bibliographystyle{}', + snippet: '\\bibliographystyle{$1}', + meta: 'chapterbib-cmd', + score: 0.25122317941387773, + }, + { + caption: '\\bibliography{}', + snippet: '\\bibliography{$1}', + meta: 'chapterbib-cmd', + score: 0.2659628337907604, + }, + { + caption: '\\include{}', + snippet: '\\include{$1}', + meta: 'chapterbib-cmd', + score: 0.1547080054979312, + }, + ], + scalerel: [ + { + caption: '\\scaleto{}{}', + snippet: '\\scaleto{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.00027615383978106523, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'scalerel-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'scalerel-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'scalerel-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'scalerel-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'scalerel-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'scalerel-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'scalerel-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'scalerel-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'scalerel-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'scalerel-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'scalerel-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'scalerel-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'scalerel-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'scalerel-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'scalerel-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'scalerel-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'scalerel-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'scalerel-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'scalerel-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'scalerel-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'scalerel-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'scalerel-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'scalerel-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'scalerel-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'scalerel-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'scalerel-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'scalerel-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'scalerel-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'scalerel-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'scalerel-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'scalerel-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'scalerel-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'scalerel-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'scalerel-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'scalerel-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'scalerel-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'scalerel-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'scalerel-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'scalerel-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'scalerel-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'scalerel-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'scalerel-cmd', + score: 0.004719094298848707, + }, + ], + extarrows: [ + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'extarrows-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'extarrows-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'extarrows-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'extarrows-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'extarrows-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'extarrows-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'extarrows-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'extarrows-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'extarrows-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'extarrows-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'extarrows-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'extarrows-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'extarrows-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'extarrows-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'extarrows-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'extarrows-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'extarrows-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'extarrows-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'extarrows-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'extarrows-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'extarrows-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'extarrows-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'extarrows-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'extarrows-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'extarrows-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'extarrows-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'extarrows-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'extarrows-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'extarrows-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'extarrows-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'extarrows-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'extarrows-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'extarrows-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'extarrows-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'extarrows-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'extarrows-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'extarrows-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'extarrows-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'extarrows-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'extarrows-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'extarrows-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'extarrows-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'extarrows-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'extarrows-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'extarrows-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'extarrows-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'extarrows-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'extarrows-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'extarrows-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'extarrows-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'extarrows-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'extarrows-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'extarrows-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'extarrows-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'extarrows-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'extarrows-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'extarrows-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'extarrows-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'extarrows-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'extarrows-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'extarrows-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'extarrows-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'extarrows-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'extarrows-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'extarrows-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'extarrows-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'extarrows-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'extarrows-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'extarrows-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'extarrows-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'extarrows-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'extarrows-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'extarrows-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'extarrows-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'extarrows-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'extarrows-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'extarrows-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'extarrows-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'extarrows-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'extarrows-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'extarrows-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'extarrows-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'extarrows-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'extarrows-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'extarrows-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'extarrows-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'extarrows-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'extarrows-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'extarrows-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'extarrows-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'extarrows-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'extarrows-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'extarrows-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'extarrows-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'extarrows-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'extarrows-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'extarrows-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'extarrows-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'extarrows-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'extarrows-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'extarrows-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'extarrows-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'extarrows-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'extarrows-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'extarrows-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'extarrows-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'extarrows-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'extarrows-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'extarrows-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'extarrows-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'extarrows-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'extarrows-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'extarrows-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'extarrows-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'extarrows-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'extarrows-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'extarrows-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'extarrows-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'extarrows-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'extarrows-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'extarrows-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'extarrows-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'extarrows-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'extarrows-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'extarrows-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'extarrows-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'extarrows-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'extarrows-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'extarrows-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'extarrows-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'extarrows-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'extarrows-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'extarrows-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'extarrows-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'extarrows-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'extarrows-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'extarrows-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'extarrows-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'extarrows-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'extarrows-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'extarrows-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'extarrows-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'extarrows-cmd', + score: 0.0063276692758974925, + }, + ], + listingsutf8: [ + { + caption: '\\vskip', + snippet: '\\vskip', + meta: 'listingsutf8-cmd', + score: 0.05143052892347224, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'listingsutf8-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'listingsutf8-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'listingsutf8-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\thelstlisting', + snippet: '\\thelstlisting', + meta: 'listingsutf8-cmd', + score: 0.00012774128088872144, + }, + { + caption: '\\lstinputlisting[]{}', + snippet: '\\lstinputlisting[$1]{$2}', + meta: 'listingsutf8-cmd', + score: 0.011660477607086044, + }, + { + caption: '\\lstinputlisting{}', + snippet: '\\lstinputlisting{$1}', + meta: 'listingsutf8-cmd', + score: 0.011660477607086044, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'listingsutf8-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'listingsutf8-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\lstinline', + snippet: '\\lstinline', + meta: 'listingsutf8-cmd', + score: 0.005972262850694285, + }, + { + caption: '\\lstinline{}', + snippet: '\\lstinline{$1}', + meta: 'listingsutf8-cmd', + score: 0.005972262850694285, + }, + { + caption: '\\lstlistoflistings', + snippet: '\\lstlistoflistings', + meta: 'listingsutf8-cmd', + score: 0.005279080363360602, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'listingsutf8-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'listingsutf8-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'listingsutf8-cmd', + score: 0.00037306820619479756, + }, + ], + forloop: [ + { + caption: '\\forloop{}{}{}{}', + snippet: '\\forloop{$1}{$2}{$3}{$4}', + meta: 'forloop-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'forloop-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'forloop-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'forloop-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'forloop-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'forloop-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'forloop-cmd', + score: 0.0018957469739775527, + }, + ], + xymtex: [ + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'xymtex-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'xymtex-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\mathcal{}', + snippet: '\\mathcal{$1}', + meta: 'xymtex-cmd', + score: 0.35084018920966636, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'xymtex-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'xymtex-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xymtex-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xymtex-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'xymtex-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'xymtex-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'xymtex-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'xymtex-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'xymtex-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xymtex-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'xymtex-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'xymtex-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xymtex-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'xymtex-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'xymtex-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xymtex-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xymtex-cmd', + score: 0.2864294797053033, + }, + ], + eqlist: [ + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'eqlist-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'eqlist-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'eqlist-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'eqlist-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'eqlist-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'eqlist-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'eqlist-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'eqlist-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'eqlist-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'eqlist-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'eqlist-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\eqparbox{}{}', + snippet: '\\eqparbox{$1}{$2}', + meta: 'eqlist-cmd', + score: 2.9423534119530166e-5, + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'eqlist-cmd', + score: 3.800886892251021, + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'eqlist-cmd', + score: 3.800886892251021, + }, + ], + tgschola: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgschola-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgschola-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgschola-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgschola-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgschola-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgschola-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgschola-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgschola-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tgschola-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tgschola-cmd', + score: 0.021170869458413965, + }, + ], + mfirstuc: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'mfirstuc-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'mfirstuc-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'mfirstuc-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'mfirstuc-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'mfirstuc-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'mfirstuc-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'mfirstuc-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'mfirstuc-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'mfirstuc-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'mfirstuc-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'mfirstuc-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'mfirstuc-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'mfirstuc-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'mfirstuc-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'mfirstuc-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'mfirstuc-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'mfirstuc-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'mfirstuc-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'mfirstuc-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'mfirstuc-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'mfirstuc-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'mfirstuc-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'mfirstuc-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'mfirstuc-cmd', + score: 0.008565354665444157, + }, + ], + gloss: [ + { + caption: '\\makegloss', + snippet: '\\makegloss', + meta: 'gloss-cmd', + score: 0.0018653410309739879, + }, + ], + ltxcmds: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ltxcmds-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'ltxcmds-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'ltxcmds-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'ltxcmds-cmd', + score: 0.021170869458413965, + }, + ], + outlines: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'outlines-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'outlines-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'outlines-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'outlines-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'outlines-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'outlines-cmd', + score: 0.0018957469739775527, + }, + ], + typearea: [ + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'typearea-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'typearea-cmd', + score: 0.1789117552185788, + }, + { + caption: '\\addtokomafont{}{}', + snippet: '\\addtokomafont{$1}{$2}', + meta: 'typearea-cmd', + score: 0.0008555564394100388, + }, + { + caption: '\\setkomafont{}{}', + snippet: '\\setkomafont{$1}{$2}', + meta: 'typearea-cmd', + score: 0.012985816912639263, + }, + { + caption: '\\KOMAoptions{}', + snippet: '\\KOMAoptions{$1}', + meta: 'typearea-cmd', + score: 0.000396664302361659, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'typearea-cmd', + score: 0.00037306820619479756, + }, + ], + currfile: [ + { + caption: '\\currfiledir', + snippet: '\\currfiledir', + meta: 'currfile-cmd', + score: 0.0002459788020229296, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'currfile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'currfile-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'currfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'currfile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'currfile-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'currfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'currfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'currfile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'currfile-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'currfile-cmd', + score: 0.021170869458413965, + }, + ], + toptesi: [ + { + caption: '\\tomo', + snippet: '\\tomo', + meta: 'toptesi-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\mainmatter', + snippet: '\\mainmatter', + meta: 'toptesi-cmd', + score: 0.025705092792367497, + }, + { + caption: '\\ringraziamenti', + snippet: '\\ringraziamenti', + meta: 'toptesi-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\sommario', + snippet: '\\sommario', + meta: 'toptesi-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\NoteWhiteLine', + snippet: '\\NoteWhiteLine', + meta: 'toptesi-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\paginavuota', + snippet: '\\paginavuota', + meta: 'toptesi-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\nota{}', + snippet: '\\nota{$1}', + meta: 'toptesi-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\indici', + snippet: '\\indici', + meta: 'toptesi-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'toptesi-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'toptesi-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'toptesi-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'toptesi-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'toptesi-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'toptesi-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'toptesi-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'toptesi-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'toptesi-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'toptesi-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'toptesi-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'toptesi-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'toptesi-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'toptesi-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'toptesi-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'toptesi-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'toptesi-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'toptesi-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'toptesi-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'toptesi-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'toptesi-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'toptesi-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'toptesi-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'toptesi-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'toptesi-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'toptesi-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'toptesi-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'toptesi-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'toptesi-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'toptesi-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'toptesi-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'toptesi-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'toptesi-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'toptesi-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'toptesi-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'toptesi-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'toptesi-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'toptesi-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'toptesi-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'toptesi-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'toptesi-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'toptesi-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\listing{}', + snippet: '\\listing{$1}', + meta: 'toptesi-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\micro', + snippet: '\\micro', + meta: 'toptesi-cmd', + score: 0.011051971930487929, + }, + { + caption: '\\gradi', + snippet: '\\gradi', + meta: 'toptesi-cmd', + score: 0.00023765162173466673, + }, + { + caption: '\\unit[]{}', + snippet: '\\unit[$1]{$2}', + meta: 'toptesi-cmd', + score: 0.028299796173135428, + }, + { + caption: '\\unit{}', + snippet: '\\unit{$1}', + meta: 'toptesi-cmd', + score: 0.028299796173135428, + }, + { + caption: '\\ped{}', + snippet: '\\ped{$1}', + meta: 'toptesi-cmd', + score: 0.0007129548652040002, + }, + { + caption: '\\ohm', + snippet: '\\ohm', + meta: 'toptesi-cmd', + score: 0.0038146685721293138, + }, + { + caption: '\\gei', + snippet: '\\gei', + meta: 'toptesi-cmd', + score: 0.00023765162173466673, + }, + ], + amsrefs: [ + { + caption: '\\ndash', + snippet: '\\ndash', + meta: 'amsrefs-cmd', + score: 0.0003420867634658178, + }, + { + caption: '\\bib{}{}{}', + snippet: '\\bib{$1}{$2}{$3}', + meta: 'amsrefs-cmd', + score: 0.0017473230242849183, + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'amsrefs-cmd', + score: 2.341195220791228, + }, + ], + sistyle: [ + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'sistyle-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'sistyle-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'sistyle-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'sistyle-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'sistyle-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'sistyle-cmd', + score: 0.0063276692758974925, + }, + ], + suffix: [ + { + caption: '\\let', + snippet: '\\let', + meta: 'suffix-cmd', + score: 0.03789745970461662, + }, + ], + sansmath: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'sansmath-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'sansmath-cmd', + score: 0.021170869458413965, + }, + ], + 'tikz-qtree': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikz-qtree-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-qtree-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-qtree-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikz-qtree-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikz-qtree-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikz-qtree-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikz-qtree-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikz-qtree-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-qtree-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikz-qtree-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-qtree-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikz-qtree-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-qtree-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-qtree-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-qtree-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikz-qtree-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-qtree-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-qtree-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-qtree-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-qtree-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tikz-qtree-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-qtree-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-qtree-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tikz-qtree-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tikz-qtree-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tikz-qtree-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tikz-qtree-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tikz-qtree-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-qtree-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tikz-qtree-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tikz-qtree-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-qtree-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tikz-qtree-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tikz-qtree-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikz-qtree-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikz-qtree-cmd', + score: 0.2864294797053033, + }, + ], + floatpag: [ + { + caption: '\\rotfloatpagestyle{}', + snippet: '\\rotfloatpagestyle{$1}', + meta: 'floatpag-cmd', + score: 0.0004535003423927585, + }, + { + caption: '\\floatpagestyle{}', + snippet: '\\floatpagestyle{$1}', + meta: 'floatpag-cmd', + score: 0.0004535003423927585, + }, + ], + colortab: [ + { + caption: '\\shadowbox{}', + snippet: '\\shadowbox{$1}', + meta: 'colortab-cmd', + score: 0.00107667147399019, + }, + { + caption: '\\doublebox', + snippet: '\\doublebox', + meta: 'colortab-cmd', + score: 0.00015142240898356106, + }, + { + caption: '\\VerbatimEnvironment', + snippet: '\\VerbatimEnvironment', + meta: 'colortab-cmd', + score: 4.5350034239275855e-5, + }, + { + caption: '\\thisfancypage{}{}', + snippet: '\\thisfancypage{$1}{$2}', + meta: 'colortab-cmd', + score: 0.00015142240898356106, + }, + { + caption: '\\TheSbox', + snippet: '\\TheSbox', + meta: 'colortab-cmd', + score: 4.5350034239275855e-5, + }, + { + caption: '\\green', + snippet: '\\green', + meta: 'colortab-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'colortab-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'colortab-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'colortab-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'colortab-cmd', + score: 0.0005786730478266738, + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'colortab-cmd', + score: 0.006520475264573554, + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'colortab-cmd', + score: 0.006520475264573554, + }, + ], + parcolumns: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'parcolumns-cmd', + score: 0.00037306820619479756, + }, + ], + dingbat: [ + { + caption: '\\checkmark', + snippet: '\\checkmark', + meta: 'dingbat-cmd', + score: 0.025060530944368123, + }, + ], + ifoddpage: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'ifoddpage-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\checkoddpage', + snippet: '\\checkoddpage', + meta: 'ifoddpage-cmd', + score: 0.00028672585452906425, + }, + ], + kvoptions: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'kvoptions-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'kvoptions-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'kvoptions-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'kvoptions-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'kvoptions-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'kvoptions-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'kvoptions-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'kvoptions-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'kvoptions-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'kvoptions-cmd', + score: 0.021170869458413965, + }, + ], + 'pst-tree': [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pst-tree-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pst-tree-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pst-tree-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pst-tree-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pst-tree-cmd', + score: 0.0005786730478266738, + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pst-tree-cmd', + score: 0.006520475264573554, + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pst-tree-cmd', + score: 0.006520475264573554, + }, + ], + nonfloat: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'nonfloat-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'nonfloat-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'nonfloat-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'nonfloat-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'nonfloat-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'nonfloat-cmd', + score: 0.0018957469739775527, + }, + ], + rsphrase: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'rsphrase-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'rsphrase-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'rsphrase-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'rsphrase-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'rsphrase-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'rsphrase-cmd', + score: 0.0018957469739775527, + }, + ], + beramono: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'beramono-cmd', + score: 0.00037306820619479756, + }, + ], + pgfbaseimage: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfbaseimage-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfbaseimage-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfbaseimage-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfbaseimage-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfbaseimage-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfbaseimage-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfbaseimage-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfbaseimage-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfbaseimage-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfbaseimage-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfbaseimage-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfbaseimage-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfbaseimage-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfbaseimage-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfbaseimage-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfbaseimage-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfbaseimage-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfbaseimage-cmd', + score: 0.2864294797053033, + }, + ], + romannum: [ + { + caption: '\\thefootnote', + snippet: '\\thefootnote', + meta: 'romannum-cmd', + score: 0.007676927812687567, + }, + { + caption: '\\thefootnote{}', + snippet: '\\thefootnote{$1}', + meta: 'romannum-cmd', + score: 0.007676927812687567, + }, + ], + tgtermes: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgtermes-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgtermes-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgtermes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgtermes-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgtermes-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgtermes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgtermes-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgtermes-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tgtermes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tgtermes-cmd', + score: 0.021170869458413965, + }, + ], + Alegreya: [ + { + caption: '\\rmfamily', + snippet: '\\rmfamily', + meta: 'Alegreya-cmd', + score: 0.00898937903263608, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'Alegreya-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'Alegreya-cmd', + score: 0.008565354665444157, + }, + ], + 'glossaries-extra': [ + { + caption: '\\gls{}', + snippet: '\\gls{$1}', + meta: 'glossaries-extra-cmd', + score: 0.06939353309055077, + }, + { + caption: '\\Gls{}', + snippet: '\\Gls{$1}', + meta: 'glossaries-extra-cmd', + score: 0.003696678698317109, + }, + { + caption: '\\makeglossaries', + snippet: '\\makeglossaries', + meta: 'glossaries-extra-cmd', + score: 0.0056737600836936995, + }, + { + caption: '\\newabbreviation{}{}{}', + snippet: '\\newabbreviation{$1}{$2}{$3}', + meta: 'glossaries-extra-cmd', + score: 0.00023275591440052114, + }, + { + caption: '\\newglossaryentry{}{}', + snippet: '\\newglossaryentry{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.018524394136900962, + }, + { + caption: '\\newglossary{}{}', + snippet: '\\newglossary{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 1.4547244650032571e-5, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'glossaries-extra-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'glossaries-extra-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'glossaries-extra-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'glossaries-extra-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\glslongpluralkey', + snippet: '\\glslongpluralkey', + meta: 'glossaries-extra-cmd', + score: 1.4538687447297259e-5, + }, + { + caption: '\\Glspl{}', + snippet: '\\Glspl{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0025291265119320736, + }, + { + caption: '\\glossarysection', + snippet: '\\glossarysection', + meta: 'glossaries-extra-cmd', + score: 9.579755294730752e-5, + }, + { + caption: '\\printglossaries', + snippet: '\\printglossaries', + meta: 'glossaries-extra-cmd', + score: 0.0010106582768889887, + }, + { + caption: '\\Gls{}', + snippet: '\\Gls{$1}', + meta: 'glossaries-extra-cmd', + score: 0.003696678698317109, + }, + { + caption: '\\setglossarystyle{}', + snippet: '\\setglossarystyle{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0003758893277679221, + }, + { + caption: '\\printglossary', + snippet: '\\printglossary', + meta: 'glossaries-extra-cmd', + score: 0.009139682306158714, + }, + { + caption: '\\printglossary[]', + snippet: '\\printglossary[$1]', + meta: 'glossaries-extra-cmd', + score: 0.009139682306158714, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'glossaries-extra-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\setglossarysection{}', + snippet: '\\setglossarysection{$1}', + meta: 'glossaries-extra-cmd', + score: 3.6081414102781514e-5, + }, + { + caption: '\\glsresetall', + snippet: '\\glsresetall', + meta: 'glossaries-extra-cmd', + score: 0.0006123462672467326, + }, + { + caption: '\\the', + snippet: '\\the', + meta: 'glossaries-extra-cmd', + score: 0.007238960303946444, + }, + { + caption: '\\acrshort{}', + snippet: '\\acrshort{$1}', + meta: 'glossaries-extra-cmd', + score: 0.009936841864059727, + }, + { + caption: '\\printnoidxglossary[]', + snippet: '\\printnoidxglossary[$1]', + meta: 'glossaries-extra-cmd', + score: 0.00021912375285685037, + }, + { + caption: '\\newglossary{}{}', + snippet: '\\newglossary{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 1.4547244650032571e-5, + }, + { + caption: '\\gls{}', + snippet: '\\gls{$1}', + meta: 'glossaries-extra-cmd', + score: 0.06939353309055077, + }, + { + caption: '\\printnoidxglossaries', + snippet: '\\printnoidxglossaries', + meta: 'glossaries-extra-cmd', + score: 5.6789564226023136e-5, + }, + { + caption: '\\printindex', + snippet: '\\printindex', + meta: 'glossaries-extra-cmd', + score: 0.004417016910870522, + }, + { + caption: '\\defglsentryfmt[]{}', + snippet: '\\defglsentryfmt[$1]{$2}', + meta: 'glossaries-extra-cmd', + score: 4.8990621725283124e-5, + }, + { + caption: '\\glspostdescription', + snippet: '\\glspostdescription', + meta: 'glossaries-extra-cmd', + score: 0.0006337376579591112, + }, + { + caption: '\\number', + snippet: '\\number', + meta: 'glossaries-extra-cmd', + score: 0.000968714260809983, + }, + { + caption: '\\glsaddall', + snippet: '\\glsaddall', + meta: 'glossaries-extra-cmd', + score: 0.0008363820557740373, + }, + { + caption: '\\glsaddall[]', + snippet: '\\glsaddall[$1]', + meta: 'glossaries-extra-cmd', + score: 0.0008363820557740373, + }, + { + caption: '\\makeglossaries', + snippet: '\\makeglossaries', + meta: 'glossaries-extra-cmd', + score: 0.0056737600836936995, + }, + { + caption: '\\glossaryname', + snippet: '\\glossaryname', + meta: 'glossaries-extra-cmd', + score: 0.0006174536302752427, + }, + { + caption: '\\newglossaryentry{}{}', + snippet: '\\newglossaryentry{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.018524394136900962, + }, + { + caption: '\\glslabel', + snippet: '\\glslabel', + meta: 'glossaries-extra-cmd', + score: 4.8990621725283124e-5, + }, + { + caption: '\\glsadd{}', + snippet: '\\glsadd{$1}', + meta: 'glossaries-extra-cmd', + score: 3.0150373480213892e-5, + }, + { + caption: '\\makenoidxglossaries', + snippet: '\\makenoidxglossaries', + meta: 'glossaries-extra-cmd', + score: 0.0001382210125680805, + }, + { + caption: '\\glsgenentryfmt', + snippet: '\\glsgenentryfmt', + meta: 'glossaries-extra-cmd', + score: 4.8990621725283124e-5, + }, + { + caption: '\\acronymtype', + snippet: '\\acronymtype', + meta: 'glossaries-extra-cmd', + score: 0.002000834271117562, + }, + { + caption: '\\acrfull{}', + snippet: '\\acrfull{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0032622587277765067, + }, + { + caption: '\\newacronym{}{}{}', + snippet: '\\newacronym{$1}{$2}{$3}', + meta: 'glossaries-extra-cmd', + score: 0.03193935544723102, + }, + { + caption: '\\glspl{}', + snippet: '\\glspl{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0034025897522047717, + }, + { + caption: '\\ifglsused{}{}{}', + snippet: '\\ifglsused{$1}{$2}{$3}', + meta: 'glossaries-extra-cmd', + score: 4.8990621725283124e-5, + }, + { + caption: '\\acrlong{}', + snippet: '\\acrlong{$1}', + meta: 'glossaries-extra-cmd', + score: 0.002517821598213752, + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'glossaries-extra-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'glossaries-extra-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'glossaries-extra-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'glossaries-extra-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'glossaries-extra-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'glossaries-extra-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'glossaries-extra-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'glossaries-extra-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'glossaries-extra-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'glossaries-extra-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'glossaries-extra-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'glossaries-extra-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'glossaries-extra-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'glossaries-extra-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'glossaries-extra-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'glossaries-extra-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'glossaries-extra-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'glossaries-extra-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'glossaries-extra-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'glossaries-extra-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'glossaries-extra-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'glossaries-extra-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'glossaries-extra-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'glossaries-extra-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'glossaries-extra-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'glossaries-extra-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'glossaries-extra-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'glossaries-extra-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'glossaries-extra-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'glossaries-extra-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'glossaries-extra-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'glossaries-extra-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'glossaries-extra-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'glossaries-extra-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'glossaries-extra-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'glossaries-extra-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'glossaries-extra-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'glossaries-extra-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'glossaries-extra-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'glossaries-extra-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'glossaries-extra-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'glossaries-extra-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'glossaries-extra-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'glossaries-extra-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'glossaries-extra-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'glossaries-extra-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'glossaries-extra-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'glossaries-extra-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'glossaries-extra-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'glossaries-extra-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'glossaries-extra-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'glossaries-extra-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'glossaries-extra-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'glossaries-extra-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'glossaries-extra-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'glossaries-extra-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'glossaries-extra-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'glossaries-extra-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'glossaries-extra-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'glossaries-extra-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'glossaries-extra-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'glossaries-extra-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'glossaries-extra-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'glossaries-extra-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'glossaries-extra-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'glossaries-extra-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'glossaries-extra-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'glossaries-extra-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'glossaries-extra-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'glossaries-extra-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'glossaries-extra-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'glossaries-extra-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'glossaries-extra-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'glossaries-extra-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'glossaries-extra-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'glossaries-extra-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'glossaries-extra-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'glossaries-extra-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'glossaries-extra-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'glossaries-extra-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'glossaries-extra-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'glossaries-extra-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'glossaries-extra-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'glossaries-extra-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'glossaries-extra-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'glossaries-extra-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'glossaries-extra-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'glossaries-extra-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'glossaries-extra-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'glossaries-extra-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'glossaries-extra-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'glossaries-extra-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'glossaries-extra-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'glossaries-extra-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'glossaries-extra-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'glossaries-extra-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'glossaries-extra-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'glossaries-extra-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'glossaries-extra-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'glossaries-extra-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'glossaries-extra-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'glossaries-extra-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'glossaries-extra-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'glossaries-extra-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'glossaries-extra-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'glossaries-extra-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'glossaries-extra-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'glossaries-extra-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'glossaries-extra-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'glossaries-extra-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'glossaries-extra-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'glossaries-extra-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'glossaries-extra-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'glossaries-extra-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'glossaries-extra-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'glossaries-extra-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'glossaries-extra-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'glossaries-extra-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'glossaries-extra-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'glossaries-extra-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'glossaries-extra-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'glossaries-extra-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'glossaries-extra-cmd', + score: 2.341195220791228, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'glossaries-extra-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'glossaries-extra-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'glossaries-extra-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'glossaries-extra-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'glossaries-extra-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'glossaries-extra-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'glossaries-extra-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'glossaries-extra-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'glossaries-extra-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'glossaries-extra-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'glossaries-extra-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'glossaries-extra-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'glossaries-extra-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'glossaries-extra-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'glossaries-extra-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'glossaries-extra-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'glossaries-extra-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'glossaries-extra-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'glossaries-extra-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'glossaries-extra-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'glossaries-extra-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'glossaries-extra-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'glossaries-extra-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'glossaries-extra-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'glossaries-extra-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'glossaries-extra-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'glossaries-extra-cmd', + score: 0.0063276692758974925, + }, + ], + dashrule: [ + { + caption: '\\hdashrule[]{}{}{}', + snippet: '\\hdashrule[$1]{$2}{$3}{$4}', + meta: 'dashrule-cmd', + score: 0.00029867998381154486, + }, + ], + bclogo: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bclogo-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'bclogo-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'bclogo-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'bclogo-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'bclogo-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'bclogo-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'bclogo-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'bclogo-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'bclogo-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'bclogo-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'bclogo-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'bclogo-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'bclogo-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'bclogo-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'bclogo-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'bclogo-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'bclogo-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'bclogo-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bclogo-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'bclogo-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'bclogo-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'bclogo-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'bclogo-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bclogo-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bclogo-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bclogo-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'bclogo-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'bclogo-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'bclogo-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'bclogo-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'bclogo-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bclogo-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'bclogo-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bclogo-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'bclogo-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'bclogo-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'bclogo-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'bclogo-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'bclogo-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'bclogo-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'bclogo-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'bclogo-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'bclogo-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'bclogo-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'bclogo-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'bclogo-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'bclogo-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'bclogo-cmd', + score: 0.004719094298848707, + }, + ], + isomath: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'isomath-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'isomath-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'isomath-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'isomath-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'isomath-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'isomath-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'isomath-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'isomath-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'isomath-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'isomath-cmd', + score: 0.021170869458413965, + }, + ], + 'tkz-graph': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-graph-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-graph-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tkz-graph-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tkz-graph-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tkz-graph-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tkz-graph-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tkz-graph-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tkz-graph-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-graph-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tkz-graph-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tkz-graph-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tkz-graph-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tkz-graph-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'tkz-graph-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'tkz-graph-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'tkz-graph-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'tkz-graph-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'tkz-graph-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tkz-graph-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tkz-graph-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\reserveinserts{}', + snippet: '\\reserveinserts{$1}', + meta: 'tkz-graph-cmd', + score: 0.0018653410309739879, + }, + { + caption: '\\newtoks', + snippet: '\\newtoks', + meta: 'tkz-graph-cmd', + score: 0.00031058155311734754, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-graph-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tkz-graph-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-graph-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-graph-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tkz-graph-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tkz-graph-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tkz-graph-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tkz-graph-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tkz-graph-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-graph-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tkz-graph-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tkz-graph-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tkz-graph-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tkz-graph-cmd', + score: 0.2864294797053033, + }, + ], + sourcesanspro: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'sourcesanspro-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'sourcesanspro-cmd', + score: 0.008565354665444157, + }, + ], + longdivision: [ + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'longdivision-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'longdivision-cmd', + score: 0.2864294797053033, + }, + ], + xmpmulti: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'xmpmulti-cmd', + score: 0.00037306820619479756, + }, + ], + epsdice: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epsdice-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'epsdice-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'epsdice-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'epsdice-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'epsdice-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'epsdice-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'epsdice-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'epsdice-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'epsdice-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'epsdice-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'epsdice-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'epsdice-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'epsdice-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'epsdice-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'epsdice-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'epsdice-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'epsdice-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'epsdice-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'epsdice-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'epsdice-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'epsdice-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'epsdice-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'epsdice-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'epsdice-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'epsdice-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'epsdice-cmd', + score: 0.004719094298848707, + }, + ], + apptools: [ + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'apptools-cmd', + score: 0.047007158741781095, + }, + { + caption: '\\AtAppendix{}', + snippet: '\\AtAppendix{$1}', + meta: 'apptools-cmd', + score: 8.82390883984482e-6, + }, + ], + letltxmacro: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'letltxmacro-cmd', + score: 0.008565354665444157, + }, + ], + menukeys: [ + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'menukeys-cmd', + score: 0.354445763583904, + }, + { + caption: '\\adjustbox{}{}', + snippet: '\\adjustbox{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.002008185536556013, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'menukeys-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'menukeys-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'menukeys-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'menukeys-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\usepackage{}', + snippet: '\\usepackage{$1}', + meta: 'menukeys-cmd', + score: 5.427890758130527, + }, + { + caption: '\\usepackage[]{}', + snippet: '\\usepackage[$1]{$2}', + meta: 'menukeys-cmd', + score: 5.427890758130527, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'menukeys-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'menukeys-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'menukeys-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'menukeys-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'menukeys-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'menukeys-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'menukeys-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'menukeys-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'menukeys-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'menukeys-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'menukeys-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\mathlarger{}', + snippet: '\\mathlarger{$1}', + meta: 'menukeys-cmd', + score: 0.0031475241540308316, + }, + { + caption: '\\smaller', + snippet: '\\smaller', + meta: 'menukeys-cmd', + score: 0.001271007880944704, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'menukeys-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'menukeys-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'menukeys-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'menukeys-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'menukeys-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'menukeys-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'menukeys-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'menukeys-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'menukeys-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'menukeys-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'menukeys-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'menukeys-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'menukeys-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'menukeys-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'menukeys-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'menukeys-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'menukeys-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'menukeys-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'menukeys-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'menukeys-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'menukeys-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'menukeys-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'menukeys-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'menukeys-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'menukeys-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'menukeys-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'menukeys-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'menukeys-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'menukeys-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'menukeys-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'menukeys-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'menukeys-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'menukeys-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'menukeys-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'menukeys-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'menukeys-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'menukeys-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'menukeys-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'menukeys-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'menukeys-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'menukeys-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'menukeys-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'menukeys-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'menukeys-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'menukeys-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'menukeys-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'menukeys-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'menukeys-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'menukeys-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'menukeys-cmd', + score: 0.2864294797053033, + }, + ], + hypdvips: [ + { + caption: '\\begin{}', + snippet: '\\begin{$1}', + meta: 'hypdvips-cmd', + score: 7.849662248028187, + }, + { + caption: '\\begin{}[]', + snippet: '\\begin{$1}[$2]', + meta: 'hypdvips-cmd', + score: 7.849662248028187, + }, + { + caption: '\\begin{}{}', + snippet: '\\begin{$1}{$2}', + meta: 'hypdvips-cmd', + score: 7.849662248028187, + }, + { + caption: '\\author{}', + snippet: '\\author{$1}', + meta: 'hypdvips-cmd', + score: 0.8973590434087177, + }, + { + caption: '\\author[]{}', + snippet: '\\author[$1]{$2}', + meta: 'hypdvips-cmd', + score: 0.8973590434087177, + }, + { + caption: '\\title{}', + snippet: '\\title{$1}', + meta: 'hypdvips-cmd', + score: 0.9202908262245683, + }, + { + caption: '\\end{}', + snippet: '\\end{$1}', + meta: 'hypdvips-cmd', + score: 7.847906405228455, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'hypdvips-cmd', + score: 0.1789117552185788, + }, + { + caption: '\\global', + snippet: '\\global', + meta: 'hypdvips-cmd', + score: 0.006609629561859019, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hypdvips-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hypdvips-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'hypdvips-cmd', + score: 0.00047530324346933345, + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'hypdvips-cmd', + score: 0.0005277905480209891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'hypdvips-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'hypdvips-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'hypdvips-cmd', + score: 0.0002854206807593436, + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'hypdvips-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'hypdvips-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'hypdvips-cmd', + score: 0.010515056688180681, + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'hypdvips-cmd', + score: 0.008041789461944983, + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'hypdvips-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'hypdvips-cmd', + score: 0.0032990580087398644, + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'hypdvips-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'hypdvips-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\nameref{}', + snippet: '\\nameref{$1}', + meta: 'hypdvips-cmd', + score: 0.009472569279662113, + }, + { + caption: '\\pdfbookmark[]{}{}', + snippet: '\\pdfbookmark[$1]{$2}{$3}', + meta: 'hypdvips-cmd', + score: 0.006492248863367502, + }, + { + caption: '\\figureautorefname', + snippet: '\\figureautorefname', + meta: 'hypdvips-cmd', + score: 0.00014582556188448738, + }, + { + caption: '\\figureautorefname{}', + snippet: '\\figureautorefname{$1}', + meta: 'hypdvips-cmd', + score: 0.00014582556188448738, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hypdvips-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hypdvips-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\footnoteautorefname', + snippet: '\\footnoteautorefname', + meta: 'hypdvips-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\roman{}', + snippet: '\\roman{$1}', + meta: 'hypdvips-cmd', + score: 0.005553384455935491, + }, + { + caption: '\\roman', + snippet: '\\roman', + meta: 'hypdvips-cmd', + score: 0.005553384455935491, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'hypdvips-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\MakeLowercase{}', + snippet: '\\MakeLowercase{$1}', + meta: 'hypdvips-cmd', + score: 0.017289599800633146, + }, + { + caption: '\\textunderscore', + snippet: '\\textunderscore', + meta: 'hypdvips-cmd', + score: 0.001509072212764015, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'hypdvips-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\begin{}', + snippet: '\\begin{$1}', + meta: 'hypdvips-cmd', + score: 7.849662248028187, + }, + { + caption: '\\begin{}[]', + snippet: '\\begin{$1}[$2]', + meta: 'hypdvips-cmd', + score: 7.849662248028187, + }, + { + caption: '\\begin{}{}', + snippet: '\\begin{$1}{$2}', + meta: 'hypdvips-cmd', + score: 7.849662248028187, + }, + { + caption: '\\FancyVerbLineautorefname', + snippet: '\\FancyVerbLineautorefname', + meta: 'hypdvips-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\hyperlink{}{}', + snippet: '\\hyperlink{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.00978652043902115, + }, + { + caption: '\\tableautorefname', + snippet: '\\tableautorefname', + meta: 'hypdvips-cmd', + score: 0.00012704528567339081, + }, + { + caption: '\\tableautorefname{}', + snippet: '\\tableautorefname{$1}', + meta: 'hypdvips-cmd', + score: 0.00012704528567339081, + }, + { + caption: '\\equationautorefname', + snippet: '\\equationautorefname', + meta: 'hypdvips-cmd', + score: 0.00018777198999871106, + }, + { + caption: '\\equationautorefname{}', + snippet: '\\equationautorefname{$1}', + meta: 'hypdvips-cmd', + score: 0.00018777198999871106, + }, + { + caption: '\\chapterautorefname', + snippet: '\\chapterautorefname', + meta: 'hypdvips-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\TeX', + snippet: '\\TeX', + meta: 'hypdvips-cmd', + score: 0.02873756018238537, + }, + { + caption: '\\TeX{}', + snippet: '\\TeX{$1}', + meta: 'hypdvips-cmd', + score: 0.02873756018238537, + }, + { + caption: '\\protect', + snippet: '\\protect', + meta: 'hypdvips-cmd', + score: 0.0200686676229443, + }, + { + caption: '\\appendixautorefname', + snippet: '\\appendixautorefname', + meta: 'hypdvips-cmd', + score: 7.950698053641679e-5, + }, + { + caption: '\\appendixautorefname{}', + snippet: '\\appendixautorefname{$1}', + meta: 'hypdvips-cmd', + score: 7.950698053641679e-5, + }, + { + caption: '\\newlabel{}{}', + snippet: '\\newlabel{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.00029737672328168955, + }, + { + caption: '\\texorpdfstring{}{}', + snippet: '\\texorpdfstring{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.0073781967296121, + }, + { + caption: '\\refstepcounter{}', + snippet: '\\refstepcounter{$1}', + meta: 'hypdvips-cmd', + score: 0.002140559856649122, + }, + { + caption: '\\alph', + snippet: '\\alph', + meta: 'hypdvips-cmd', + score: 0.01034327266194849, + }, + { + caption: '\\alph{}', + snippet: '\\alph{$1}', + meta: 'hypdvips-cmd', + score: 0.01034327266194849, + }, + { + caption: '\\pageref{}', + snippet: '\\pageref{$1}', + meta: 'hypdvips-cmd', + score: 0.019788865471151957, + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'hypdvips-cmd', + score: 3.800886892251021, + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'hypdvips-cmd', + score: 3.800886892251021, + }, + { + caption: '\\LaTeX', + snippet: '\\LaTeX', + meta: 'hypdvips-cmd', + score: 0.2334089308452787, + }, + { + caption: '\\LaTeX{}', + snippet: '\\LaTeX{$1}', + meta: 'hypdvips-cmd', + score: 0.2334089308452787, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\itemautorefname', + snippet: '\\itemautorefname', + meta: 'hypdvips-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'hypdvips-cmd', + score: 1.2569477427490174, + }, + { + caption: '\\sectionautorefname', + snippet: '\\sectionautorefname', + meta: 'hypdvips-cmd', + score: 0.0019832324299155183, + }, + { + caption: '\\sectionautorefname{}', + snippet: '\\sectionautorefname{$1}', + meta: 'hypdvips-cmd', + score: 0.0019832324299155183, + }, + { + caption: '\\LaTeXe', + snippet: '\\LaTeXe', + meta: 'hypdvips-cmd', + score: 0.007928096378157487, + }, + { + caption: '\\LaTeXe{}', + snippet: '\\LaTeXe{$1}', + meta: 'hypdvips-cmd', + score: 0.007928096378157487, + }, + { + caption: '\\footref{}', + snippet: '\\footref{$1}', + meta: 'hypdvips-cmd', + score: 0.0003680857021151614, + }, + { + caption: '\\footref', + snippet: '\\footref', + meta: 'hypdvips-cmd', + score: 0.0003680857021151614, + }, + { + caption: '\\hypertarget{}{}', + snippet: '\\hypertarget{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.009652820108904094, + }, + { + caption: '\\theoremautorefname', + snippet: '\\theoremautorefname', + meta: 'hypdvips-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'hypdvips-cmd', + score: 0.7504160124360846, + }, + { + caption: '\\subparagraphautorefname', + snippet: '\\subparagraphautorefname', + meta: 'hypdvips-cmd', + score: 0.0005446476945175932, + }, + { + caption: '\\url{}', + snippet: '\\url{$1}', + meta: 'hypdvips-cmd', + score: 0.13586474005868793, + }, + { + caption: '\\author{}', + snippet: '\\author{$1}', + meta: 'hypdvips-cmd', + score: 0.8973590434087177, + }, + { + caption: '\\author[]{}', + snippet: '\\author[$1]{$2}', + meta: 'hypdvips-cmd', + score: 0.8973590434087177, + }, + { + caption: '\\href{}{}', + snippet: '\\href{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.27111130260612365, + }, + { + caption: '\\Roman{}', + snippet: '\\Roman{$1}', + meta: 'hypdvips-cmd', + score: 0.0038703587462843594, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hypdvips-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\autoref{}', + snippet: '\\autoref{$1}', + meta: 'hypdvips-cmd', + score: 0.03741172773691362, + }, + { + caption: '\\nolinkurl{}', + snippet: '\\nolinkurl{$1}', + meta: 'hypdvips-cmd', + score: 0.0004995635515943437, + }, + { + caption: '\\end{}', + snippet: '\\end{$1}', + meta: 'hypdvips-cmd', + score: 7.847906405228455, + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'hypdvips-cmd', + score: 0.0174633138331273, + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'hypdvips-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'hypdvips-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\partautorefname', + snippet: '\\partautorefname', + meta: 'hypdvips-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\Itemautorefname{}', + snippet: '\\Itemautorefname{$1}', + meta: 'hypdvips-cmd', + score: 6.006262128895586e-5, + }, + { + caption: '\\halign{}', + snippet: '\\halign{$1}', + meta: 'hypdvips-cmd', + score: 0.00017906650306643613, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\ref{}', + snippet: '\\ref{$1}', + meta: 'hypdvips-cmd', + score: 1.4380093454211778, + }, + { + caption: '\\Alph{}', + snippet: '\\Alph{$1}', + meta: 'hypdvips-cmd', + score: 0.002233258780143355, + }, + { + caption: '\\Alph', + snippet: '\\Alph', + meta: 'hypdvips-cmd', + score: 0.002233258780143355, + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'hypdvips-cmd', + score: 0.047007158741781095, + }, + { + caption: '\\MP', + snippet: '\\MP', + meta: 'hypdvips-cmd', + score: 0.00018344383742255004, + }, + { + caption: '\\MP{}', + snippet: '\\MP{$1}', + meta: 'hypdvips-cmd', + score: 0.00018344383742255004, + }, + { + caption: '\\paragraphautorefname', + snippet: '\\paragraphautorefname', + meta: 'hypdvips-cmd', + score: 0.0005446476945175932, + }, + { + caption: '\\citeN{}', + snippet: '\\citeN{$1}', + meta: 'hypdvips-cmd', + score: 0.0018503938529945614, + }, + { + caption: '\\citeN', + snippet: '\\citeN', + meta: 'hypdvips-cmd', + score: 0.0018503938529945614, + }, + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'hypdvips-cmd', + score: 0.07503475348393239, + }, + { + caption: '\\subsectionautorefname', + snippet: '\\subsectionautorefname', + meta: 'hypdvips-cmd', + score: 0.0012546605780895737, + }, + { + caption: '\\subsectionautorefname{}', + snippet: '\\subsectionautorefname{$1}', + meta: 'hypdvips-cmd', + score: 0.0012546605780895737, + }, + { + caption: '\\hyperref[]{}', + snippet: '\\hyperref[$1]{$2}', + meta: 'hypdvips-cmd', + score: 0.004515152477030062, + }, + { + caption: '\\arabic{}', + snippet: '\\arabic{$1}', + meta: 'hypdvips-cmd', + score: 0.02445837629741638, + }, + { + caption: '\\arabic', + snippet: '\\arabic', + meta: 'hypdvips-cmd', + score: 0.02445837629741638, + }, + { + caption: '\\newline', + snippet: '\\newline', + meta: 'hypdvips-cmd', + score: 0.3311721696201715, + }, + { + caption: '\\hypersetup{}', + snippet: '\\hypersetup{$1}', + meta: 'hypdvips-cmd', + score: 0.06967310843464661, + }, + { + caption: '\\subsubsectionautorefname', + snippet: '\\subsubsectionautorefname', + meta: 'hypdvips-cmd', + score: 0.0012064581899162352, + }, + { + caption: '\\subsubsectionautorefname{}', + snippet: '\\subsubsectionautorefname{$1}', + meta: 'hypdvips-cmd', + score: 0.0012064581899162352, + }, + { + caption: '\\title{}', + snippet: '\\title{$1}', + meta: 'hypdvips-cmd', + score: 0.9202908262245683, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hypdvips-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hypdvips-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hypdvips-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hypdvips-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\pdfbookmark[]{}{}', + snippet: '\\pdfbookmark[$1]{$2}{$3}', + meta: 'hypdvips-cmd', + score: 0.006492248863367502, + }, + { + caption: '\\bookmarkget{}', + snippet: '\\bookmarkget{$1}', + meta: 'hypdvips-cmd', + score: 0.00026847053008917257, + }, + { + caption: '\\bookmarksetup{}', + snippet: '\\bookmarksetup{$1}', + meta: 'hypdvips-cmd', + score: 0.001134118016265821, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hypdvips-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hypdvips-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hypdvips-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'hypdvips-cmd', + score: 0.0058342578961340175, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'hypdvips-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hypdvips-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hypdvips-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'hypdvips-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'hypdvips-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'hypdvips-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hypdvips-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hypdvips-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'hypdvips-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'hypdvips-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'hypdvips-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hypdvips-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'hypdvips-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hypdvips-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'hypdvips-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'hypdvips-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'hypdvips-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'hypdvips-cmd', + score: 0.2864294797053033, + }, + ], + easyReview: [ + { + caption: '\\highlight{}', + snippet: '\\highlight{$1}', + meta: 'easyReview-cmd', + score: 0.00021546602164732416, + }, + { + caption: '\\highlight', + snippet: '\\highlight', + meta: 'easyReview-cmd', + score: 0.00021546602164732416, + }, + { + caption: '\\alert{}', + snippet: '\\alert{$1}', + meta: 'easyReview-cmd', + score: 0.02756568949970745, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'easyReview-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'easyReview-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'easyReview-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'easyReview-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'easyReview-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'easyReview-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'easyReview-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'easyReview-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'easyReview-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'easyReview-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'easyReview-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'easyReview-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'easyReview-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\missingfigure[]{}', + snippet: '\\missingfigure[$1]{$2}', + meta: 'easyReview-cmd', + score: 0.001558719179721163, + }, + { + caption: '\\missingfigure', + snippet: '\\missingfigure', + meta: 'easyReview-cmd', + score: 0.001558719179721163, + }, + { + caption: '\\todototoc', + snippet: '\\todototoc', + meta: 'easyReview-cmd', + score: 0.000325977535138643, + }, + { + caption: '\\todo{}', + snippet: '\\todo{$1}', + meta: 'easyReview-cmd', + score: 0.04115074278362878, + }, + { + caption: '\\todo[]{}', + snippet: '\\todo[$1]{$2}', + meta: 'easyReview-cmd', + score: 0.04115074278362878, + }, + { + caption: '\\todo', + snippet: '\\todo', + meta: 'easyReview-cmd', + score: 0.04115074278362878, + }, + { + caption: '\\listoftodos', + snippet: '\\listoftodos', + meta: 'easyReview-cmd', + score: 0.0005325975940754609, + }, + { + caption: '\\listoftodos[]', + snippet: '\\listoftodos[$1]', + meta: 'easyReview-cmd', + score: 0.0005325975940754609, + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'easyReview-cmd', + score: 0.0174633138331273, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'easyReview-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'easyReview-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'easyReview-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'easyReview-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'easyReview-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'easyReview-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'easyReview-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'easyReview-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'easyReview-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'easyReview-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'easyReview-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'easyReview-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareRobustCommand{}{}', + snippet: '\\DeclareRobustCommand{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.0010373158471650705, + }, + { + caption: '\\DeclareRobustCommand{}[]{}', + snippet: '\\DeclareRobustCommand{$1}[$2]{$3}', + meta: 'easyReview-cmd', + score: 0.0010373158471650705, + }, + { + caption: '\\sethlcolor{}', + snippet: '\\sethlcolor{$1}', + meta: 'easyReview-cmd', + score: 0.01970230898277056, + }, + { + caption: '\\st', + snippet: '\\st', + meta: 'easyReview-cmd', + score: 0.004652662833362787, + }, + { + caption: '\\st{}', + snippet: '\\st{$1}', + meta: 'easyReview-cmd', + score: 0.004652662833362787, + }, + { + caption: '\\def', + snippet: '\\def', + meta: 'easyReview-cmd', + score: 0.21357759092476175, + }, + { + caption: '\\hl{}', + snippet: '\\hl{$1}', + meta: 'easyReview-cmd', + score: 0.03421486301062431, + }, + { + caption: '\\sodef', + snippet: '\\sodef', + meta: 'easyReview-cmd', + score: 0.0017045357696831268, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'easyReview-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\so', + snippet: '\\so', + meta: 'easyReview-cmd', + score: 0.004308800134587786, + }, + { + caption: '\\so{}', + snippet: '\\so{$1}', + meta: 'easyReview-cmd', + score: 0.004308800134587786, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'easyReview-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'easyReview-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'easyReview-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'easyReview-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'easyReview-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'easyReview-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'easyReview-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'easyReview-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'easyReview-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'easyReview-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'easyReview-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'easyReview-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'easyReview-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'easyReview-cmd', + score: 0.2864294797053033, + }, + ], + quoting: [ + { + caption: '\\par', + snippet: '\\par', + meta: 'quoting-cmd', + score: 0.413853376001159, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'quoting-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'quoting-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'quoting-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'quoting-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'quoting-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'quoting-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'quoting-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'quoting-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'quoting-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'quoting-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'quoting-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'quoting-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'quoting-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'quoting-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'quoting-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'quoting-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'quoting-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'quoting-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'quoting-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'quoting-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'quoting-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'quoting-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'quoting-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'quoting-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'quoting-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'quoting-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'quoting-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'quoting-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'quoting-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'quoting-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'quoting-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'quoting-cmd', + score: 0.008565354665444157, + }, + ], + fouriernc: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'fouriernc-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'fouriernc-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'fouriernc-cmd', + score: 0.021170869458413965, + }, + ], + realboxes: [ + { + caption: '\\Rotatebox{}{}', + snippet: '\\Rotatebox{$1}{$2}', + meta: 'realboxes-cmd', + score: 1.8920528094586312e-5, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'realboxes-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'realboxes-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'realboxes-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'realboxes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'realboxes-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'realboxes-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'realboxes-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'realboxes-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'realboxes-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\shadowbox{}', + snippet: '\\shadowbox{$1}', + meta: 'realboxes-cmd', + score: 0.00107667147399019, + }, + { + caption: '\\doublebox', + snippet: '\\doublebox', + meta: 'realboxes-cmd', + score: 0.00015142240898356106, + }, + { + caption: '\\VerbatimEnvironment', + snippet: '\\VerbatimEnvironment', + meta: 'realboxes-cmd', + score: 4.5350034239275855e-5, + }, + { + caption: '\\thisfancypage{}{}', + snippet: '\\thisfancypage{$1}{$2}', + meta: 'realboxes-cmd', + score: 0.00015142240898356106, + }, + { + caption: '\\TheSbox', + snippet: '\\TheSbox', + meta: 'realboxes-cmd', + score: 4.5350034239275855e-5, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'realboxes-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'realboxes-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'realboxes-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'realboxes-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'realboxes-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'realboxes-cmd', + score: 0.0018957469739775527, + }, + ], + etextools: [ + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'etextools-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'etextools-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'etextools-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'etextools-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'etextools-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'etextools-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'etextools-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'etextools-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'etextools-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'etextools-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'etextools-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'etextools-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'etextools-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'etextools-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'etextools-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'etextools-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'etextools-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'etextools-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'etextools-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'etextools-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'etextools-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'etextools-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'etextools-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\reserveinserts{}', + snippet: '\\reserveinserts{$1}', + meta: 'etextools-cmd', + score: 0.0018653410309739879, + }, + { + caption: '\\newtoks', + snippet: '\\newtoks', + meta: 'etextools-cmd', + score: 0.00031058155311734754, + }, + ], + ccaption: [ + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'ccaption-cmd', + score: 1.2569477427490174, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'ccaption-cmd', + score: 1.897791904799601, + }, + ], + exercise: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'exercise-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'exercise-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'exercise-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'exercise-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'exercise-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'exercise-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'exercise-cmd', + score: 0.00037306820619479756, + }, + ], + slantsc: [ + { + caption: '\\scshape', + snippet: '\\scshape', + meta: 'slantsc-cmd', + score: 0.05364108855914402, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'slantsc-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'slantsc-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'slantsc-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'slantsc-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'slantsc-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'slantsc-cmd', + score: 0.0018957469739775527, + }, + ], + 'glossary-longbooktabs': [ + { + caption: '\\specialrule{}{}{}', + snippet: '\\specialrule{$1}{$2}{$3}', + meta: 'glossary-longbooktabs-cmd', + score: 0.004974385202605165, + }, + { + caption: '\\cmidrule', + snippet: '\\cmidrule', + meta: 'glossary-longbooktabs-cmd', + score: 0.01894952272365088, + }, + { + caption: '\\cmidrule{}', + snippet: '\\cmidrule{$1}', + meta: 'glossary-longbooktabs-cmd', + score: 0.01894952272365088, + }, + { + caption: '\\bottomrule', + snippet: '\\bottomrule', + meta: 'glossary-longbooktabs-cmd', + score: 0.04533364657852219, + }, + { + caption: '\\midrule', + snippet: '\\midrule', + meta: 'glossary-longbooktabs-cmd', + score: 0.07098077735912875, + }, + { + caption: '\\addlinespace', + snippet: '\\addlinespace', + meta: 'glossary-longbooktabs-cmd', + score: 0.005865460617491447, + }, + { + caption: '\\addlinespace[]', + snippet: '\\addlinespace[$1]', + meta: 'glossary-longbooktabs-cmd', + score: 0.005865460617491447, + }, + { + caption: '\\toprule', + snippet: '\\toprule', + meta: 'glossary-longbooktabs-cmd', + score: 0.059857788139528495, + }, + { + caption: '\\endhead', + snippet: '\\endhead', + meta: 'glossary-longbooktabs-cmd', + score: 0.0023853501147448834, + }, + { + caption: '\\endfoot', + snippet: '\\endfoot', + meta: 'glossary-longbooktabs-cmd', + score: 0.00044045261916551967, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'glossary-longbooktabs-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'glossary-longbooktabs-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\nopagebreak', + snippet: '\\nopagebreak', + meta: 'glossary-longbooktabs-cmd', + score: 9.952664522415981e-5, + }, + { + caption: '\\endfirsthead', + snippet: '\\endfirsthead', + meta: 'glossary-longbooktabs-cmd', + score: 0.0016148498709822416, + }, + { + caption: '\\endlastfoot', + snippet: '\\endlastfoot', + meta: 'glossary-longbooktabs-cmd', + score: 0.00044045261916551967, + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'glossary-longbooktabs-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\tablename', + snippet: '\\tablename', + meta: 'glossary-longbooktabs-cmd', + score: 0.0029238994233674776, + }, + { + caption: '\\pagebreak', + snippet: '\\pagebreak', + meta: 'glossary-longbooktabs-cmd', + score: 0.0313525090421608, + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'glossary-longbooktabs-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'glossary-longbooktabs-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'glossary-longbooktabs-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'glossary-longbooktabs-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'glossary-longbooktabs-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'glossary-longbooktabs-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'glossary-longbooktabs-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'glossary-longbooktabs-cmd', + score: 0.018615449342361392, + }, + ], + pgflibraryarrows: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgflibraryarrows-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgflibraryarrows-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgflibraryarrows-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgflibraryarrows-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgflibraryarrows-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgflibraryarrows-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgflibraryarrows-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgflibraryarrows-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgflibraryarrows-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgflibraryarrows-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgflibraryarrows-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgflibraryarrows-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgflibraryarrows-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgflibraryarrows-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgflibraryarrows-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgflibraryarrows-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgflibraryarrows-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgflibraryarrows-cmd', + score: 0.2864294797053033, + }, + ], + soulpos: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'soulpos-cmd', + score: 0.00037306820619479756, + }, + ], + gmp: [ + { + caption: '\\par', + snippet: '\\par', + meta: 'gmp-cmd', + score: 0.413853376001159, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'gmp-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'gmp-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'gmp-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'gmp-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'gmp-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'gmp-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'gmp-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'gmp-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'gmp-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'gmp-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'gmp-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'gmp-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'gmp-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'gmp-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'gmp-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'gmp-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'gmp-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'gmp-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'gmp-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'gmp-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'gmp-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'gmp-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'gmp-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'gmp-cmd', + score: 0.021170869458413965, + }, + ], + csvsimple: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'csvsimple-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'csvsimple-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'csvsimple-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'csvsimple-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'csvsimple-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'csvsimple-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'csvsimple-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'csvsimple-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'csvsimple-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'csvsimple-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'csvsimple-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'csvsimple-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'csvsimple-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'csvsimple-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'csvsimple-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'csvsimple-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'csvsimple-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'csvsimple-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'csvsimple-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'csvsimple-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'csvsimple-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'csvsimple-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'csvsimple-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'csvsimple-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'csvsimple-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'csvsimple-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'csvsimple-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'csvsimple-cmd', + score: 0.008565354665444157, + }, + ], + ebgaramond: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'ebgaramond-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ebgaramond-cmd', + score: 0.008565354665444157, + }, + ], + boldline: [ + { + caption: '\\hlineB{}', + snippet: '\\hlineB{$1}', + meta: 'boldline-cmd', + score: 0.0009735563258863602, + }, + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'boldline-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'boldline-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'boldline-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'boldline-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'boldline-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'boldline-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'boldline-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'boldline-cmd', + score: 0.018615449342361392, + }, + ], + fontaxes: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'fontaxes-cmd', + score: 0.008565354665444157, + }, + ], + pbsi: [ + { + caption: '\\bsifamily', + snippet: '\\bsifamily', + meta: 'pbsi-cmd', + score: 3.140504277052775e-5, + }, + ], + 'tikz-qtree-compat': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-qtree-compat-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tikz-qtree-compat-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tikz-qtree-compat-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-qtree-compat-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-qtree-compat-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-qtree-compat-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tikz-qtree-compat-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tikz-qtree-compat-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-qtree-compat-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tikz-qtree-compat-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tikz-qtree-compat-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tikz-qtree-compat-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tikz-qtree-compat-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tikz-qtree-compat-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tikz-qtree-compat-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tikz-qtree-compat-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tikz-qtree-compat-cmd', + score: 0.2864294797053033, + }, + ], + 'ebgaramond-maths': [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'ebgaramond-maths-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ebgaramond-maths-cmd', + score: 0.008565354665444157, + }, + ], + complexity: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'complexity-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'complexity-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'complexity-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'complexity-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'complexity-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'complexity-cmd', + score: 0.0018957469739775527, + }, + ], + everysel: [ + { + caption: '\\selectfont', + snippet: '\\selectfont', + meta: 'everysel-cmd', + score: 0.04598628699063736, + }, + ], + txfontsb: [ + { + caption: '\\sqrt{}', + snippet: '\\sqrt{$1}', + meta: 'txfontsb-cmd', + score: 0.20240160977404634, + }, + ], + nath: [ + { + caption: '\\vert', + snippet: '\\vert', + meta: 'nath-cmd', + score: 0.05152912629788525, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'nath-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\quad', + snippet: '\\quad', + meta: 'nath-cmd', + score: 0.15242755832392743, + }, + { + caption: '\\underbrace{}', + snippet: '\\underbrace{$1}', + meta: 'nath-cmd', + score: 0.010373780436850907, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'nath-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\delimgrowth', + snippet: '\\delimgrowth', + meta: 'nath-cmd', + score: 1.8073688234300064e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'nath-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'nath-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\underline{}', + snippet: '\\underline{$1}', + meta: 'nath-cmd', + score: 0.14748550887002482, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'nath-cmd', + score: 1.897791904799601, + }, + { + caption: '\\qquad', + snippet: '\\qquad', + meta: 'nath-cmd', + score: 0.0878145577017131, + }, + ], + vietnam: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'vietnam-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'vietnam-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'vietnam-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'vietnam-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'vietnam-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'vietnam-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'vietnam-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'vietnam-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'vietnam-cmd', + score: 0.021170869458413965, + }, + ], + answers: [ + { + caption: '\\endverbatim', + snippet: '\\endverbatim', + meta: 'answers-cmd', + score: 0.0022216421267780076, + }, + { + caption: '\\verbatim', + snippet: '\\verbatim', + meta: 'answers-cmd', + score: 0.0072203369120285256, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'answers-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'answers-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\par', + snippet: '\\par', + meta: 'answers-cmd', + score: 0.413853376001159, + }, + { + caption: '\\verbatiminput{}', + snippet: '\\verbatiminput{$1}', + meta: 'answers-cmd', + score: 0.0024547099784948665, + }, + { + caption: '\\verbatiminput', + snippet: '\\verbatiminput', + meta: 'answers-cmd', + score: 0.0024547099784948665, + }, + ], + attachfile: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'attachfile-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'attachfile-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'attachfile-cmd', + score: 0.00047530324346933345, + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'attachfile-cmd', + score: 0.0005277905480209891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'attachfile-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'attachfile-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'attachfile-cmd', + score: 0.0002854206807593436, + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'attachfile-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'attachfile-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'attachfile-cmd', + score: 0.010515056688180681, + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'attachfile-cmd', + score: 0.008041789461944983, + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'attachfile-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'attachfile-cmd', + score: 0.0032990580087398644, + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'attachfile-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'attachfile-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\nameref{}', + snippet: '\\nameref{$1}', + meta: 'attachfile-cmd', + score: 0.009472569279662113, + }, + { + caption: '\\pdfbookmark[]{}{}', + snippet: '\\pdfbookmark[$1]{$2}{$3}', + meta: 'attachfile-cmd', + score: 0.006492248863367502, + }, + { + caption: '\\figureautorefname', + snippet: '\\figureautorefname', + meta: 'attachfile-cmd', + score: 0.00014582556188448738, + }, + { + caption: '\\figureautorefname{}', + snippet: '\\figureautorefname{$1}', + meta: 'attachfile-cmd', + score: 0.00014582556188448738, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'attachfile-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'attachfile-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\footnoteautorefname', + snippet: '\\footnoteautorefname', + meta: 'attachfile-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\roman{}', + snippet: '\\roman{$1}', + meta: 'attachfile-cmd', + score: 0.005553384455935491, + }, + { + caption: '\\roman', + snippet: '\\roman', + meta: 'attachfile-cmd', + score: 0.005553384455935491, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'attachfile-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\MakeLowercase{}', + snippet: '\\MakeLowercase{$1}', + meta: 'attachfile-cmd', + score: 0.017289599800633146, + }, + { + caption: '\\textunderscore', + snippet: '\\textunderscore', + meta: 'attachfile-cmd', + score: 0.001509072212764015, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'attachfile-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\begin{}', + snippet: '\\begin{$1}', + meta: 'attachfile-cmd', + score: 7.849662248028187, + }, + { + caption: '\\begin{}[]', + snippet: '\\begin{$1}[$2]', + meta: 'attachfile-cmd', + score: 7.849662248028187, + }, + { + caption: '\\begin{}{}', + snippet: '\\begin{$1}{$2}', + meta: 'attachfile-cmd', + score: 7.849662248028187, + }, + { + caption: '\\FancyVerbLineautorefname', + snippet: '\\FancyVerbLineautorefname', + meta: 'attachfile-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\hyperlink{}{}', + snippet: '\\hyperlink{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.00978652043902115, + }, + { + caption: '\\tableautorefname', + snippet: '\\tableautorefname', + meta: 'attachfile-cmd', + score: 0.00012704528567339081, + }, + { + caption: '\\tableautorefname{}', + snippet: '\\tableautorefname{$1}', + meta: 'attachfile-cmd', + score: 0.00012704528567339081, + }, + { + caption: '\\equationautorefname', + snippet: '\\equationautorefname', + meta: 'attachfile-cmd', + score: 0.00018777198999871106, + }, + { + caption: '\\equationautorefname{}', + snippet: '\\equationautorefname{$1}', + meta: 'attachfile-cmd', + score: 0.00018777198999871106, + }, + { + caption: '\\chapterautorefname', + snippet: '\\chapterautorefname', + meta: 'attachfile-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\TeX', + snippet: '\\TeX', + meta: 'attachfile-cmd', + score: 0.02873756018238537, + }, + { + caption: '\\TeX{}', + snippet: '\\TeX{$1}', + meta: 'attachfile-cmd', + score: 0.02873756018238537, + }, + { + caption: '\\protect', + snippet: '\\protect', + meta: 'attachfile-cmd', + score: 0.0200686676229443, + }, + { + caption: '\\appendixautorefname', + snippet: '\\appendixautorefname', + meta: 'attachfile-cmd', + score: 7.950698053641679e-5, + }, + { + caption: '\\appendixautorefname{}', + snippet: '\\appendixautorefname{$1}', + meta: 'attachfile-cmd', + score: 7.950698053641679e-5, + }, + { + caption: '\\newlabel{}{}', + snippet: '\\newlabel{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.00029737672328168955, + }, + { + caption: '\\texorpdfstring{}{}', + snippet: '\\texorpdfstring{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.0073781967296121, + }, + { + caption: '\\refstepcounter{}', + snippet: '\\refstepcounter{$1}', + meta: 'attachfile-cmd', + score: 0.002140559856649122, + }, + { + caption: '\\alph', + snippet: '\\alph', + meta: 'attachfile-cmd', + score: 0.01034327266194849, + }, + { + caption: '\\alph{}', + snippet: '\\alph{$1}', + meta: 'attachfile-cmd', + score: 0.01034327266194849, + }, + { + caption: '\\pageref{}', + snippet: '\\pageref{$1}', + meta: 'attachfile-cmd', + score: 0.019788865471151957, + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'attachfile-cmd', + score: 3.800886892251021, + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'attachfile-cmd', + score: 3.800886892251021, + }, + { + caption: '\\LaTeX', + snippet: '\\LaTeX', + meta: 'attachfile-cmd', + score: 0.2334089308452787, + }, + { + caption: '\\LaTeX{}', + snippet: '\\LaTeX{$1}', + meta: 'attachfile-cmd', + score: 0.2334089308452787, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\itemautorefname', + snippet: '\\itemautorefname', + meta: 'attachfile-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'attachfile-cmd', + score: 1.2569477427490174, + }, + { + caption: '\\sectionautorefname', + snippet: '\\sectionautorefname', + meta: 'attachfile-cmd', + score: 0.0019832324299155183, + }, + { + caption: '\\sectionautorefname{}', + snippet: '\\sectionautorefname{$1}', + meta: 'attachfile-cmd', + score: 0.0019832324299155183, + }, + { + caption: '\\LaTeXe', + snippet: '\\LaTeXe', + meta: 'attachfile-cmd', + score: 0.007928096378157487, + }, + { + caption: '\\LaTeXe{}', + snippet: '\\LaTeXe{$1}', + meta: 'attachfile-cmd', + score: 0.007928096378157487, + }, + { + caption: '\\footref{}', + snippet: '\\footref{$1}', + meta: 'attachfile-cmd', + score: 0.0003680857021151614, + }, + { + caption: '\\footref', + snippet: '\\footref', + meta: 'attachfile-cmd', + score: 0.0003680857021151614, + }, + { + caption: '\\hypertarget{}{}', + snippet: '\\hypertarget{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.009652820108904094, + }, + { + caption: '\\theoremautorefname', + snippet: '\\theoremautorefname', + meta: 'attachfile-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'attachfile-cmd', + score: 0.7504160124360846, + }, + { + caption: '\\subparagraphautorefname', + snippet: '\\subparagraphautorefname', + meta: 'attachfile-cmd', + score: 0.0005446476945175932, + }, + { + caption: '\\url{}', + snippet: '\\url{$1}', + meta: 'attachfile-cmd', + score: 0.13586474005868793, + }, + { + caption: '\\author{}', + snippet: '\\author{$1}', + meta: 'attachfile-cmd', + score: 0.8973590434087177, + }, + { + caption: '\\author[]{}', + snippet: '\\author[$1]{$2}', + meta: 'attachfile-cmd', + score: 0.8973590434087177, + }, + { + caption: '\\href{}{}', + snippet: '\\href{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.27111130260612365, + }, + { + caption: '\\Roman{}', + snippet: '\\Roman{$1}', + meta: 'attachfile-cmd', + score: 0.0038703587462843594, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'attachfile-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\autoref{}', + snippet: '\\autoref{$1}', + meta: 'attachfile-cmd', + score: 0.03741172773691362, + }, + { + caption: '\\nolinkurl{}', + snippet: '\\nolinkurl{$1}', + meta: 'attachfile-cmd', + score: 0.0004995635515943437, + }, + { + caption: '\\end{}', + snippet: '\\end{$1}', + meta: 'attachfile-cmd', + score: 7.847906405228455, + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'attachfile-cmd', + score: 0.0174633138331273, + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'attachfile-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'attachfile-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\partautorefname', + snippet: '\\partautorefname', + meta: 'attachfile-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\Itemautorefname{}', + snippet: '\\Itemautorefname{$1}', + meta: 'attachfile-cmd', + score: 6.006262128895586e-5, + }, + { + caption: '\\halign{}', + snippet: '\\halign{$1}', + meta: 'attachfile-cmd', + score: 0.00017906650306643613, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\ref{}', + snippet: '\\ref{$1}', + meta: 'attachfile-cmd', + score: 1.4380093454211778, + }, + { + caption: '\\Alph{}', + snippet: '\\Alph{$1}', + meta: 'attachfile-cmd', + score: 0.002233258780143355, + }, + { + caption: '\\Alph', + snippet: '\\Alph', + meta: 'attachfile-cmd', + score: 0.002233258780143355, + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'attachfile-cmd', + score: 0.047007158741781095, + }, + { + caption: '\\MP', + snippet: '\\MP', + meta: 'attachfile-cmd', + score: 0.00018344383742255004, + }, + { + caption: '\\MP{}', + snippet: '\\MP{$1}', + meta: 'attachfile-cmd', + score: 0.00018344383742255004, + }, + { + caption: '\\paragraphautorefname', + snippet: '\\paragraphautorefname', + meta: 'attachfile-cmd', + score: 0.0005446476945175932, + }, + { + caption: '\\citeN{}', + snippet: '\\citeN{$1}', + meta: 'attachfile-cmd', + score: 0.0018503938529945614, + }, + { + caption: '\\citeN', + snippet: '\\citeN', + meta: 'attachfile-cmd', + score: 0.0018503938529945614, + }, + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'attachfile-cmd', + score: 0.07503475348393239, + }, + { + caption: '\\subsectionautorefname', + snippet: '\\subsectionautorefname', + meta: 'attachfile-cmd', + score: 0.0012546605780895737, + }, + { + caption: '\\subsectionautorefname{}', + snippet: '\\subsectionautorefname{$1}', + meta: 'attachfile-cmd', + score: 0.0012546605780895737, + }, + { + caption: '\\hyperref[]{}', + snippet: '\\hyperref[$1]{$2}', + meta: 'attachfile-cmd', + score: 0.004515152477030062, + }, + { + caption: '\\arabic{}', + snippet: '\\arabic{$1}', + meta: 'attachfile-cmd', + score: 0.02445837629741638, + }, + { + caption: '\\arabic', + snippet: '\\arabic', + meta: 'attachfile-cmd', + score: 0.02445837629741638, + }, + { + caption: '\\newline', + snippet: '\\newline', + meta: 'attachfile-cmd', + score: 0.3311721696201715, + }, + { + caption: '\\hypersetup{}', + snippet: '\\hypersetup{$1}', + meta: 'attachfile-cmd', + score: 0.06967310843464661, + }, + { + caption: '\\subsubsectionautorefname', + snippet: '\\subsubsectionautorefname', + meta: 'attachfile-cmd', + score: 0.0012064581899162352, + }, + { + caption: '\\subsubsectionautorefname{}', + snippet: '\\subsubsectionautorefname{$1}', + meta: 'attachfile-cmd', + score: 0.0012064581899162352, + }, + { + caption: '\\title{}', + snippet: '\\title{$1}', + meta: 'attachfile-cmd', + score: 0.9202908262245683, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'attachfile-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'attachfile-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'attachfile-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'attachfile-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'attachfile-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'attachfile-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'attachfile-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'attachfile-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'attachfile-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'attachfile-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'attachfile-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'attachfile-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'attachfile-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'attachfile-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'attachfile-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'attachfile-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'attachfile-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'attachfile-cmd', + score: 0.0058342578961340175, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'attachfile-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'attachfile-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'attachfile-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'attachfile-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'attachfile-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'attachfile-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'attachfile-cmd', + score: 0.00530510025314411, + }, + ], + doc: [ + { + caption: '\\do', + snippet: '\\do', + meta: 'doc-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\verb', + snippet: '\\verb', + meta: 'doc-cmd', + score: 0.1323269725886312, + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'doc-cmd', + score: 0.7504160124360846, + }, + { + caption: '\\verbatim', + snippet: '\\verbatim', + meta: 'doc-cmd', + score: 0.0072203369120285256, + }, + ], + 'tkz-fct': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tkz-fct-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-fct-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-fct-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-fct-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tkz-fct-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tkz-fct-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tkz-fct-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\reserveinserts{}', + snippet: '\\reserveinserts{$1}', + meta: 'tkz-fct-cmd', + score: 0.0018653410309739879, + }, + { + caption: '\\newtoks', + snippet: '\\newtoks', + meta: 'tkz-fct-cmd', + score: 0.00031058155311734754, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-fct-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-fct-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tkz-fct-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tkz-fct-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tkz-fct-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tkz-fct-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tkz-fct-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tkz-fct-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tkz-fct-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-fct-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tkz-fct-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tkz-fct-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tkz-fct-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tkz-fct-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tkz-fct-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tkz-fct-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-fct-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-fct-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tkz-fct-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tkz-fct-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tkz-fct-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tkz-fct-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tkz-fct-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tkz-fct-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tkz-fct-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tkz-fct-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-fct-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tkz-fct-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tkz-fct-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tkz-fct-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tkz-fct-cmd', + score: 0.2864294797053033, + }, + ], + notes2bib: [ + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'notes2bib-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'notes2bib-cmd', + score: 0.2864294797053033, + }, + ], + stackengine: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'stackengine-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'stackengine-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'stackengine-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'stackengine-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'stackengine-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'stackengine-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'stackengine-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'stackengine-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'stackengine-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'stackengine-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'stackengine-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'stackengine-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'stackengine-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'stackengine-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'stackengine-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'stackengine-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'stackengine-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'stackengine-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'stackengine-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'stackengine-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'stackengine-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'stackengine-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'stackengine-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'stackengine-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'stackengine-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'stackengine-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'stackengine-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'stackengine-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'stackengine-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'stackengine-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'stackengine-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'stackengine-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'stackengine-cmd', + score: 0.008565354665444157, + }, + ], + cellspace: [ + { + caption: '\\endtabular', + snippet: '\\endtabular', + meta: 'cellspace-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\multicolumn{}{}{}', + snippet: '\\multicolumn{$1}{$2}{$3}', + meta: 'cellspace-cmd', + score: 0.5473606021405326, + }, + { + caption: '\\array{}', + snippet: '\\array{$1}', + meta: 'cellspace-cmd', + score: 2.650484574842396e-5, + }, + { + caption: '\\arraybackslash', + snippet: '\\arraybackslash', + meta: 'cellspace-cmd', + score: 0.014532521139459619, + }, + { + caption: '\\tabular{}', + snippet: '\\tabular{$1}', + meta: 'cellspace-cmd', + score: 0.0005078239917067089, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'cellspace-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\newcolumntype{}[]{}', + snippet: '\\newcolumntype{$1}[$2]{$3}', + meta: 'cellspace-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\newcolumntype{}{}', + snippet: '\\newcolumntype{$1}{$2}', + meta: 'cellspace-cmd', + score: 0.018615449342361392, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'cellspace-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'cellspace-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'cellspace-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'cellspace-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'cellspace-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'cellspace-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'cellspace-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'cellspace-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'cellspace-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'cellspace-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'cellspace-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'cellspace-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'cellspace-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'cellspace-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'cellspace-cmd', + score: 0.028955796305270766, + }, + ], + zxjatype: [ + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'zxjatype-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'zxjatype-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'zxjatype-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'zxjatype-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'zxjatype-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'zxjatype-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'zxjatype-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'zxjatype-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'zxjatype-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'zxjatype-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'zxjatype-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'zxjatype-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'zxjatype-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'zxjatype-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'zxjatype-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'zxjatype-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'zxjatype-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'zxjatype-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'zxjatype-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'zxjatype-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'zxjatype-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zxjatype-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'zxjatype-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'zxjatype-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'zxjatype-cmd', + score: 0.2864294797053033, + }, + ], + newclude: [ + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'newclude-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'newclude-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\include{}', + snippet: '\\include{$1}', + meta: 'newclude-cmd', + score: 0.1547080054979312, + }, + ], + 'pgf-umlcd': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-umlcd-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgf-umlcd-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgf-umlcd-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgf-umlcd-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgf-umlcd-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgf-umlcd-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-umlcd-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgf-umlcd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgf-umlcd-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgf-umlcd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgf-umlcd-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgf-umlcd-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgf-umlcd-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgf-umlcd-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgf-umlcd-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgf-umlcd-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgf-umlcd-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgf-umlcd-cmd', + score: 0.2864294797053033, + }, + ], + 'thm-listof': [ + { + caption: '\\listtheoremname', + snippet: '\\listtheoremname', + meta: 'thm-listof-cmd', + score: 1.9443373798666845e-5, + }, + { + caption: '\\thmtformatoptarg', + snippet: '\\thmtformatoptarg', + meta: 'thm-listof-cmd', + score: 6.353668036093916e-5, + }, + { + caption: '\\listoftheorems[]', + snippet: '\\listoftheorems[$1]', + meta: 'thm-listof-cmd', + score: 1.9443373798666845e-5, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'thm-listof-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'thm-listof-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'thm-listof-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'thm-listof-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\proof{}', + snippet: '\\proof{$1}', + meta: 'thm-listof-cmd', + score: 0.000701497773639073, + }, + { + caption: '\\proof', + snippet: '\\proof', + meta: 'thm-listof-cmd', + score: 0.000701497773639073, + }, + { + caption: '\\newtheorem{}[]{}', + snippet: '\\newtheorem{$1}[$2]{$3}', + meta: 'thm-listof-cmd', + score: 0.215689795055434, + }, + { + caption: '\\newtheorem{}{}', + snippet: '\\newtheorem{$1}{$2}', + meta: 'thm-listof-cmd', + score: 0.215689795055434, + }, + { + caption: '\\newtheorem{}{}[]', + snippet: '\\newtheorem{$1}{$2}[$3]', + meta: 'thm-listof-cmd', + score: 0.215689795055434, + }, + { + caption: '\\endproof', + snippet: '\\endproof', + meta: 'thm-listof-cmd', + score: 0.0006133100544751855, + }, + { + caption: '\\endproof{}', + snippet: '\\endproof{$1}', + meta: 'thm-listof-cmd', + score: 0.0006133100544751855, + }, + ], + 'thm-autoref': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'thm-autoref-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\proof{}', + snippet: '\\proof{$1}', + meta: 'thm-autoref-cmd', + score: 0.000701497773639073, + }, + { + caption: '\\proof', + snippet: '\\proof', + meta: 'thm-autoref-cmd', + score: 0.000701497773639073, + }, + { + caption: '\\newtheorem{}[]{}', + snippet: '\\newtheorem{$1}[$2]{$3}', + meta: 'thm-autoref-cmd', + score: 0.215689795055434, + }, + { + caption: '\\newtheorem{}{}', + snippet: '\\newtheorem{$1}{$2}', + meta: 'thm-autoref-cmd', + score: 0.215689795055434, + }, + { + caption: '\\newtheorem{}{}[]', + snippet: '\\newtheorem{$1}{$2}[$3]', + meta: 'thm-autoref-cmd', + score: 0.215689795055434, + }, + { + caption: '\\endproof', + snippet: '\\endproof', + meta: 'thm-autoref-cmd', + score: 0.0006133100544751855, + }, + { + caption: '\\endproof{}', + snippet: '\\endproof{$1}', + meta: 'thm-autoref-cmd', + score: 0.0006133100544751855, + }, + ], + 'thm-patch': [ + { + caption: '\\proof{}', + snippet: '\\proof{$1}', + meta: 'thm-patch-cmd', + score: 0.000701497773639073, + }, + { + caption: '\\proof', + snippet: '\\proof', + meta: 'thm-patch-cmd', + score: 0.000701497773639073, + }, + { + caption: '\\newtheorem{}[]{}', + snippet: '\\newtheorem{$1}[$2]{$3}', + meta: 'thm-patch-cmd', + score: 0.215689795055434, + }, + { + caption: '\\newtheorem{}{}', + snippet: '\\newtheorem{$1}{$2}', + meta: 'thm-patch-cmd', + score: 0.215689795055434, + }, + { + caption: '\\newtheorem{}{}[]', + snippet: '\\newtheorem{$1}{$2}[$3]', + meta: 'thm-patch-cmd', + score: 0.215689795055434, + }, + { + caption: '\\endproof', + snippet: '\\endproof', + meta: 'thm-patch-cmd', + score: 0.0006133100544751855, + }, + { + caption: '\\endproof{}', + snippet: '\\endproof{$1}', + meta: 'thm-patch-cmd', + score: 0.0006133100544751855, + }, + ], + 'thm-kv': [ + { + caption: '\\declaretheoremstyle[]{}', + snippet: '\\declaretheoremstyle[$1]{$2}', + meta: 'thm-kv-cmd', + score: 0.0001168034231635369, + }, + { + caption: '\\declaretheorem[]{}', + snippet: '\\declaretheorem[$1]{$2}', + meta: 'thm-kv-cmd', + score: 0.0004904790216915127, + }, + { + caption: '\\theoremstyle{}', + snippet: '\\theoremstyle{$1}', + meta: 'thm-kv-cmd', + score: 0.02533412165007986, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'thm-kv-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\proof{}', + snippet: '\\proof{$1}', + meta: 'thm-kv-cmd', + score: 0.000701497773639073, + }, + { + caption: '\\proof', + snippet: '\\proof', + meta: 'thm-kv-cmd', + score: 0.000701497773639073, + }, + { + caption: '\\newtheorem{}[]{}', + snippet: '\\newtheorem{$1}[$2]{$3}', + meta: 'thm-kv-cmd', + score: 0.215689795055434, + }, + { + caption: '\\newtheorem{}{}', + snippet: '\\newtheorem{$1}{$2}', + meta: 'thm-kv-cmd', + score: 0.215689795055434, + }, + { + caption: '\\newtheorem{}{}[]', + snippet: '\\newtheorem{$1}{$2}[$3]', + meta: 'thm-kv-cmd', + score: 0.215689795055434, + }, + { + caption: '\\endproof', + snippet: '\\endproof', + meta: 'thm-kv-cmd', + score: 0.0006133100544751855, + }, + { + caption: '\\endproof{}', + snippet: '\\endproof{$1}', + meta: 'thm-kv-cmd', + score: 0.0006133100544751855, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'thm-kv-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'thm-kv-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'thm-kv-cmd', + score: 0.008565354665444157, + }, + ], + onlyamsmath: [ + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'onlyamsmath-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'onlyamsmath-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'onlyamsmath-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'onlyamsmath-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'onlyamsmath-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'onlyamsmath-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'onlyamsmath-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'onlyamsmath-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'onlyamsmath-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'onlyamsmath-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'onlyamsmath-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'onlyamsmath-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'onlyamsmath-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'onlyamsmath-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'onlyamsmath-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'onlyamsmath-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'onlyamsmath-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'onlyamsmath-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'onlyamsmath-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'onlyamsmath-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'onlyamsmath-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'onlyamsmath-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'onlyamsmath-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'onlyamsmath-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'onlyamsmath-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'onlyamsmath-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'onlyamsmath-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'onlyamsmath-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'onlyamsmath-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'onlyamsmath-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'onlyamsmath-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'onlyamsmath-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'onlyamsmath-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'onlyamsmath-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'onlyamsmath-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'onlyamsmath-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'onlyamsmath-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'onlyamsmath-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'onlyamsmath-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'onlyamsmath-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'onlyamsmath-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'onlyamsmath-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'onlyamsmath-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'onlyamsmath-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'onlyamsmath-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'onlyamsmath-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'onlyamsmath-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'onlyamsmath-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'onlyamsmath-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'onlyamsmath-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'onlyamsmath-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'onlyamsmath-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'onlyamsmath-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'onlyamsmath-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'onlyamsmath-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'onlyamsmath-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'onlyamsmath-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'onlyamsmath-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'onlyamsmath-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'onlyamsmath-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'onlyamsmath-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'onlyamsmath-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'onlyamsmath-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'onlyamsmath-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'onlyamsmath-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'onlyamsmath-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'onlyamsmath-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'onlyamsmath-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'onlyamsmath-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'onlyamsmath-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'onlyamsmath-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'onlyamsmath-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'onlyamsmath-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'onlyamsmath-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'onlyamsmath-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'onlyamsmath-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'onlyamsmath-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'onlyamsmath-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'onlyamsmath-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'onlyamsmath-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'onlyamsmath-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'onlyamsmath-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'onlyamsmath-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'onlyamsmath-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'onlyamsmath-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'onlyamsmath-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'onlyamsmath-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'onlyamsmath-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'onlyamsmath-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'onlyamsmath-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'onlyamsmath-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'onlyamsmath-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'onlyamsmath-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'onlyamsmath-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'onlyamsmath-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'onlyamsmath-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'onlyamsmath-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'onlyamsmath-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'onlyamsmath-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'onlyamsmath-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'onlyamsmath-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'onlyamsmath-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'onlyamsmath-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'onlyamsmath-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'onlyamsmath-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'onlyamsmath-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'onlyamsmath-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'onlyamsmath-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'onlyamsmath-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'onlyamsmath-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'onlyamsmath-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'onlyamsmath-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'onlyamsmath-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'onlyamsmath-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'onlyamsmath-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'onlyamsmath-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'onlyamsmath-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'onlyamsmath-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'onlyamsmath-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'onlyamsmath-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'onlyamsmath-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'onlyamsmath-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'onlyamsmath-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'onlyamsmath-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'onlyamsmath-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'onlyamsmath-cmd', + score: 0.0063276692758974925, + }, + ], + arsclassica: [ + { + caption: '\\spacedlowsmallcaps{}', + snippet: '\\spacedlowsmallcaps{$1}', + meta: 'arsclassica-cmd', + score: 0.002677188251799468, + }, + { + caption: '\\sectionmark', + snippet: '\\sectionmark', + meta: 'arsclassica-cmd', + score: 0.005008938879210868, + }, + { + caption: '\\spacedallcaps{}', + snippet: '\\spacedallcaps{$1}', + meta: 'arsclassica-cmd', + score: 0.0015281000475958944, + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'arsclassica-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'arsclassica-cmd', + score: 0.1789117552185788, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\specialrule{}{}{}', + snippet: '\\specialrule{$1}{$2}{$3}', + meta: 'arsclassica-cmd', + score: 0.004974385202605165, + }, + { + caption: '\\cmidrule', + snippet: '\\cmidrule', + meta: 'arsclassica-cmd', + score: 0.01894952272365088, + }, + { + caption: '\\cmidrule{}', + snippet: '\\cmidrule{$1}', + meta: 'arsclassica-cmd', + score: 0.01894952272365088, + }, + { + caption: '\\bottomrule', + snippet: '\\bottomrule', + meta: 'arsclassica-cmd', + score: 0.04533364657852219, + }, + { + caption: '\\midrule', + snippet: '\\midrule', + meta: 'arsclassica-cmd', + score: 0.07098077735912875, + }, + { + caption: '\\addlinespace', + snippet: '\\addlinespace', + meta: 'arsclassica-cmd', + score: 0.005865460617491447, + }, + { + caption: '\\addlinespace[]', + snippet: '\\addlinespace[$1]', + meta: 'arsclassica-cmd', + score: 0.005865460617491447, + }, + { + caption: '\\toprule', + snippet: '\\toprule', + meta: 'arsclassica-cmd', + score: 0.059857788139528495, + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'arsclassica-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'arsclassica-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\captionof{}{}', + snippet: '\\captionof{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.018348594199161503, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'arsclassica-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'arsclassica-cmd', + score: 0.047007158741781095, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'arsclassica-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'arsclassica-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\chapter{}', + snippet: '\\chapter{$1}', + meta: 'arsclassica-cmd', + score: 0.422097569591803, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'arsclassica-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\hspace{}', + snippet: '\\hspace{$1}', + meta: 'arsclassica-cmd', + score: 0.3147206476372336, + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'arsclassica-cmd', + score: 1.2569477427490174, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'arsclassica-cmd', + score: 1.897791904799601, + }, + { + caption: '\\ContinuedFloat', + snippet: '\\ContinuedFloat', + meta: 'arsclassica-cmd', + score: 5.806935368083486e-5, + }, + { + caption: '\\noindent', + snippet: '\\noindent', + meta: 'arsclassica-cmd', + score: 0.42355747798114207, + }, + { + caption: '\\titleclass{}{}[]', + snippet: '\\titleclass{$1}{$2}[$3]', + meta: 'arsclassica-cmd', + score: 0.00028979763314974667, + }, + { + caption: '\\titlelabel{}', + snippet: '\\titlelabel{$1}', + meta: 'arsclassica-cmd', + score: 6.40387839367932e-6, + }, + { + caption: '\\thetitle', + snippet: '\\thetitle', + meta: 'arsclassica-cmd', + score: 0.0015531478302713473, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'arsclassica-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'arsclassica-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\titleformat{}{}{}{}{}[]', + snippet: '\\titleformat{$1}{$2}{$3}{$4}{$5}[$6]', + meta: 'arsclassica-cmd', + score: 0.03475519439740096, + }, + { + caption: '\\titleformat{}[]{}{}{}{}', + snippet: '\\titleformat{$1}[$2]{$3}{$4}{$5}{$6}', + meta: 'arsclassica-cmd', + score: 0.03475519439740096, + }, + { + caption: '\\titleformat{}{}', + snippet: '\\titleformat{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.03475519439740096, + }, + { + caption: '\\titleformat{}{}{}{}{}', + snippet: '\\titleformat{$1}{$2}{$3}{$4}{$5}', + meta: 'arsclassica-cmd', + score: 0.03475519439740096, + }, + { + caption: '\\titlespacing{}{}{}{}', + snippet: '\\titlespacing{$1}{$2}{$3}{$4}', + meta: 'arsclassica-cmd', + score: 0.023062744385192156, + }, + { + caption: '\\markboth{}{}', + snippet: '\\markboth{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.038323601301945065, + }, + { + caption: '\\markboth{}', + snippet: '\\markboth{$1}', + meta: 'arsclassica-cmd', + score: 0.038323601301945065, + }, + { + caption: '\\markright{}', + snippet: '\\markright{$1}', + meta: 'arsclassica-cmd', + score: 0.007138622674767024, + }, + { + caption: '\\markright{}{}', + snippet: '\\markright{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.007138622674767024, + }, + { + caption: '\\filleft', + snippet: '\\filleft', + meta: 'arsclassica-cmd', + score: 7.959989906732799e-5, + }, + { + caption: '\\filcenter', + snippet: '\\filcenter', + meta: 'arsclassica-cmd', + score: 0.0004835660211260246, + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'arsclassica-cmd', + score: 0.2253056071787701, + }, + { + caption: '\\cleardoublepage', + snippet: '\\cleardoublepage', + meta: 'arsclassica-cmd', + score: 0.044016804142963585, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'arsclassica-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\chaptertitlename', + snippet: '\\chaptertitlename', + meta: 'arsclassica-cmd', + score: 0.0016985007766926272, + }, + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'arsclassica-cmd', + score: 0.3277033727934986, + }, + { + caption: '\\filright', + snippet: '\\filright', + meta: 'arsclassica-cmd', + score: 7.959989906732799e-5, + }, + { + caption: '\\titlerule', + snippet: '\\titlerule', + meta: 'arsclassica-cmd', + score: 0.019273712561461216, + }, + { + caption: '\\titlerule[]{}', + snippet: '\\titlerule[$1]{$2}', + meta: 'arsclassica-cmd', + score: 0.019273712561461216, + }, + { + caption: '\\DeclareCaptionJustification{}{}', + snippet: '\\DeclareCaptionJustification{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.0001872850414971473, + }, + { + caption: '\\DeclareCaptionLabelSeparator{}{}', + snippet: '\\DeclareCaptionLabelSeparator{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.0003890810058478364, + }, + { + caption: '\\DeclareCaptionFormat{}{}', + snippet: '\\DeclareCaptionFormat{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.0004717618449370015, + }, + { + caption: '\\DeclareCaptionFont{}{}', + snippet: '\\DeclareCaptionFont{$1}{$2}', + meta: 'arsclassica-cmd', + score: 5.0133404990680195e-5, + }, + { + caption: '\\DeclareCaptionSubType[]{}', + snippet: '\\DeclareCaptionSubType[$1]{$2}', + meta: 'arsclassica-cmd', + score: 0.0001872850414971473, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'arsclassica-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'arsclassica-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'arsclassica-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'arsclassica-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'arsclassica-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\DeclareCaptionType{}[][]', + snippet: '\\DeclareCaptionType{$1}[$2][$3]', + meta: 'arsclassica-cmd', + score: 0.00015256647321237863, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'arsclassica-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'arsclassica-cmd', + score: 0.2253056071787701, + }, + { + caption: '\\footnotemark[]', + snippet: '\\footnotemark[$1]', + meta: 'arsclassica-cmd', + score: 0.021473212893597875, + }, + { + caption: '\\footnotemark', + snippet: '\\footnotemark', + meta: 'arsclassica-cmd', + score: 0.021473212893597875, + }, + { + caption: '\\marginpar{}', + snippet: '\\marginpar{$1}', + meta: 'arsclassica-cmd', + score: 0.003400158497921723, + }, + { + caption: '\\marginpar', + snippet: '\\marginpar', + meta: 'arsclassica-cmd', + score: 0.003400158497921723, + }, + { + caption: '\\cftsecleader', + snippet: '\\cftsecleader', + meta: 'arsclassica-cmd', + score: 0.0011340882025681251, + }, + { + caption: '\\cftsubsecleader', + snippet: '\\cftsubsecleader', + meta: 'arsclassica-cmd', + score: 1.0644172549700836e-5, + }, + { + caption: '\\spacedlowsmallcaps{}', + snippet: '\\spacedlowsmallcaps{$1}', + meta: 'arsclassica-cmd', + score: 0.002677188251799468, + }, + { + caption: '\\sectionmark', + snippet: '\\sectionmark', + meta: 'arsclassica-cmd', + score: 0.005008938879210868, + }, + { + caption: '\\chaptermark', + snippet: '\\chaptermark', + meta: 'arsclassica-cmd', + score: 0.005924520024686584, + }, + { + caption: '\\chaptermark{}', + snippet: '\\chaptermark{$1}', + meta: 'arsclassica-cmd', + score: 0.005924520024686584, + }, + { + caption: '\\part{}', + snippet: '\\part{$1}', + meta: 'arsclassica-cmd', + score: 0.022180129487444723, + }, + { + caption: '\\tocEntry{}', + snippet: '\\tocEntry{$1}', + meta: 'arsclassica-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\graffito{}', + snippet: '\\graffito{$1}', + meta: 'arsclassica-cmd', + score: 1.1006799670632527e-5, + }, + { + caption: '\\chapter{}', + snippet: '\\chapter{$1}', + meta: 'arsclassica-cmd', + score: 0.422097569591803, + }, + { + caption: '\\spacedallcaps{}', + snippet: '\\spacedallcaps{$1}', + meta: 'arsclassica-cmd', + score: 0.0015281000475958944, + }, + { + caption: '\\cftchapleader', + snippet: '\\cftchapleader', + meta: 'arsclassica-cmd', + score: 1.0644172549700836e-5, + }, + { + caption: '\\myVersion', + snippet: '\\myVersion', + meta: 'arsclassica-cmd', + score: 0.00018029288638573757, + }, + { + caption: '\\ctparttext{}', + snippet: '\\ctparttext{$1}', + meta: 'arsclassica-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\addtokomafont{}{}', + snippet: '\\addtokomafont{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.0008555564394100388, + }, + { + caption: '\\setkomafont{}{}', + snippet: '\\setkomafont{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.012985816912639263, + }, + { + caption: '\\KOMAoptions{}', + snippet: '\\KOMAoptions{$1}', + meta: 'arsclassica-cmd', + score: 0.000396664302361659, + }, + { + caption: '\\cite{}', + snippet: '\\cite{$1}', + meta: 'arsclassica-cmd', + score: 2.341195220791228, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'arsclassica-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'arsclassica-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'arsclassica-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'arsclassica-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'arsclassica-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'arsclassica-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'arsclassica-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'arsclassica-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'arsclassica-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\lsstyle', + snippet: '\\lsstyle', + meta: 'arsclassica-cmd', + score: 0.0023367519914345774, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'arsclassica-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\DisableLigatures[]{}', + snippet: '\\DisableLigatures[$1]{$2}', + meta: 'arsclassica-cmd', + score: 0.0009805246614299932, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'arsclassica-cmd', + score: 0.00021116765384691477, + }, + ], + blkarray: [ + { + caption: '\\small', + snippet: '\\small', + meta: 'blkarray-cmd', + score: 0.2447632045426295, + }, + { + caption: '\\small{}', + snippet: '\\small{$1}', + meta: 'blkarray-cmd', + score: 0.2447632045426295, + }, + ], + 'tkz-tab': [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-tab-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-tab-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'tkz-tab-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'tkz-tab-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'tkz-tab-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'tkz-tab-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tkz-tab-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'tkz-tab-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-tab-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'tkz-tab-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tkz-tab-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tkz-tab-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'tkz-tab-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'tkz-tab-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'tkz-tab-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'tkz-tab-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'tkz-tab-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'tkz-tab-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'tkz-tab-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'tkz-tab-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\reserveinserts{}', + snippet: '\\reserveinserts{$1}', + meta: 'tkz-tab-cmd', + score: 0.0018653410309739879, + }, + { + caption: '\\newtoks', + snippet: '\\newtoks', + meta: 'tkz-tab-cmd', + score: 0.00031058155311734754, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-tab-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'tkz-tab-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tkz-tab-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tkz-tab-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'tkz-tab-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'tkz-tab-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'tkz-tab-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tkz-tab-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'tkz-tab-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tkz-tab-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'tkz-tab-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'tkz-tab-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'tkz-tab-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'tkz-tab-cmd', + score: 0.2864294797053033, + }, + ], + todo: [ + { + caption: '\\frak{}', + snippet: '\\frak{$1}', + meta: 'todo-cmd', + score: 0.0017966000518546787, + }, + { + caption: '\\checkmark', + snippet: '\\checkmark', + meta: 'todo-cmd', + score: 0.025060530944368123, + }, + { + caption: '\\bold', + snippet: '\\bold', + meta: 'todo-cmd', + score: 0.0014358547624941567, + }, + { + caption: '\\bold{}', + snippet: '\\bold{$1}', + meta: 'todo-cmd', + score: 0.0014358547624941567, + }, + { + caption: '\\Bbb{}', + snippet: '\\Bbb{$1}', + meta: 'todo-cmd', + score: 0.0006671850995492977, + }, + { + caption: '\\Bbb', + snippet: '\\Bbb', + meta: 'todo-cmd', + score: 0.0006671850995492977, + }, + ], + lcg: [ + { + caption: '\\rand', + snippet: '\\rand', + meta: 'lcg-cmd', + score: 6.2350576842596716e-6, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'lcg-cmd', + score: 0.00037306820619479756, + }, + ], + kantlipsum: [ + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'kantlipsum-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'kantlipsum-cmd', + score: 0.2864294797053033, + }, + ], + chappg: [ + { + caption: '\\pagenumbering{}', + snippet: '\\pagenumbering{$1}', + meta: 'chappg-cmd', + score: 0.06731737633021802, + }, + ], + chessboard: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'chessboard-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'chessboard-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'chessboard-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboardfontencoding{}', + snippet: '\\setboardfontencoding{$1}', + meta: 'chessboard-cmd', + score: 0.00014668111964632249, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chessboard-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chessboard-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'chessboard-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'chessboard-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'chessboard-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'chessboard-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chessboard-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'chessboard-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chessboard-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'chessboard-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chessboard-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chessboard-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'chessboard-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'chessboard-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chessboard-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chessboard-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'chessboard-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'chessboard-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'chessboard-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chessboard-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'chessboard-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chessboard-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'chessboard-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'chessboard-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'chessboard-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'chessboard-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chessboard-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chessboard-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'chessboard-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'chessboard-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\green', + snippet: '\\green', + meta: 'chessboard-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'chessboard-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'chessboard-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'chessboard-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'chessboard-cmd', + score: 0.0005786730478266738, + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'chessboard-cmd', + score: 0.006520475264573554, + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'chessboard-cmd', + score: 0.006520475264573554, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'chessboard-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'chessboard-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'chessboard-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'chessboard-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'chessboard-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chessboard-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chessboard-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chessboard-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chessboard-cmd', + score: 0.008565354665444157, + }, + ], + xskak: [ + { + caption: '\\mainline{}', + snippet: '\\mainline{$1}', + meta: 'xskak-cmd', + score: 0.0010267678375242572, + }, + { + caption: '\\newchessgame', + snippet: '\\newchessgame', + meta: 'xskak-cmd', + score: 0.000880086717877935, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'xskak-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'xskak-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'xskak-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboardfontencoding{}', + snippet: '\\setboardfontencoding{$1}', + meta: 'xskak-cmd', + score: 0.00014668111964632249, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xskak-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xskak-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'xskak-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'xskak-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'xskak-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'xskak-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xskak-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'xskak-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xskak-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'xskak-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'xskak-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'xskak-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'xskak-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'xskak-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'xskak-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xskak-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xskak-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'xskak-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'xskak-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'xskak-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xskak-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'xskak-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'xskak-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xskak-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'xskak-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xskak-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xskak-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'xskak-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'xskak-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'xskak-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xskak-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xskak-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'xskak-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'xskak-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'xskak-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'xskak-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'xskak-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'xskak-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'xskak-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'xskak-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'xskak-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'xskak-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'xskak-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'xskak-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xskak-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'xskak-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'xskak-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'xskak-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'xskak-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'xskak-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'xskak-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'xskak-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'xskak-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'xskak-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'xskak-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'xskak-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'xskak-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'xskak-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xskak-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'xskak-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'xskak-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'xskak-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'xskak-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xskak-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\green', + snippet: '\\green', + meta: 'xskak-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'xskak-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'xskak-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'xskak-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'xskak-cmd', + score: 0.0005786730478266738, + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'xskak-cmd', + score: 0.006520475264573554, + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'xskak-cmd', + score: 0.006520475264573554, + }, + ], + pgfheaps: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfheaps-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfheaps-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfheaps-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfheaps-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfheaps-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfheaps-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfheaps-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfheaps-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfheaps-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfheaps-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfheaps-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfheaps-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfheaps-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfheaps-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfheaps-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfheaps-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfheaps-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfheaps-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfheaps-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfheaps-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfheaps-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfheaps-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfheaps-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfheaps-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfheaps-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfheaps-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfheaps-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfheaps-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfheaps-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfheaps-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfheaps-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfheaps-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfheaps-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfheaps-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfheaps-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfheaps-cmd', + score: 0.2864294797053033, + }, + ], + pgfshade: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'pgfshade-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfshade-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfshade-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'pgfshade-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'pgfshade-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'pgfshade-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'pgfshade-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'pgfshade-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfshade-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'pgfshade-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfshade-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'pgfshade-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfshade-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfshade-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfshade-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'pgfshade-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'pgfshade-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'pgfshade-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'pgfshade-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfshade-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'pgfshade-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pgfshade-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pgfshade-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'pgfshade-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'pgfshade-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'pgfshade-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'pgfshade-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'pgfshade-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pgfshade-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'pgfshade-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'pgfshade-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pgfshade-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'pgfshade-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'pgfshade-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'pgfshade-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'pgfshade-cmd', + score: 0.2864294797053033, + }, + ], + showframe: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'showframe-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'showframe-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'showframe-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'showframe-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'showframe-cmd', + score: 0.00047530324346933345, + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'showframe-cmd', + score: 0.0005277905480209891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'showframe-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\AddToShipoutPictureFG{}', + snippet: '\\AddToShipoutPictureFG{$1}', + meta: 'showframe-cmd', + score: 0.000325977535138643, + }, + { + caption: '\\AddToShipoutPictureBG{}', + snippet: '\\AddToShipoutPictureBG{$1}', + meta: 'showframe-cmd', + score: 0.0008957666085644653, + }, + { + caption: '\\AtPageUpperLeft{}', + snippet: '\\AtPageUpperLeft{$1}', + meta: 'showframe-cmd', + score: 0.0003608141410278152, + }, + { + caption: '\\LenToUnit{}', + snippet: '\\LenToUnit{$1}', + meta: 'showframe-cmd', + score: 0.0007216282820556304, + }, + { + caption: '\\AddToShipoutPicture{}', + snippet: '\\AddToShipoutPicture{$1}', + meta: 'showframe-cmd', + score: 0.0017658629469099734, + }, + ], + psvectorian: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'psvectorian-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'psvectorian-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'psvectorian-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'psvectorian-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'psvectorian-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'psvectorian-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'psvectorian-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'psvectorian-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'psvectorian-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'psvectorian-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'psvectorian-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'psvectorian-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'psvectorian-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'psvectorian-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'psvectorian-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'psvectorian-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'psvectorian-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'psvectorian-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'psvectorian-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'psvectorian-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\green', + snippet: '\\green', + meta: 'psvectorian-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'psvectorian-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'psvectorian-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'psvectorian-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'psvectorian-cmd', + score: 0.0005786730478266738, + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'psvectorian-cmd', + score: 0.006520475264573554, + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'psvectorian-cmd', + score: 0.006520475264573554, + }, + ], + 'pst-grad': [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pst-grad-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pst-grad-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pst-grad-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pst-grad-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pst-grad-cmd', + score: 0.0005786730478266738, + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pst-grad-cmd', + score: 0.006520475264573554, + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pst-grad-cmd', + score: 0.006520475264573554, + }, + ], + cool: [ + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'cool-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'cool-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'cool-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'cool-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'cool-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'cool-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'cool-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'cool-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'cool-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'cool-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'cool-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'cool-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'cool-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'cool-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'cool-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'cool-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'cool-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'cool-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'cool-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'cool-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'cool-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'cool-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'cool-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'cool-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'cool-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'cool-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'cool-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'cool-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'cool-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'cool-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'cool-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'cool-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'cool-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'cool-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'cool-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'cool-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'cool-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'cool-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'cool-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'cool-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'cool-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'cool-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'cool-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'cool-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'cool-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'cool-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'cool-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'cool-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'cool-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'cool-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'cool-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'cool-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'cool-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'cool-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'cool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'cool-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'cool-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'cool-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'cool-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'cool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'cool-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'cool-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'cool-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'cool-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'cool-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'cool-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'cool-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'cool-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'cool-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'cool-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'cool-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'cool-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'cool-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'cool-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'cool-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'cool-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'cool-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'cool-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'cool-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'cool-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'cool-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'cool-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'cool-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'cool-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'cool-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'cool-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'cool-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'cool-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'cool-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'cool-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'cool-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'cool-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'cool-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'cool-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'cool-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'cool-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'cool-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'cool-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'cool-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'cool-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'cool-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'cool-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'cool-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'cool-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'cool-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'cool-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'cool-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'cool-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'cool-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'cool-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'cool-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'cool-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'cool-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'cool-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'cool-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'cool-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'cool-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'cool-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'cool-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'cool-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'cool-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'cool-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'cool-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'cool-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'cool-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'cool-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'cool-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'cool-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'cool-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'cool-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'cool-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'cool-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'cool-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'cool-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'cool-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'cool-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'cool-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'cool-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'cool-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'cool-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'cool-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'cool-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'cool-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'cool-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\frak{}', + snippet: '\\frak{$1}', + meta: 'cool-cmd', + score: 0.0017966000518546787, + }, + { + caption: '\\checkmark', + snippet: '\\checkmark', + meta: 'cool-cmd', + score: 0.025060530944368123, + }, + { + caption: '\\bold', + snippet: '\\bold', + meta: 'cool-cmd', + score: 0.0014358547624941567, + }, + { + caption: '\\bold{}', + snippet: '\\bold{$1}', + meta: 'cool-cmd', + score: 0.0014358547624941567, + }, + { + caption: '\\Bbb{}', + snippet: '\\Bbb{$1}', + meta: 'cool-cmd', + score: 0.0006671850995492977, + }, + { + caption: '\\Bbb', + snippet: '\\Bbb', + meta: 'cool-cmd', + score: 0.0006671850995492977, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'cool-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'cool-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'cool-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'cool-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'cool-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'cool-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'cool-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\forloop{}{}{}{}', + snippet: '\\forloop{$1}{$2}{$3}{$4}', + meta: 'cool-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'cool-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'cool-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'cool-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'cool-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'cool-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'cool-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'cool-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'cool-cmd', + score: 0.0063276692758974925, + }, + ], + xassoccnt: [ + { + caption: '\\NewTotalDocumentCounter{}', + snippet: '\\NewTotalDocumentCounter{$1}', + meta: 'xassoccnt-cmd', + score: 1.5075186740106946e-5, + }, + { + caption: '\\DeclareAssociatedCounters{}{}', + snippet: '\\DeclareAssociatedCounters{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 1.5075186740106946e-5, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xassoccnt-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'xassoccnt-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'xassoccnt-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'xassoccnt-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'xassoccnt-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'xassoccnt-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'xassoccnt-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'xassoccnt-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'xassoccnt-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'xassoccnt-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'xassoccnt-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'xassoccnt-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'xassoccnt-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'xassoccnt-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xassoccnt-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'xassoccnt-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'xassoccnt-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'xassoccnt-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xassoccnt-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xassoccnt-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xassoccnt-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'xassoccnt-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xassoccnt-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xassoccnt-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'xassoccnt-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'xassoccnt-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'xassoccnt-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'xassoccnt-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'xassoccnt-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'xassoccnt-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'xassoccnt-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'xassoccnt-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'xassoccnt-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'xassoccnt-cmd', + score: 0.2864294797053033, + }, + ], + chemscheme: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemscheme-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'chemscheme-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chemscheme-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemscheme-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chemscheme-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chemscheme-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'chemscheme-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'chemscheme-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'chemscheme-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'chemscheme-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'chemscheme-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chemscheme-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'chemscheme-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemscheme-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'chemscheme-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chemscheme-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chemscheme-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chemscheme-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'chemscheme-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'chemscheme-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chemscheme-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemscheme-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chemscheme-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'chemscheme-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chemscheme-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chemscheme-cmd', + score: 0.021170869458413965, + }, + ], + 'pst-all': [ + { + caption: '\\green', + snippet: '\\green', + meta: 'pst-all-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\green{}', + snippet: '\\green{$1}', + meta: 'pst-all-cmd', + score: 0.0016005722621532548, + }, + { + caption: '\\documentclass[]{}', + snippet: '\\documentclass[$1]{$2}', + meta: 'pst-all-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\documentclass{}', + snippet: '\\documentclass{$1}', + meta: 'pst-all-cmd', + score: 1.4425339817971206, + }, + { + caption: '\\gray', + snippet: '\\gray', + meta: 'pst-all-cmd', + score: 0.0005786730478266738, + }, + { + caption: '\\red{}', + snippet: '\\red{$1}', + meta: 'pst-all-cmd', + score: 0.006520475264573554, + }, + { + caption: '\\red', + snippet: '\\red', + meta: 'pst-all-cmd', + score: 0.006520475264573554, + }, + ], + regexpatch: [ + { + caption: '\\xpatchcmd{}{}{}{}{}', + snippet: '\\xpatchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'regexpatch-cmd', + score: 0.0019344877752147675, + }, + { + caption: '\\xpatchcmd', + snippet: '\\xpatchcmd', + meta: 'regexpatch-cmd', + score: 0.0019344877752147675, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'regexpatch-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'regexpatch-cmd', + score: 0.2864294797053033, + }, + ], + chronosys: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'chronosys-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chronosys-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chronosys-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chronosys-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'chronosys-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'chronosys-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'chronosys-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'chronosys-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'chronosys-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chronosys-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'chronosys-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chronosys-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'chronosys-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chronosys-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chronosys-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chronosys-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'chronosys-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chronosys-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chronosys-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chronosys-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'chronosys-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chronosys-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chronosys-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'chronosys-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'chronosys-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'chronosys-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'chronosys-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'chronosys-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chronosys-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'chronosys-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'chronosys-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chronosys-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'chronosys-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'chronosys-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'chronosys-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'chronosys-cmd', + score: 0.2864294797053033, + }, + ], + newfloat: [ + { + caption: '\\DeclareFloatingEnvironment[]{}', + snippet: '\\DeclareFloatingEnvironment[$1]{$2}', + meta: 'newfloat-cmd', + score: 2.603029874713569e-5, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'newfloat-cmd', + score: 0.00037306820619479756, + }, + ], + zref: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'zref-cmd', + score: 0.0058342578961340175, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'zref-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'zref-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'zref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'zref-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'zref-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'zref-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'zref-cmd', + score: 0.002958865219480927, + }, + ], + bmpsize: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'bmpsize-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bmpsize-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bmpsize-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'bmpsize-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'bmpsize-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'bmpsize-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'bmpsize-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'bmpsize-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bmpsize-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'bmpsize-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bmpsize-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'bmpsize-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'bmpsize-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'bmpsize-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'bmpsize-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'bmpsize-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bmpsize-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bmpsize-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bmpsize-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'bmpsize-cmd', + score: 0.0058342578961340175, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'bmpsize-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bmpsize-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bmpsize-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bmpsize-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bmpsize-cmd', + score: 0.021170869458413965, + }, + ], + steinmetz: [ + { + caption: '\\Line', + snippet: '\\Line', + meta: 'steinmetz-cmd', + score: 0.0006078790177929149, + }, + { + caption: '\\polygon', + snippet: '\\polygon', + meta: 'steinmetz-cmd', + score: 0.0008987552240147395, + }, + { + caption: '\\line', + snippet: '\\line', + meta: 'steinmetz-cmd', + score: 0.014519741542622297, + }, + { + caption: '\\polyline', + snippet: '\\polyline', + meta: 'steinmetz-cmd', + score: 0.00022468880600368487, + }, + { + caption: '\\vector', + snippet: '\\vector', + meta: 'steinmetz-cmd', + score: 0.002970308722584179, + }, + ], + pageslts: [ + { + caption: '\\thepage', + snippet: '\\thepage', + meta: 'pageslts-cmd', + score: 0.0591555998103519, + }, + { + caption: '\\pagenumbering{}', + snippet: '\\pagenumbering{$1}', + meta: 'pageslts-cmd', + score: 0.06731737633021802, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'pageslts-cmd', + score: 0.1789117552185788, + }, + { + caption: '\\global', + snippet: '\\global', + meta: 'pageslts-cmd', + score: 0.006609629561859019, + }, + { + caption: '\\makeindex', + snippet: '\\makeindex', + meta: 'pageslts-cmd', + score: 0.010304996748556729, + }, + { + caption: '\\index{}', + snippet: '\\index{$1}', + meta: 'pageslts-cmd', + score: 0.013774721817648336, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pageslts-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'pageslts-cmd', + score: 0.0058342578961340175, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'pageslts-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pageslts-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pageslts-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pageslts-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pageslts-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pageslts-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pageslts-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pageslts-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pageslts-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pageslts-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pageslts-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pageslts-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pageslts-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pageslts-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pageslts-cmd', + score: 0.008565354665444157, + }, + ], + chronology: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'chronology-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'chronology-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\setlength{}{}', + snippet: '\\setlength{$1}{$2}', + meta: 'chronology-cmd', + score: 0.354445763583904, + }, + { + caption: '\\setlength', + snippet: '\\setlength', + meta: 'chronology-cmd', + score: 0.354445763583904, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chronology-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chronology-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'chronology-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\setcounter{}{}', + snippet: '\\setcounter{$1}{$2}', + meta: 'chronology-cmd', + score: 0.10068045662118841, + }, + { + caption: '\\addtolength{}{}', + snippet: '\\addtolength{$1}{$2}', + meta: 'chronology-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\addtolength', + snippet: '\\addtolength', + meta: 'chronology-cmd', + score: 0.028955796305270766, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chronology-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chronology-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chronology-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'chronology-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'chronology-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'chronology-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'chronology-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'chronology-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chronology-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'chronology-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chronology-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'chronology-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chronology-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chronology-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chronology-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'chronology-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'chronology-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'chronology-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'chronology-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'chronology-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'chronology-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'chronology-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'chronology-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'chronology-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'chronology-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'chronology-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'chronology-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'chronology-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'chronology-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'chronology-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'chronology-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'chronology-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'chronology-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'chronology-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'chronology-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'chronology-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'chronology-cmd', + score: 0.2864294797053033, + }, + ], + spreadtab: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'spreadtab-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'spreadtab-cmd', + score: 0.021170869458413965, + }, + ], + algpascal: [ + { + caption: '\\algrenewcommand', + snippet: '\\algrenewcommand', + meta: 'algpascal-cmd', + score: 0.0019861803661869416, + }, + { + caption: '\\Statex', + snippet: '\\Statex', + meta: 'algpascal-cmd', + score: 0.008622777195102994, + }, + { + caption: '\\BState{}', + snippet: '\\BState{$1}', + meta: 'algpascal-cmd', + score: 0.0008685861525307122, + }, + { + caption: '\\BState', + snippet: '\\BState', + meta: 'algpascal-cmd', + score: 0.0008685861525307122, + }, + { + caption: '\\algloopdefx{}[][]{}', + snippet: '\\algloopdefx{$1}[$2][$3]{$4}', + meta: 'algpascal-cmd', + score: 0.00025315185701145097, + }, + { + caption: '\\algnewcommand', + snippet: '\\algnewcommand', + meta: 'algpascal-cmd', + score: 0.0030209395012065327, + }, + { + caption: '\\algnewcommand{}[]{}', + snippet: '\\algnewcommand{$1}[$2]{$3}', + meta: 'algpascal-cmd', + score: 0.0030209395012065327, + }, + { + caption: '\\Comment{}', + snippet: '\\Comment{$1}', + meta: 'algpascal-cmd', + score: 0.005178604573219454, + }, + { + caption: '\\algblockdefx{}{}[]', + snippet: '\\algblockdefx{$1}{$2}[$3]', + meta: 'algpascal-cmd', + score: 0.00025315185701145097, + }, + { + caption: '\\algrenewtext{}{}', + snippet: '\\algrenewtext{$1}{$2}', + meta: 'algpascal-cmd', + score: 0.0024415580558825975, + }, + { + caption: '\\algrenewtext{}[]{}', + snippet: '\\algrenewtext{$1}[$2]{$3}', + meta: 'algpascal-cmd', + score: 0.0024415580558825975, + }, + { + caption: '\\algblock{}{}', + snippet: '\\algblock{$1}{$2}', + meta: 'algpascal-cmd', + score: 0.0007916858220314837, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'algpascal-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\algdef{}[]{}{}{}{}', + snippet: '\\algdef{$1}[$2]{$3}{$4}{$5}{$6}', + meta: 'algpascal-cmd', + score: 0.0003102486920966127, + }, + { + caption: '\\algdef{}[]{}{}[]{}{}', + snippet: '\\algdef{$1}[$2]{$3}{$4}[$5]{$6}{$7}', + meta: 'algpascal-cmd', + score: 0.0003102486920966127, + }, + { + caption: '\\algdef{}[]{}[]{}', + snippet: '\\algdef{$1}[$2]{$3}[$4]{$5}', + meta: 'algpascal-cmd', + score: 0.0003102486920966127, + }, + { + caption: '\\algtext{}', + snippet: '\\algtext{$1}', + meta: 'algpascal-cmd', + score: 0.0005463612015579842, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'algpascal-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'algpascal-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'algpascal-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'algpascal-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'algpascal-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'algpascal-cmd', + score: 0.0018957469739775527, + }, + ], + cabin: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'cabin-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'cabin-cmd', + score: 0.008565354665444157, + }, + ], + erewhon: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'erewhon-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'erewhon-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'erewhon-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'erewhon-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'erewhon-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'erewhon-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'erewhon-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'erewhon-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'erewhon-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'erewhon-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'erewhon-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'erewhon-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'erewhon-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'erewhon-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'erewhon-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'erewhon-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'erewhon-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'erewhon-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'erewhon-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'erewhon-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'erewhon-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'erewhon-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'erewhon-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'erewhon-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'erewhon-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'erewhon-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'erewhon-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'erewhon-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'erewhon-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'erewhon-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'erewhon-cmd', + score: 0.008565354665444157, + }, + ], + tgcursor: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgcursor-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgcursor-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgcursor-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgcursor-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'tgcursor-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgcursor-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'tgcursor-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'tgcursor-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'tgcursor-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'tgcursor-cmd', + score: 0.021170869458413965, + }, + ], + ifvtex: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'ifvtex-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'ifvtex-cmd', + score: 0.002958865219480927, + }, + ], + memhfixc: [ + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'memhfixc-cmd', + score: 1.2569477427490174, + }, + ], + longfigure: [ + { + caption: '\\newpage', + snippet: '\\newpage', + meta: 'longfigure-cmd', + score: 0.3277033727934986, + }, + ], + lato: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'lato-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'lato-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'lato-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'lato-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'lato-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'lato-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\scshape', + snippet: '\\scshape', + meta: 'lato-cmd', + score: 0.05364108855914402, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'lato-cmd', + score: 0.00037306820619479756, + }, + ], + authoraftertitle: [ + { + caption: '\\author{}', + snippet: '\\author{$1}', + meta: 'authoraftertitle-cmd', + score: 0.8973590434087177, + }, + { + caption: '\\author[]{}', + snippet: '\\author[$1]{$2}', + meta: 'authoraftertitle-cmd', + score: 0.8973590434087177, + }, + { + caption: '\\title{}', + snippet: '\\title{$1}', + meta: 'authoraftertitle-cmd', + score: 0.9202908262245683, + }, + ], + listofsymbols: [ + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'listofsymbols-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'listofsymbols-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'listofsymbols-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'listofsymbols-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'listofsymbols-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'listofsymbols-cmd', + score: 0.0018957469739775527, + }, + ], + hvfloat: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'hvfloat-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hvfloat-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'hvfloat-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'hvfloat-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'hvfloat-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'hvfloat-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'hvfloat-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\captionof{}{}', + snippet: '\\captionof{$1}{$2}', + meta: 'hvfloat-cmd', + score: 0.018348594199161503, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'hvfloat-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'hvfloat-cmd', + score: 0.047007158741781095, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hvfloat-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'hvfloat-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\chapter{}', + snippet: '\\chapter{$1}', + meta: 'hvfloat-cmd', + score: 0.422097569591803, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hvfloat-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\hspace{}', + snippet: '\\hspace{$1}', + meta: 'hvfloat-cmd', + score: 0.3147206476372336, + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'hvfloat-cmd', + score: 1.2569477427490174, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'hvfloat-cmd', + score: 1.897791904799601, + }, + { + caption: '\\ContinuedFloat', + snippet: '\\ContinuedFloat', + meta: 'hvfloat-cmd', + score: 5.806935368083486e-5, + }, + { + caption: '\\noindent', + snippet: '\\noindent', + meta: 'hvfloat-cmd', + score: 0.42355747798114207, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hvfloat-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hvfloat-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'hvfloat-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'hvfloat-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'hvfloat-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'hvfloat-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'hvfloat-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hvfloat-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'hvfloat-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hvfloat-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'hvfloat-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'hvfloat-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'hvfloat-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'hvfloat-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'hvfloat-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\DeclareCaptionJustification{}{}', + snippet: '\\DeclareCaptionJustification{$1}{$2}', + meta: 'hvfloat-cmd', + score: 0.0001872850414971473, + }, + { + caption: '\\DeclareCaptionLabelSeparator{}{}', + snippet: '\\DeclareCaptionLabelSeparator{$1}{$2}', + meta: 'hvfloat-cmd', + score: 0.0003890810058478364, + }, + { + caption: '\\DeclareCaptionFormat{}{}', + snippet: '\\DeclareCaptionFormat{$1}{$2}', + meta: 'hvfloat-cmd', + score: 0.0004717618449370015, + }, + { + caption: '\\DeclareCaptionFont{}{}', + snippet: '\\DeclareCaptionFont{$1}{$2}', + meta: 'hvfloat-cmd', + score: 5.0133404990680195e-5, + }, + { + caption: '\\DeclareCaptionSubType[]{}', + snippet: '\\DeclareCaptionSubType[$1]{$2}', + meta: 'hvfloat-cmd', + score: 0.0001872850414971473, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'hvfloat-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'hvfloat-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\captionsetup{}', + snippet: '\\captionsetup{$1}', + meta: 'hvfloat-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\captionsetup[]{}', + snippet: '\\captionsetup[$1]{$2}', + meta: 'hvfloat-cmd', + score: 0.02900783226643065, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'hvfloat-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\DeclareCaptionType{}[][]', + snippet: '\\DeclareCaptionType{$1}[$2][$3]', + meta: 'hvfloat-cmd', + score: 0.00015256647321237863, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hvfloat-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\footnote{}', + snippet: '\\footnote{$1}', + meta: 'hvfloat-cmd', + score: 0.2253056071787701, + }, + { + caption: '\\footnotemark[]', + snippet: '\\footnotemark[$1]', + meta: 'hvfloat-cmd', + score: 0.021473212893597875, + }, + { + caption: '\\footnotemark', + snippet: '\\footnotemark', + meta: 'hvfloat-cmd', + score: 0.021473212893597875, + }, + ], + thmbox: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'thmbox-cmd', + score: 0.00037306820619479756, + }, + ], + proba: [ + { + caption: '\\frak{}', + snippet: '\\frak{$1}', + meta: 'proba-cmd', + score: 0.0017966000518546787, + }, + { + caption: '\\checkmark', + snippet: '\\checkmark', + meta: 'proba-cmd', + score: 0.025060530944368123, + }, + { + caption: '\\bold', + snippet: '\\bold', + meta: 'proba-cmd', + score: 0.0014358547624941567, + }, + { + caption: '\\bold{}', + snippet: '\\bold{$1}', + meta: 'proba-cmd', + score: 0.0014358547624941567, + }, + { + caption: '\\Bbb{}', + snippet: '\\Bbb{$1}', + meta: 'proba-cmd', + score: 0.0006671850995492977, + }, + { + caption: '\\Bbb', + snippet: '\\Bbb', + meta: 'proba-cmd', + score: 0.0006671850995492977, + }, + ], + datatool: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'datatool-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'datatool-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\longmapsto', + snippet: '\\longmapsto', + meta: 'datatool-cmd', + score: 0.0017755897148012264, + }, + { + caption: '\\Check{}', + snippet: '\\Check{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'datatool-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'datatool-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\iff', + snippet: '\\iff', + meta: 'datatool-cmd', + score: 0.004209937150980285, + }, + { + caption: '\\And', + snippet: '\\And', + meta: 'datatool-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\And{}', + snippet: '\\And{$1}', + meta: 'datatool-cmd', + score: 0.0011582952152188854, + }, + { + caption: '\\oint', + snippet: '\\oint', + meta: 'datatool-cmd', + score: 0.0028650540724050534, + }, + { + caption: '\\boxed{}', + snippet: '\\boxed{$1}', + meta: 'datatool-cmd', + score: 0.0035536135737312827, + }, + { + caption: '\\Ddot{}', + snippet: '\\Ddot{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ignorespacesafterend', + snippet: '\\ignorespacesafterend', + meta: 'datatool-cmd', + score: 0.0010893680553454854, + }, + { + caption: '\\nonumber', + snippet: '\\nonumber', + meta: 'datatool-cmd', + score: 0.051980653969641216, + }, + { + caption: '\\Breve{}', + snippet: '\\Breve{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\mapsto', + snippet: '\\mapsto', + meta: 'datatool-cmd', + score: 0.006473769486518971, + }, + { + caption: '\\over{}', + snippet: '\\over{$1}', + meta: 'datatool-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\over', + snippet: '\\over', + meta: 'datatool-cmd', + score: 0.0054372322008878786, + }, + { + caption: '\\bigotimes', + snippet: '\\bigotimes', + meta: 'datatool-cmd', + score: 0.000984722260624791, + }, + { + caption: '\\bigoplus', + snippet: '\\bigoplus', + meta: 'datatool-cmd', + score: 0.0011508785476242003, + }, + { + caption: '\\theequation', + snippet: '\\theequation', + meta: 'datatool-cmd', + score: 0.002995924112493351, + }, + { + caption: '\\bigcap', + snippet: '\\bigcap', + meta: 'datatool-cmd', + score: 0.005709261168797874, + }, + { + caption: '\\xrightarrow{}', + snippet: '\\xrightarrow{$1}', + meta: 'datatool-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\xrightarrow[]{}', + snippet: '\\xrightarrow[$1]{$2}', + meta: 'datatool-cmd', + score: 0.004163642482777231, + }, + { + caption: '\\atop', + snippet: '\\atop', + meta: 'datatool-cmd', + score: 0.0006518541515279979, + }, + { + caption: '\\dfrac{}{}', + snippet: '\\dfrac{$1}{$2}', + meta: 'datatool-cmd', + score: 0.05397545277891961, + }, + { + caption: '\\pmod', + snippet: '\\pmod', + meta: 'datatool-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\pmod{}', + snippet: '\\pmod{$1}', + meta: 'datatool-cmd', + score: 0.0011773327219377148, + }, + { + caption: '\\notag', + snippet: '\\notag', + meta: 'datatool-cmd', + score: 0.00322520920930312, + }, + { + caption: '\\int', + snippet: '\\int', + meta: 'datatool-cmd', + score: 0.11946660537765894, + }, + { + caption: '\\Vec{}', + snippet: '\\Vec{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\bigvee', + snippet: '\\bigvee', + meta: 'datatool-cmd', + score: 0.0011677288242806726, + }, + { + caption: '\\sum', + snippet: '\\sum', + meta: 'datatool-cmd', + score: 0.42607994509619934, + }, + { + caption: '\\hookrightarrow', + snippet: '\\hookrightarrow', + meta: 'datatool-cmd', + score: 0.0015607282046545064, + }, + { + caption: '\\bigsqcup', + snippet: '\\bigsqcup', + meta: 'datatool-cmd', + score: 0.0003468284144579442, + }, + { + caption: '\\hookleftarrow', + snippet: '\\hookleftarrow', + meta: 'datatool-cmd', + score: 0.0016498799924012809, + }, + { + caption: '\\Dot{}', + snippet: '\\Dot{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\dots', + snippet: '\\dots', + meta: 'datatool-cmd', + score: 0.0847414497955395, + }, + { + caption: '\\genfrac{}{}{}{}{}{}', + snippet: '\\genfrac{$1}{$2}{$3}{$4}{$5}{$6}', + meta: 'datatool-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\genfrac', + snippet: '\\genfrac', + meta: 'datatool-cmd', + score: 0.004820143328295316, + }, + { + caption: '\\cfrac{}{}', + snippet: '\\cfrac{$1}{$2}', + meta: 'datatool-cmd', + score: 0.006765684097139381, + }, + { + caption: '\\Acute{}', + snippet: '\\Acute{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\ldots', + snippet: '\\ldots', + meta: 'datatool-cmd', + score: 0.11585556755884258, + }, + { + caption: '\\coprod', + snippet: '\\coprod', + meta: 'datatool-cmd', + score: 0.00011383372700282614, + }, + { + caption: '\\impliedby', + snippet: '\\impliedby', + meta: 'datatool-cmd', + score: 2.3482915591834053e-5, + }, + { + caption: '\\big', + snippet: '\\big', + meta: 'datatool-cmd', + score: 0.05613164277964739, + }, + { + caption: '\\idotsint', + snippet: '\\idotsint', + meta: 'datatool-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\Longrightarrow', + snippet: '\\Longrightarrow', + meta: 'datatool-cmd', + score: 0.002459139437356601, + }, + { + caption: '\\allowdisplaybreaks', + snippet: '\\allowdisplaybreaks', + meta: 'datatool-cmd', + score: 0.005931777024772073, + }, + { + caption: '\\eqref{}', + snippet: '\\eqref{$1}', + meta: 'datatool-cmd', + score: 0.06345266254167037, + }, + { + caption: '\\mod', + snippet: '\\mod', + meta: 'datatool-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\mod{}', + snippet: '\\mod{$1}', + meta: 'datatool-cmd', + score: 0.0015181439193121889, + }, + { + caption: '\\arraystretch', + snippet: '\\arraystretch', + meta: 'datatool-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\arraystretch{}', + snippet: '\\arraystretch{$1}', + meta: 'datatool-cmd', + score: 0.022224283488673075, + }, + { + caption: '\\bigg', + snippet: '\\bigg', + meta: 'datatool-cmd', + score: 0.04318078602869565, + }, + { + caption: '\\underset{}{}', + snippet: '\\underset{$1}{$2}', + meta: 'datatool-cmd', + score: 0.012799893214578391, + }, + { + caption: '\\dotsc', + snippet: '\\dotsc', + meta: 'datatool-cmd', + score: 0.0008555101484119994, + }, + { + caption: '\\doteq', + snippet: '\\doteq', + meta: 'datatool-cmd', + score: 3.164631070474435e-5, + }, + { + caption: '\\leftroot{}', + snippet: '\\leftroot{$1}', + meta: 'datatool-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\substack{}', + snippet: '\\substack{$1}', + meta: 'datatool-cmd', + score: 0.0037482529712850755, + }, + { + caption: '\\Hat{}', + snippet: '\\Hat{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\frac{}{}', + snippet: '\\frac{$1}{$2}', + meta: 'datatool-cmd', + score: 1.4341091141105058, + }, + { + caption: '\\mspace{}', + snippet: '\\mspace{$1}', + meta: 'datatool-cmd', + score: 3.423236656565836e-5, + }, + { + caption: '\\Bar{}', + snippet: '\\Bar{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\Grave{}', + snippet: '\\Grave{$1}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\implies', + snippet: '\\implies', + meta: 'datatool-cmd', + score: 0.021828316911576096, + }, + { + caption: '\\tbinom', + snippet: '\\tbinom', + meta: 'datatool-cmd', + score: 1.3908704929884828e-5, + }, + { + caption: '\\dotsi', + snippet: '\\dotsi', + meta: 'datatool-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\bigwedge', + snippet: '\\bigwedge', + meta: 'datatool-cmd', + score: 0.000347742918592393, + }, + { + caption: '\\sideset{}{}', + snippet: '\\sideset{$1}{$2}', + meta: 'datatool-cmd', + score: 5.563481971953931e-5, + }, + { + caption: '\\smash{}', + snippet: '\\smash{$1}', + meta: 'datatool-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\smash[]{}', + snippet: '\\smash[$1]{$2}', + meta: 'datatool-cmd', + score: 0.008197171096663127, + }, + { + caption: '\\colon', + snippet: '\\colon', + meta: 'datatool-cmd', + score: 0.005300291684408929, + }, + { + caption: '\\intertext{}', + snippet: '\\intertext{$1}', + meta: 'datatool-cmd', + score: 0.0016148076375871775, + }, + { + caption: '\\Longleftarrow', + snippet: '\\Longleftarrow', + meta: 'datatool-cmd', + score: 8.477207854183949e-5, + }, + { + caption: '\\prod', + snippet: '\\prod', + meta: 'datatool-cmd', + score: 0.02549889375975901, + }, + { + caption: '\\AmS', + snippet: '\\AmS', + meta: 'datatool-cmd', + score: 0.00047859486202980376, + }, + { + caption: '\\overline{}', + snippet: '\\overline{$1}', + meta: 'datatool-cmd', + score: 0.11280487530505384, + }, + { + caption: '\\tfrac{}{}', + snippet: '\\tfrac{$1}{$2}', + meta: 'datatool-cmd', + score: 0.0005923542426657187, + }, + { + caption: '\\uproot{}', + snippet: '\\uproot{$1}', + meta: 'datatool-cmd', + score: 6.625561928497235e-5, + }, + { + caption: '\\bmod', + snippet: '\\bmod', + meta: 'datatool-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\bmod{}', + snippet: '\\bmod{$1}', + meta: 'datatool-cmd', + score: 0.002022594681005002, + }, + { + caption: '\\pod{}', + snippet: '\\pod{$1}', + meta: 'datatool-cmd', + score: 2.7817409859769657e-5, + }, + { + caption: '\\label{}', + snippet: '\\label{$1}', + meta: 'datatool-cmd', + score: 1.897791904799601, + }, + { + caption: '\\longrightarrow', + snippet: '\\longrightarrow', + meta: 'datatool-cmd', + score: 0.013399422292458848, + }, + { + caption: '\\xleftarrow[]{}', + snippet: '\\xleftarrow[$1]{$2}', + meta: 'datatool-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\xleftarrow{}', + snippet: '\\xleftarrow{$1}', + meta: 'datatool-cmd', + score: 3.5779964196240445e-5, + }, + { + caption: '\\mathaccentV', + snippet: '\\mathaccentV', + meta: 'datatool-cmd', + score: 6.216218551413489e-5, + }, + { + caption: '\\hdotsfor{}', + snippet: '\\hdotsfor{$1}', + meta: 'datatool-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\hdotsfor[]{}', + snippet: '\\hdotsfor[$1]{$2}', + meta: 'datatool-cmd', + score: 0.00024247684499275043, + }, + { + caption: '\\Bigg', + snippet: '\\Bigg', + meta: 'datatool-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\Bigg[]', + snippet: '\\Bigg[$1]', + meta: 'datatool-cmd', + score: 0.015507614799858266, + }, + { + caption: '\\overset{}{}', + snippet: '\\overset{$1}{$2}', + meta: 'datatool-cmd', + score: 0.007611544955294224, + }, + { + caption: '\\Big', + snippet: '\\Big', + meta: 'datatool-cmd', + score: 0.050370758781422345, + }, + { + caption: '\\longleftrightarrow', + snippet: '\\longleftrightarrow', + meta: 'datatool-cmd', + score: 0.0002851769278703356, + }, + { + caption: '\\Longleftrightarrow', + snippet: '\\Longleftrightarrow', + meta: 'datatool-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\Longleftrightarrow{}', + snippet: '\\Longleftrightarrow{$1}', + meta: 'datatool-cmd', + score: 0.0004896780659212191, + }, + { + caption: '\\binom{}{}', + snippet: '\\binom{$1}{$2}', + meta: 'datatool-cmd', + score: 0.013010882180364367, + }, + { + caption: '\\longleftarrow', + snippet: '\\longleftarrow', + meta: 'datatool-cmd', + score: 0.0011096532692473691, + }, + { + caption: '\\dbinom{}{}', + snippet: '\\dbinom{$1}{$2}', + meta: 'datatool-cmd', + score: 0.006800272303210672, + }, + { + caption: '\\Tilde{}', + snippet: '\\Tilde{$1}', + meta: 'datatool-cmd', + score: 7.874446783586035e-5, + }, + { + caption: '\\bigcup', + snippet: '\\bigcup', + meta: 'datatool-cmd', + score: 0.0058847868741168765, + }, + { + caption: '\\sinh', + snippet: '\\sinh', + meta: 'datatool-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\sinh{}', + snippet: '\\sinh{$1}', + meta: 'datatool-cmd', + score: 0.0006435164702005918, + }, + { + caption: '\\operatorname{}', + snippet: '\\operatorname{$1}', + meta: 'datatool-cmd', + score: 0.02181954887028883, + }, + { + caption: '\\max', + snippet: '\\max', + meta: 'datatool-cmd', + score: 0.04116833357968482, + }, + { + caption: '\\liminf', + snippet: '\\liminf', + meta: 'datatool-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\liminf{}', + snippet: '\\liminf{$1}', + meta: 'datatool-cmd', + score: 0.0015513861600956144, + }, + { + caption: '\\operatornamewithlimits{}', + snippet: '\\operatornamewithlimits{$1}', + meta: 'datatool-cmd', + score: 0.0022415507993352067, + }, + { + caption: '\\exp', + snippet: '\\exp', + meta: 'datatool-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\exp{}', + snippet: '\\exp{$1}', + meta: 'datatool-cmd', + score: 0.02404262443651467, + }, + { + caption: '\\lim', + snippet: '\\lim', + meta: 'datatool-cmd', + score: 0.05285123457928509, + }, + { + caption: '\\sin', + snippet: '\\sin', + meta: 'datatool-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\sin{}', + snippet: '\\sin{$1}', + meta: 'datatool-cmd', + score: 0.040463088537699636, + }, + { + caption: '\\arg', + snippet: '\\arg', + meta: 'datatool-cmd', + score: 0.007190995792600074, + }, + { + caption: '\\cos', + snippet: '\\cos', + meta: 'datatool-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\cos{}', + snippet: '\\cos{$1}', + meta: 'datatool-cmd', + score: 0.050370402546134785, + }, + { + caption: '\\varliminf', + snippet: '\\varliminf', + meta: 'datatool-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\hom', + snippet: '\\hom', + meta: 'datatool-cmd', + score: 8.180643329881783e-5, + }, + { + caption: '\\tan', + snippet: '\\tan', + meta: 'datatool-cmd', + score: 0.006176447465423192, + }, + { + caption: '\\det', + snippet: '\\det', + meta: 'datatool-cmd', + score: 0.005640718203101287, + }, + { + caption: '\\ln', + snippet: '\\ln', + meta: 'datatool-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\ln{}', + snippet: '\\ln{$1}', + meta: 'datatool-cmd', + score: 0.025366949660913504, + }, + { + caption: '\\cosh', + snippet: '\\cosh', + meta: 'datatool-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\cosh{}', + snippet: '\\cosh{$1}', + meta: 'datatool-cmd', + score: 0.0008896391580266903, + }, + { + caption: '\\gcd', + snippet: '\\gcd', + meta: 'datatool-cmd', + score: 0.002254008371792865, + }, + { + caption: '\\limsup', + snippet: '\\limsup', + meta: 'datatool-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\limsup{}', + snippet: '\\limsup{$1}', + meta: 'datatool-cmd', + score: 0.002354950225950599, + }, + { + caption: '\\inf', + snippet: '\\inf', + meta: 'datatool-cmd', + score: 0.00340470256994063, + }, + { + caption: '\\arccos', + snippet: '\\arccos', + meta: 'datatool-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\arccos{}', + snippet: '\\arccos{$1}', + meta: 'datatool-cmd', + score: 0.001781687642431819, + }, + { + caption: '\\ker', + snippet: '\\ker', + meta: 'datatool-cmd', + score: 0.002475379242338094, + }, + { + caption: '\\cot', + snippet: '\\cot', + meta: 'datatool-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\cot{}', + snippet: '\\cot{$1}', + meta: 'datatool-cmd', + score: 0.0003640644365701238, + }, + { + caption: '\\coth{}', + snippet: '\\coth{$1}', + meta: 'datatool-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\coth', + snippet: '\\coth', + meta: 'datatool-cmd', + score: 0.00025939638266884963, + }, + { + caption: '\\varlimsup', + snippet: '\\varlimsup', + meta: 'datatool-cmd', + score: 6.204977642542802e-5, + }, + { + caption: '\\log', + snippet: '\\log', + meta: 'datatool-cmd', + score: 0.048131780413380156, + }, + { + caption: '\\varinjlim', + snippet: '\\varinjlim', + meta: 'datatool-cmd', + score: 0.000361814283649031, + }, + { + caption: '\\deg', + snippet: '\\deg', + meta: 'datatool-cmd', + score: 0.005542465148816408, + }, + { + caption: '\\arctan', + snippet: '\\arctan', + meta: 'datatool-cmd', + score: 0.0011971697553682045, + }, + { + caption: '\\dim', + snippet: '\\dim', + meta: 'datatool-cmd', + score: 0.0038210003967178293, + }, + { + caption: '\\min', + snippet: '\\min', + meta: 'datatool-cmd', + score: 0.03051120054363316, + }, + { + caption: '\\Pr', + snippet: '\\Pr', + meta: 'datatool-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\Pr[]', + snippet: '\\Pr[$1]', + meta: 'datatool-cmd', + score: 0.010227440663206161, + }, + { + caption: '\\tanh', + snippet: '\\tanh', + meta: 'datatool-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\tanh{}', + snippet: '\\tanh{$1}', + meta: 'datatool-cmd', + score: 0.0021229156376192525, + }, + { + caption: '\\arcsin', + snippet: '\\arcsin', + meta: 'datatool-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\arcsin{}', + snippet: '\\arcsin{$1}', + meta: 'datatool-cmd', + score: 0.0007754886988089101, + }, + { + caption: '\\DeclareMathOperator{}{}', + snippet: '\\DeclareMathOperator{$1}{$2}', + meta: 'datatool-cmd', + score: 0.029440493885398676, + }, + { + caption: '\\csc', + snippet: '\\csc', + meta: 'datatool-cmd', + score: 0.00013963711107573638, + }, + { + caption: '\\sup', + snippet: '\\sup', + meta: 'datatool-cmd', + score: 0.009355514755312534, + }, + { + caption: '\\sec', + snippet: '\\sec', + meta: 'datatool-cmd', + score: 0.0005912636157903734, + }, + { + caption: '\\varprojlim', + snippet: '\\varprojlim', + meta: 'datatool-cmd', + score: 0.0004286136584068833, + }, + { + caption: '\\stepcounter{}', + snippet: '\\stepcounter{$1}', + meta: 'datatool-cmd', + score: 0.0030745841706804776, + }, + { + caption: '\\addtocounter{}{}', + snippet: '\\addtocounter{$1}{$2}', + meta: 'datatool-cmd', + score: 0.010241823778997489, + }, + { + caption: '\\text{}', + snippet: '\\text{$1}', + meta: 'datatool-cmd', + score: 0.3608680734736821, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'datatool-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\pmb{}', + snippet: '\\pmb{$1}', + meta: 'datatool-cmd', + score: 0.019171182556792562, + }, + { + caption: '\\boldsymbol{}', + snippet: '\\boldsymbol{$1}', + meta: 'datatool-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\boldsymbol', + snippet: '\\boldsymbol', + meta: 'datatool-cmd', + score: 0.18137737738638837, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'datatool-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'datatool-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'datatool-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'datatool-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'datatool-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'datatool-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'datatool-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'datatool-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'datatool-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'datatool-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'datatool-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'datatool-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'datatool-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'datatool-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'datatool-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'datatool-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'datatool-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'datatool-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'datatool-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'datatool-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'datatool-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'datatool-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'datatool-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'datatool-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'datatool-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'datatool-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'datatool-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'datatool-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'datatool-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'datatool-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'datatool-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'datatool-cmd', + score: 0.0063276692758974925, + }, + ], + fmtcount: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'fmtcount-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'fmtcount-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'fmtcount-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'fmtcount-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'fmtcount-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'fmtcount-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'fmtcount-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'fmtcount-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'fmtcount-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'fmtcount-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\robustify{}', + snippet: '\\robustify{$1}', + meta: 'fmtcount-cmd', + score: 0.002671974990314091, + }, + { + caption: '\\setbool{}{}', + snippet: '\\setbool{$1}{$2}', + meta: 'fmtcount-cmd', + score: 0.00023171033119130004, + }, + { + caption: '\\ifdefempty{}{}{}', + snippet: '\\ifdefempty{$1}{$2}{$3}', + meta: 'fmtcount-cmd', + score: 7.482069221111606e-5, + }, + { + caption: '\\apptocmd{}{}{}{}', + snippet: '\\apptocmd{$1}{$2}{$3}{$4}', + meta: 'fmtcount-cmd', + score: 0.00035805058319299113, + }, + { + caption: '\\ifstrequal{}{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}{$4}', + meta: 'fmtcount-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\ifstrequal{}{}{}', + snippet: '\\ifstrequal{$1}{$2}{$3}', + meta: 'fmtcount-cmd', + score: 0.00041307691354437894, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'fmtcount-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\csedef{}{}', + snippet: '\\csedef{$1}{$2}', + meta: 'fmtcount-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'fmtcount-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\newrobustcmd{}[]{}', + snippet: '\\newrobustcmd{$1}[$2]{$3}', + meta: 'fmtcount-cmd', + score: 0.0006607703576475988, + }, + { + caption: '\\ifdefstring{}{}{}{}', + snippet: '\\ifdefstring{$1}{$2}{$3}{$4}', + meta: 'fmtcount-cmd', + score: 0.0006796212875843042, + }, + { + caption: '\\ifbool{}{}{}', + snippet: '\\ifbool{$1}{$2}{$3}', + meta: 'fmtcount-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\patchcmd{}{}{}{}{}', + snippet: '\\patchcmd{$1}{$2}{$3}{$4}{$5}', + meta: 'fmtcount-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\patchcmd', + snippet: '\\patchcmd', + meta: 'fmtcount-cmd', + score: 0.002560998917940627, + }, + { + caption: '\\preto{}{}', + snippet: '\\preto{$1}{$2}', + meta: 'fmtcount-cmd', + score: 8.860754525300578e-5, + }, + { + caption: '\\ifnumcomp{}{}{}{}{}', + snippet: '\\ifnumcomp{$1}{$2}{$3}{$4}{$5}', + meta: 'fmtcount-cmd', + score: 0.00029867998381154486, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'fmtcount-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\newbool{}', + snippet: '\\newbool{$1}', + meta: 'fmtcount-cmd', + score: 7.723677706376668e-5, + }, + { + caption: '\\AtBeginEnvironment{}{}', + snippet: '\\AtBeginEnvironment{$1}{$2}', + meta: 'fmtcount-cmd', + score: 4.002553629215439e-5, + }, + { + caption: '\\pretocmd{}{}{}{}', + snippet: '\\pretocmd{$1}{$2}{$3}{$4}', + meta: 'fmtcount-cmd', + score: 0.00028992557275763024, + }, + { + caption: '\\ifundef{}{}{}', + snippet: '\\ifundef{$1}{$2}{$3}', + meta: 'fmtcount-cmd', + score: 0.00014933999190577243, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'fmtcount-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'fmtcount-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\frenchspacing', + snippet: '\\frenchspacing', + meta: 'fmtcount-cmd', + score: 0.0063276692758974925, + }, + ], + aurl: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'aurl-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'aurl-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'aurl-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\AtBeginShipout{}', + snippet: '\\AtBeginShipout{$1}', + meta: 'aurl-cmd', + score: 0.00047530324346933345, + }, + { + caption: '\\AtBeginShipoutNext{}', + snippet: '\\AtBeginShipoutNext{$1}', + meta: 'aurl-cmd', + score: 0.0005277905480209891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\UrlBreaks{}', + snippet: '\\UrlBreaks{$1}', + meta: 'aurl-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\UrlBreaks', + snippet: '\\UrlBreaks', + meta: 'aurl-cmd', + score: 0.001030592515645366, + }, + { + caption: '\\Url', + snippet: '\\Url', + meta: 'aurl-cmd', + score: 0.0002854206807593436, + }, + { + caption: '\\UrlOrds{}', + snippet: '\\UrlOrds{$1}', + meta: 'aurl-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\UrlOrds', + snippet: '\\UrlOrds', + meta: 'aurl-cmd', + score: 0.0006882563723629154, + }, + { + caption: '\\urlstyle{}', + snippet: '\\urlstyle{$1}', + meta: 'aurl-cmd', + score: 0.010515056688180681, + }, + { + caption: '\\urldef{}', + snippet: '\\urldef{$1}', + meta: 'aurl-cmd', + score: 0.008041789461944983, + }, + { + caption: '\\UrlBigBreaks{}', + snippet: '\\UrlBigBreaks{$1}', + meta: 'aurl-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlFont{}', + snippet: '\\UrlFont{$1}', + meta: 'aurl-cmd', + score: 0.0032990580087398644, + }, + { + caption: '\\UrlSpecials{}', + snippet: '\\UrlSpecials{$1}', + meta: 'aurl-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\UrlNoBreaks', + snippet: '\\UrlNoBreaks', + meta: 'aurl-cmd', + score: 3.7048287721105874e-5, + }, + { + caption: '\\nameref{}', + snippet: '\\nameref{$1}', + meta: 'aurl-cmd', + score: 0.009472569279662113, + }, + { + caption: '\\pdfbookmark[]{}{}', + snippet: '\\pdfbookmark[$1]{$2}{$3}', + meta: 'aurl-cmd', + score: 0.006492248863367502, + }, + { + caption: '\\figureautorefname', + snippet: '\\figureautorefname', + meta: 'aurl-cmd', + score: 0.00014582556188448738, + }, + { + caption: '\\figureautorefname{}', + snippet: '\\figureautorefname{$1}', + meta: 'aurl-cmd', + score: 0.00014582556188448738, + }, + { + caption: '\\numberwithin{}{}', + snippet: '\\numberwithin{$1}{$2}', + meta: 'aurl-cmd', + score: 0.006963729684667191, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'aurl-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'aurl-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\footnoteautorefname', + snippet: '\\footnoteautorefname', + meta: 'aurl-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\roman{}', + snippet: '\\roman{$1}', + meta: 'aurl-cmd', + score: 0.005553384455935491, + }, + { + caption: '\\roman', + snippet: '\\roman', + meta: 'aurl-cmd', + score: 0.005553384455935491, + }, + { + caption: '\\string', + snippet: '\\string', + meta: 'aurl-cmd', + score: 0.001042697111754002, + }, + { + caption: '\\MakeLowercase{}', + snippet: '\\MakeLowercase{$1}', + meta: 'aurl-cmd', + score: 0.017289599800633146, + }, + { + caption: '\\textunderscore', + snippet: '\\textunderscore', + meta: 'aurl-cmd', + score: 0.001509072212764015, + }, + { + caption: '\\do', + snippet: '\\do', + meta: 'aurl-cmd', + score: 0.009278344180101056, + }, + { + caption: '\\begin{}', + snippet: '\\begin{$1}', + meta: 'aurl-cmd', + score: 7.849662248028187, + }, + { + caption: '\\begin{}[]', + snippet: '\\begin{$1}[$2]', + meta: 'aurl-cmd', + score: 7.849662248028187, + }, + { + caption: '\\begin{}{}', + snippet: '\\begin{$1}{$2}', + meta: 'aurl-cmd', + score: 7.849662248028187, + }, + { + caption: '\\FancyVerbLineautorefname', + snippet: '\\FancyVerbLineautorefname', + meta: 'aurl-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\hyperlink{}{}', + snippet: '\\hyperlink{$1}{$2}', + meta: 'aurl-cmd', + score: 0.00978652043902115, + }, + { + caption: '\\tableautorefname', + snippet: '\\tableautorefname', + meta: 'aurl-cmd', + score: 0.00012704528567339081, + }, + { + caption: '\\tableautorefname{}', + snippet: '\\tableautorefname{$1}', + meta: 'aurl-cmd', + score: 0.00012704528567339081, + }, + { + caption: '\\equationautorefname', + snippet: '\\equationautorefname', + meta: 'aurl-cmd', + score: 0.00018777198999871106, + }, + { + caption: '\\equationautorefname{}', + snippet: '\\equationautorefname{$1}', + meta: 'aurl-cmd', + score: 0.00018777198999871106, + }, + { + caption: '\\chapterautorefname', + snippet: '\\chapterautorefname', + meta: 'aurl-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\TeX', + snippet: '\\TeX', + meta: 'aurl-cmd', + score: 0.02873756018238537, + }, + { + caption: '\\TeX{}', + snippet: '\\TeX{$1}', + meta: 'aurl-cmd', + score: 0.02873756018238537, + }, + { + caption: '\\protect', + snippet: '\\protect', + meta: 'aurl-cmd', + score: 0.0200686676229443, + }, + { + caption: '\\appendixautorefname', + snippet: '\\appendixautorefname', + meta: 'aurl-cmd', + score: 7.950698053641679e-5, + }, + { + caption: '\\appendixautorefname{}', + snippet: '\\appendixautorefname{$1}', + meta: 'aurl-cmd', + score: 7.950698053641679e-5, + }, + { + caption: '\\newlabel{}{}', + snippet: '\\newlabel{$1}{$2}', + meta: 'aurl-cmd', + score: 0.00029737672328168955, + }, + { + caption: '\\texorpdfstring{}{}', + snippet: '\\texorpdfstring{$1}{$2}', + meta: 'aurl-cmd', + score: 0.0073781967296121, + }, + { + caption: '\\refstepcounter{}', + snippet: '\\refstepcounter{$1}', + meta: 'aurl-cmd', + score: 0.002140559856649122, + }, + { + caption: '\\alph', + snippet: '\\alph', + meta: 'aurl-cmd', + score: 0.01034327266194849, + }, + { + caption: '\\alph{}', + snippet: '\\alph{$1}', + meta: 'aurl-cmd', + score: 0.01034327266194849, + }, + { + caption: '\\pageref{}', + snippet: '\\pageref{$1}', + meta: 'aurl-cmd', + score: 0.019788865471151957, + }, + { + caption: '\\item', + snippet: '\\item', + meta: 'aurl-cmd', + score: 3.800886892251021, + }, + { + caption: '\\item[]', + snippet: '\\item[$1]', + meta: 'aurl-cmd', + score: 3.800886892251021, + }, + { + caption: '\\LaTeX', + snippet: '\\LaTeX', + meta: 'aurl-cmd', + score: 0.2334089308452787, + }, + { + caption: '\\LaTeX{}', + snippet: '\\LaTeX{$1}', + meta: 'aurl-cmd', + score: 0.2334089308452787, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\itemautorefname', + snippet: '\\itemautorefname', + meta: 'aurl-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\caption{}', + snippet: '\\caption{$1}', + meta: 'aurl-cmd', + score: 1.2569477427490174, + }, + { + caption: '\\sectionautorefname', + snippet: '\\sectionautorefname', + meta: 'aurl-cmd', + score: 0.0019832324299155183, + }, + { + caption: '\\sectionautorefname{}', + snippet: '\\sectionautorefname{$1}', + meta: 'aurl-cmd', + score: 0.0019832324299155183, + }, + { + caption: '\\LaTeXe', + snippet: '\\LaTeXe', + meta: 'aurl-cmd', + score: 0.007928096378157487, + }, + { + caption: '\\LaTeXe{}', + snippet: '\\LaTeXe{$1}', + meta: 'aurl-cmd', + score: 0.007928096378157487, + }, + { + caption: '\\footref{}', + snippet: '\\footref{$1}', + meta: 'aurl-cmd', + score: 0.0003680857021151614, + }, + { + caption: '\\footref', + snippet: '\\footref', + meta: 'aurl-cmd', + score: 0.0003680857021151614, + }, + { + caption: '\\hypertarget{}{}', + snippet: '\\hypertarget{$1}{$2}', + meta: 'aurl-cmd', + score: 0.009652820108904094, + }, + { + caption: '\\theoremautorefname', + snippet: '\\theoremautorefname', + meta: 'aurl-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\maketitle', + snippet: '\\maketitle', + meta: 'aurl-cmd', + score: 0.7504160124360846, + }, + { + caption: '\\subparagraphautorefname', + snippet: '\\subparagraphautorefname', + meta: 'aurl-cmd', + score: 0.0005446476945175932, + }, + { + caption: '\\url{}', + snippet: '\\url{$1}', + meta: 'aurl-cmd', + score: 0.13586474005868793, + }, + { + caption: '\\author{}', + snippet: '\\author{$1}', + meta: 'aurl-cmd', + score: 0.8973590434087177, + }, + { + caption: '\\author[]{}', + snippet: '\\author[$1]{$2}', + meta: 'aurl-cmd', + score: 0.8973590434087177, + }, + { + caption: '\\href{}{}', + snippet: '\\href{$1}{$2}', + meta: 'aurl-cmd', + score: 0.27111130260612365, + }, + { + caption: '\\Roman{}', + snippet: '\\Roman{$1}', + meta: 'aurl-cmd', + score: 0.0038703587462843594, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'aurl-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\autoref{}', + snippet: '\\autoref{$1}', + meta: 'aurl-cmd', + score: 0.03741172773691362, + }, + { + caption: '\\nolinkurl{}', + snippet: '\\nolinkurl{$1}', + meta: 'aurl-cmd', + score: 0.0004995635515943437, + }, + { + caption: '\\end{}', + snippet: '\\end{$1}', + meta: 'aurl-cmd', + score: 7.847906405228455, + }, + { + caption: '\\phantomsection', + snippet: '\\phantomsection', + meta: 'aurl-cmd', + score: 0.0174633138331273, + }, + { + caption: '\\MakeUppercase{}', + snippet: '\\MakeUppercase{$1}', + meta: 'aurl-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\MakeUppercase', + snippet: '\\MakeUppercase', + meta: 'aurl-cmd', + score: 0.006776001543888959, + }, + { + caption: '\\partautorefname', + snippet: '\\partautorefname', + meta: 'aurl-cmd', + score: 1.8780276211096543e-5, + }, + { + caption: '\\Itemautorefname{}', + snippet: '\\Itemautorefname{$1}', + meta: 'aurl-cmd', + score: 6.006262128895586e-5, + }, + { + caption: '\\halign{}', + snippet: '\\halign{$1}', + meta: 'aurl-cmd', + score: 0.00017906650306643613, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'aurl-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\ref{}', + snippet: '\\ref{$1}', + meta: 'aurl-cmd', + score: 1.4380093454211778, + }, + { + caption: '\\Alph{}', + snippet: '\\Alph{$1}', + meta: 'aurl-cmd', + score: 0.002233258780143355, + }, + { + caption: '\\Alph', + snippet: '\\Alph', + meta: 'aurl-cmd', + score: 0.002233258780143355, + }, + { + caption: '\\appendix', + snippet: '\\appendix', + meta: 'aurl-cmd', + score: 0.047007158741781095, + }, + { + caption: '\\MP', + snippet: '\\MP', + meta: 'aurl-cmd', + score: 0.00018344383742255004, + }, + { + caption: '\\MP{}', + snippet: '\\MP{$1}', + meta: 'aurl-cmd', + score: 0.00018344383742255004, + }, + { + caption: '\\paragraphautorefname', + snippet: '\\paragraphautorefname', + meta: 'aurl-cmd', + score: 0.0005446476945175932, + }, + { + caption: '\\citeN{}', + snippet: '\\citeN{$1}', + meta: 'aurl-cmd', + score: 0.0018503938529945614, + }, + { + caption: '\\citeN', + snippet: '\\citeN', + meta: 'aurl-cmd', + score: 0.0018503938529945614, + }, + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'aurl-cmd', + score: 0.07503475348393239, + }, + { + caption: '\\subsectionautorefname', + snippet: '\\subsectionautorefname', + meta: 'aurl-cmd', + score: 0.0012546605780895737, + }, + { + caption: '\\subsectionautorefname{}', + snippet: '\\subsectionautorefname{$1}', + meta: 'aurl-cmd', + score: 0.0012546605780895737, + }, + { + caption: '\\hyperref[]{}', + snippet: '\\hyperref[$1]{$2}', + meta: 'aurl-cmd', + score: 0.004515152477030062, + }, + { + caption: '\\arabic{}', + snippet: '\\arabic{$1}', + meta: 'aurl-cmd', + score: 0.02445837629741638, + }, + { + caption: '\\arabic', + snippet: '\\arabic', + meta: 'aurl-cmd', + score: 0.02445837629741638, + }, + { + caption: '\\newline', + snippet: '\\newline', + meta: 'aurl-cmd', + score: 0.3311721696201715, + }, + { + caption: '\\hypersetup{}', + snippet: '\\hypersetup{$1}', + meta: 'aurl-cmd', + score: 0.06967310843464661, + }, + { + caption: '\\subsubsectionautorefname', + snippet: '\\subsubsectionautorefname', + meta: 'aurl-cmd', + score: 0.0012064581899162352, + }, + { + caption: '\\subsubsectionautorefname{}', + snippet: '\\subsubsectionautorefname{$1}', + meta: 'aurl-cmd', + score: 0.0012064581899162352, + }, + { + caption: '\\title{}', + snippet: '\\title{$1}', + meta: 'aurl-cmd', + score: 0.9202908262245683, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'aurl-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'aurl-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'aurl-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'aurl-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'aurl-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'aurl-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'aurl-cmd', + score: 0.0058342578961340175, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'aurl-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'aurl-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'aurl-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'aurl-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'aurl-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'aurl-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'aurl-cmd', + score: 0.00530510025314411, + }, + ], + bchart: [ + { + caption: '\\setkeys{}{}', + snippet: '\\setkeys{$1}{$2}', + meta: 'bchart-cmd', + score: 0.00037306820619479756, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bchart-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bchart-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bchart-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\scalebox{}{}', + snippet: '\\scalebox{$1}{$2}', + meta: 'bchart-cmd', + score: 0.015973401906548487, + }, + { + caption: '\\reflectbox{}', + snippet: '\\reflectbox{$1}', + meta: 'bchart-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\reflectbox', + snippet: '\\reflectbox', + meta: 'bchart-cmd', + score: 0.0005981923692899367, + }, + { + caption: '\\resizebox{}{}{}', + snippet: '\\resizebox{$1}{$2}{$3}', + meta: 'bchart-cmd', + score: 0.017834153815870245, + }, + { + caption: '\\includegraphics[]{}', + snippet: '\\includegraphics[$1]{$2}', + meta: 'bchart-cmd', + score: 1.4595731795525781, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bchart-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\DeclareGraphicsExtensions{}', + snippet: '\\DeclareGraphicsExtensions{$1}', + meta: 'bchart-cmd', + score: 0.0055519509468004175, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bchart-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\graphicspath{}', + snippet: '\\graphicspath{$1}', + meta: 'bchart-cmd', + score: 0.09973951908678011, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'bchart-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'bchart-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'bchart-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\DeclareGraphicsRule{}{}{}{}', + snippet: '\\DeclareGraphicsRule{$1}{$2}{$3}{$4}', + meta: 'bchart-cmd', + score: 0.004649150613625593, + }, + { + caption: '\\ifthenelse{}{}{}', + snippet: '\\ifthenelse{$1}{$2}{$3}', + meta: 'bchart-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\ifthenelse{}', + snippet: '\\ifthenelse{$1}', + meta: 'bchart-cmd', + score: 0.009331077109224957, + }, + { + caption: '\\setboolean{}{}', + snippet: '\\setboolean{$1}{$2}', + meta: 'bchart-cmd', + score: 0.0012203054938872515, + }, + { + caption: '\\newboolean{}', + snippet: '\\newboolean{$1}', + meta: 'bchart-cmd', + score: 0.0009170966832172938, + }, + { + caption: '\\value{}', + snippet: '\\value{$1}', + meta: 'bchart-cmd', + score: 0.01590723355124104, + }, + { + caption: '\\boolean{}', + snippet: '\\boolean{$1}', + meta: 'bchart-cmd', + score: 0.0018957469739775527, + }, + { + caption: '\\rotatebox{}{}', + snippet: '\\rotatebox{$1}{$2}', + meta: 'bchart-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox[]{}{}', + snippet: '\\rotatebox[$1]{$2}{$3}', + meta: 'bchart-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\rotatebox{}', + snippet: '\\rotatebox{$1}', + meta: 'bchart-cmd', + score: 0.004719094298848707, + }, + { + caption: '\\definecolors{}', + snippet: '\\definecolors{$1}', + meta: 'bchart-cmd', + score: 0.0003209840085766927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'bchart-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'bchart-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\fcolorbox{}{}{}', + snippet: '\\fcolorbox{$1}{$2}{$3}', + meta: 'bchart-cmd', + score: 0.00926923425734719, + }, + { + caption: '\\colorlet{}{}', + snippet: '\\colorlet{$1}{$2}', + meta: 'bchart-cmd', + score: 0.03654388342026623, + }, + { + caption: '\\textcolor{}{}', + snippet: '\\textcolor{$1}{$2}', + meta: 'bchart-cmd', + score: 0.20852115286477566, + }, + { + caption: '\\selectcolormodel{}', + snippet: '\\selectcolormodel{$1}', + meta: 'bchart-cmd', + score: 0.000264339771769041, + }, + { + caption: '\\rowcolors{}{}{}', + snippet: '\\rowcolors{$1}{$2}{$3}', + meta: 'bchart-cmd', + score: 0.0014120076489723356, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'bchart-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\pagecolor{}', + snippet: '\\pagecolor{$1}', + meta: 'bchart-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\pagecolor{}{}', + snippet: '\\pagecolor{$1}{$2}', + meta: 'bchart-cmd', + score: 0.0008147200475678891, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bchart-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\definecolor{}{}{}', + snippet: '\\definecolor{$1}{$2}{$3}', + meta: 'bchart-cmd', + score: 0.16906710888680052, + }, + { + caption: '\\colorbox{}{}', + snippet: '\\colorbox{$1}{$2}', + meta: 'bchart-cmd', + score: 0.029302172361548254, + }, + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'bchart-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'bchart-cmd', + score: 0.2864294797053033, + }, + ], + pdftexcmds: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pdftexcmds-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pdftexcmds-cmd', + score: 0.002958865219480927, + }, + ], + l3keys2e: [ + { + caption: '\\color[]{}', + snippet: '\\color[$1]{$2}', + meta: 'l3keys2e-cmd', + score: 0.2864294797053033, + }, + { + caption: '\\color{}', + snippet: '\\color{$1}', + meta: 'l3keys2e-cmd', + score: 0.2864294797053033, + }, + ], + xfor: [ + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'xfor-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'xfor-cmd', + score: 0.021170869458413965, + }, + ], + accsupp: [ + { + caption: '\\RequireXeTeX', + snippet: '\\RequireXeTeX', + meta: 'accsupp-cmd', + score: 0.00021116765384691477, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'accsupp-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'accsupp-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'accsupp-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'accsupp-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'accsupp-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'accsupp-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'accsupp-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'accsupp-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'accsupp-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'accsupp-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'accsupp-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'accsupp-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'accsupp-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'accsupp-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'accsupp-cmd', + score: 0.021170869458413965, + }, + ], + trig: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'trig-cmd', + score: 0.008565354665444157, + }, + ], + rerunfilecheck: [ + { + caption: '\\makeindex', + snippet: '\\makeindex', + meta: 'rerunfilecheck-cmd', + score: 0.010304996748556729, + }, + { + caption: '\\index{}', + snippet: '\\index{$1}', + meta: 'rerunfilecheck-cmd', + score: 0.013774721817648336, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rerunfilecheck-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'rerunfilecheck-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\clearpage', + snippet: '\\clearpage', + meta: 'rerunfilecheck-cmd', + score: 0.1789117552185788, + }, + { + caption: '\\global', + snippet: '\\global', + meta: 'rerunfilecheck-cmd', + score: 0.006609629561859019, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'rerunfilecheck-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'rerunfilecheck-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rerunfilecheck-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'rerunfilecheck-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'rerunfilecheck-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rerunfilecheck-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'rerunfilecheck-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'rerunfilecheck-cmd', + score: 0.0058342578961340175, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'rerunfilecheck-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rerunfilecheck-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'rerunfilecheck-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'rerunfilecheck-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'rerunfilecheck-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'rerunfilecheck-cmd', + score: 0.021170869458413965, + }, + ], + pdfescape: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'pdfescape-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'pdfescape-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'pdfescape-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'pdfescape-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'pdfescape-cmd', + score: 0.008565354665444157, + }, + ], + infwarerr: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'infwarerr-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\check{}', + snippet: '\\check{$1}', + meta: 'infwarerr-cmd', + score: 0.0058342578961340175, + }, + { + caption: '\\space', + snippet: '\\space', + meta: 'infwarerr-cmd', + score: 0.023010789853665694, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'infwarerr-cmd', + score: 0.008565354665444157, + }, + ], + kvsetkeys: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'kvsetkeys-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'kvsetkeys-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'kvsetkeys-cmd', + score: 0.008565354665444157, + }, + ], + gettitlestring: [ + { + caption: '\\addcontentsline{}{}{}', + snippet: '\\addcontentsline{$1}{$2}{$3}', + meta: 'gettitlestring-cmd', + score: 0.07503475348393239, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'gettitlestring-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'gettitlestring-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'gettitlestring-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'gettitlestring-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'gettitlestring-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'gettitlestring-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'gettitlestring-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'gettitlestring-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\expandafter', + snippet: '\\expandafter', + meta: 'gettitlestring-cmd', + score: 0.021170869458413965, + }, + { + caption: '\\expandafter{}', + snippet: '\\expandafter{$1}', + meta: 'gettitlestring-cmd', + score: 0.021170869458413965, + }, + ], + refcount: [ + { + caption: '\\thepage', + snippet: '\\thepage', + meta: 'refcount-cmd', + score: 0.0591555998103519, + }, + ], + bitset: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'bitset-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'bitset-cmd', + score: 0.008565354665444157, + }, + ], + etexcmds: [ + { + caption: '\\csname', + snippet: '\\csname', + meta: 'etexcmds-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\empty', + snippet: '\\empty', + meta: 'etexcmds-cmd', + score: 0.002958865219480927, + }, + ], + intcalc: [ + { + caption: '\\empty', + snippet: '\\empty', + meta: 'intcalc-cmd', + score: 0.002958865219480927, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'intcalc-cmd', + score: 0.008565354665444157, + }, + ], + hycolor: [ + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hycolor-cmd', + score: 0.00530510025314411, + }, + { + caption: '\\csname', + snippet: '\\csname', + meta: 'hycolor-cmd', + score: 0.008565354665444157, + }, + { + caption: '\\noexpand', + snippet: '\\noexpand', + meta: 'hycolor-cmd', + score: 0.00530510025314411, + }, + ], +} diff --git a/services/web/app/src/Features/Newsletter/NewsletterManager.js b/services/web/app/src/Features/Newsletter/NewsletterManager.js new file mode 100644 index 0000000000..e05a02bb86 --- /dev/null +++ b/services/web/app/src/Features/Newsletter/NewsletterManager.js @@ -0,0 +1,222 @@ +const { callbackify } = require('util') +const logger = require('logger-sharelatex') +const Settings = require('@overleaf/settings') +const crypto = require('crypto') +const Mailchimp = require('mailchimp-api-v3') +const OError = require('@overleaf/o-error') + +const provider = getProvider() + +module.exports = { + subscribe: callbackify(provider.subscribe), + unsubscribe: callbackify(provider.unsubscribe), + changeEmail: callbackify(provider.changeEmail), + promises: provider, +} + +class NonFatalEmailUpdateError extends OError { + constructor(message, oldEmail, newEmail) { + super(message, { oldEmail, newEmail }) + } +} + +function getProvider() { + if (mailchimpIsConfigured()) { + logger.info('Using newsletter provider: mailchimp') + return makeMailchimpProvider() + } else { + logger.info('Using newsletter provider: none') + return makeNullProvider() + } +} + +function mailchimpIsConfigured() { + return Settings.mailchimp != null && Settings.mailchimp.api_key != null +} + +function makeMailchimpProvider() { + const mailchimp = new Mailchimp(Settings.mailchimp.api_key) + const MAILCHIMP_LIST_ID = Settings.mailchimp.list_id + + return { + subscribe, + unsubscribe, + changeEmail, + } + + async function subscribe(user) { + try { + const path = getSubscriberPath(user.email) + await mailchimp.put(path, { + email_address: user.email, + status: 'subscribed', + status_if_new: 'subscribed', + merge_fields: getMergeFields(user), + }) + logger.info({ user }, 'finished subscribing user to newsletter') + } catch (err) { + throw OError.tag(err, 'error subscribing user to newsletter', { + userId: user._id, + }) + } + } + + async function unsubscribe(user, options = {}) { + try { + const path = getSubscriberPath(user.email) + if (options.delete) { + await mailchimp.delete(path) + } else { + await mailchimp.patch(path, { + status: 'unsubscribed', + merge_fields: getMergeFields(user), + }) + } + logger.info( + { user, options }, + 'finished unsubscribing user from newsletter' + ) + } catch (err) { + if (err.status === 404 || err.status === 405) { + // silently ignore users who were never subscribed (404) or previously deleted (405) + return + } + + if (err.message.includes('looks fake or invalid')) { + logger.info( + { err, user, options }, + 'Mailchimp declined to unsubscribe user because it finds the email looks fake' + ) + return + } + + throw OError.tag(err, 'error unsubscribing user from newsletter', { + userId: user._id, + }) + } + } + + async function changeEmail(user, newEmail) { + const oldEmail = user.email + + try { + await updateEmailInMailchimp(user, newEmail) + } catch (updateError) { + // if we failed to update the user, delete their old email address so that + // we don't leave it stuck in mailchimp + logger.info( + { oldEmail, newEmail, updateError }, + 'unable to change email in newsletter, removing old mail' + ) + + try { + await unsubscribe(user, { delete: true }) + } catch (unsubscribeError) { + // something went wrong removing the user's address + throw OError.tag( + unsubscribeError, + 'error unsubscribing old email in response to email change failure', + { oldEmail, newEmail, updateError } + ) + } + + if (!(updateError instanceof NonFatalEmailUpdateError)) { + throw updateError + } + } + } + + async function updateEmailInMailchimp(user, newEmail) { + const oldEmail = user.email + + // mailchimp doesn't give us error codes, so we have to parse the message :'( + const errors = { + 'merge fields were invalid': 'user has never subscribed', + 'could not be validated': + 'user has previously unsubscribed or new email already exist on list', + 'is already a list member': 'new email is already on mailing list', + 'looks fake or invalid': 'mail looks fake to mailchimp', + } + + try { + const path = getSubscriberPath(oldEmail) + await mailchimp.patch(path, { + email_address: newEmail, + merge_fields: getMergeFields(user), + }) + logger.info('finished changing email in the newsletter') + } catch (err) { + // silently ignore users who were never subscribed + if (err.status === 404) { + return + } + + // look through expected mailchimp errors and log if we find one + Object.keys(errors).forEach(key => { + if (err.message.includes(key)) { + const message = `unable to change email in newsletter, ${errors[key]}` + + logger.info({ oldEmail, newEmail }, message) + + throw new NonFatalEmailUpdateError( + message, + oldEmail, + newEmail + ).withCause(err) + } + }) + + // if we didn't find an expected error, generate something to throw + throw OError.tag(err, 'error changing email in newsletter', { + oldEmail, + newEmail, + }) + } + } + + function getSubscriberPath(email) { + const emailHash = hashEmail(email) + return `/lists/${MAILCHIMP_LIST_ID}/members/${emailHash}` + } + + function hashEmail(email) { + return crypto.createHash('md5').update(email.toLowerCase()).digest('hex') + } + + function getMergeFields(user) { + return { + FNAME: user.first_name, + LNAME: user.last_name, + MONGO_ID: user._id.toString(), + } + } +} + +function makeNullProvider() { + return { + subscribe, + unsubscribe, + changeEmail, + } + + async function subscribe(user) { + logger.info( + { user }, + 'Not subscribing user to newsletter because no newsletter provider is configured' + ) + } + + async function unsubscribe(user) { + logger.info( + { user }, + 'Not unsubscribing user from newsletter because no newsletter provider is configured' + ) + } + + async function changeEmail(oldEmail, newEmail) { + logger.info( + { oldEmail, newEmail }, + 'Not changing email in newsletter for user because no newsletter provider is configured' + ) + } +} diff --git a/services/web/app/src/Features/Notifications/NotificationsBuilder.js b/services/web/app/src/Features/Notifications/NotificationsBuilder.js new file mode 100644 index 0000000000..f6c6000b71 --- /dev/null +++ b/services/web/app/src/Features/Notifications/NotificationsBuilder.js @@ -0,0 +1,247 @@ +const NotificationsHandler = require('./NotificationsHandler') +const { promisifyAll } = require('../../util/promises') +const request = require('request') +const settings = require('@overleaf/settings') + +function dropboxDuplicateProjectNames(userId) { + return { + key: `dropboxDuplicateProjectNames-${userId}`, + create(projectName, callback) { + if (callback == null) { + callback = function () {} + } + NotificationsHandler.createNotification( + userId, + this.key, + 'notification_dropbox_duplicate_project_names', + { projectName }, + null, + true, + callback + ) + }, + read(callback) { + if (callback == null) { + callback = function () {} + } + NotificationsHandler.markAsReadWithKey(userId, this.key, callback) + }, + } +} + +function dropboxUnlinkedDueToLapsedReconfirmation(userId) { + return { + key: 'drobox-unlinked-due-to-lapsed-reconfirmation', + create(callback) { + NotificationsHandler.createNotification( + userId, + this.key, + 'notification_dropbox_unlinked_due_to_lapsed_reconfirmation', + {}, + null, + true, + callback + ) + }, + read(callback) { + NotificationsHandler.markAsReadWithKey(userId, this.key, callback) + }, + } +} + +function featuresUpgradedByAffiliation(affiliation, user) { + return { + key: `features-updated-by=${affiliation.institutionId}`, + create(callback) { + if (callback == null) { + callback = function () {} + } + const messageOpts = { institutionName: affiliation.institutionName } + NotificationsHandler.createNotification( + user._id, + this.key, + 'notification_features_upgraded_by_affiliation', + messageOpts, + null, + false, + callback + ) + }, + read(callback) { + if (callback == null) { + callback = function () {} + } + NotificationsHandler.markAsReadWithKey(user._id, this.key, callback) + }, + } +} + +function redundantPersonalSubscription(affiliation, user) { + return { + key: `redundant-personal-subscription-${affiliation.institutionId}`, + create(callback) { + if (callback == null) { + callback = function () {} + } + const messageOpts = { institutionName: affiliation.institutionName } + NotificationsHandler.createNotification( + user._id, + this.key, + 'notification_personal_subscription_not_required_due_to_affiliation', + messageOpts, + null, + false, + callback + ) + }, + read(callback) { + if (callback == null) { + callback = function () {} + } + NotificationsHandler.markAsReadWithKey(user._id, this.key, callback) + }, + } +} + +function projectInvite(invite, project, sendingUser, user) { + return { + key: `project-invite-${invite._id}`, + create(callback) { + if (callback == null) { + callback = function () {} + } + const messageOpts = { + userName: sendingUser.first_name, + projectName: project.name, + projectId: project._id.toString(), + token: invite.token, + } + NotificationsHandler.createNotification( + user._id, + this.key, + 'notification_project_invite', + messageOpts, + invite.expires, + callback + ) + }, + read(callback) { + if (callback == null) { + callback = function () {} + } + NotificationsHandler.markAsReadByKeyOnly(this.key, callback) + }, + } +} + +function ipMatcherAffiliation(userId) { + return { + create(ip, callback) { + if (callback == null) { + callback = function () {} + } + if (!settings.apis.v1.url) { + // service is not configured + return callback() + } + request( + { + method: 'GET', + url: `${settings.apis.v1.url}/api/v2/users/${userId}/ip_matcher`, + auth: { user: settings.apis.v1.user, pass: settings.apis.v1.pass }, + body: { ip }, + json: true, + timeout: 20 * 1000, + }, + function (error, response, body) { + if (error != null) { + return callback(error) + } + if (response.statusCode !== 200) { + return callback() + } + + const key = `ip-matched-affiliation-${body.id}` + const portalPath = body.portal_slug + ? `/${body.is_university ? 'edu' : 'org'}/${body.portal_slug}` + : undefined + const messageOpts = { + university_name: body.name, + institutionId: body.id, + content: body.enrolment_ad_html, + portalPath, + ssoEnabled: body.sso_enabled, + } + NotificationsHandler.createNotification( + userId, + key, + 'notification_ip_matched_affiliation', + messageOpts, + null, + false, + callback + ) + } + ) + }, + + read(universityId, callback) { + if (callback == null) { + callback = function () {} + } + const key = `ip-matched-affiliation-${universityId}` + NotificationsHandler.markAsReadWithKey(userId, key, callback) + }, + } +} + +function tpdsFileLimit(userId) { + return { + key: `tpdsFileLimit-${userId}`, + create(projectName, callback) { + if (callback == null) { + callback = function () {} + } + const messageOpts = { + projectName: projectName, + } + NotificationsHandler.createNotification( + userId, + this.key, + 'notification_tpds_file_limit', + messageOpts, + null, + true, + callback + ) + }, + read(callback) { + if (callback == null) { + callback = function () {} + } + NotificationsHandler.markAsReadByKeyOnly(this.key, callback) + }, + } +} + +const NotificationsBuilder = { + // Note: notification keys should be url-safe + dropboxUnlinkedDueToLapsedReconfirmation, + dropboxDuplicateProjectNames, + featuresUpgradedByAffiliation, + redundantPersonalSubscription, + projectInvite, + ipMatcherAffiliation, + tpdsFileLimit, +} + +NotificationsBuilder.promises = { + dropboxUnlinkedDueToLapsedReconfirmation: function (userId) { + return promisifyAll(dropboxUnlinkedDueToLapsedReconfirmation(userId)) + }, + redundantPersonalSubscription: function (affiliation, user) { + return promisifyAll(redundantPersonalSubscription(affiliation, user)) + }, +} + +module.exports = NotificationsBuilder diff --git a/services/web/app/src/Features/Notifications/NotificationsController.js b/services/web/app/src/Features/Notifications/NotificationsController.js new file mode 100644 index 0000000000..50c43c150e --- /dev/null +++ b/services/web/app/src/Features/Notifications/NotificationsController.js @@ -0,0 +1,33 @@ +const NotificationsHandler = require('./NotificationsHandler') +const SessionManager = require('../Authentication/SessionManager') +const _ = require('underscore') + +module.exports = { + getAllUnreadNotifications(req, res) { + const userId = SessionManager.getLoggedInUserId(req.session) + NotificationsHandler.getUserNotifications( + userId, + function (err, unreadNotifications) { + unreadNotifications = _.map( + unreadNotifications, + function (notification) { + notification.html = req.i18n.translate( + notification.templateKey, + notification.messageOpts + ) + return notification + } + ) + res.send(unreadNotifications) + } + ) + }, + + markNotificationAsRead(req, res) { + const userId = SessionManager.getLoggedInUserId(req.session) + const { notificationId } = req.params + NotificationsHandler.markAsRead(userId, notificationId, () => + res.sendStatus(200) + ) + }, +} diff --git a/services/web/app/src/Features/Notifications/NotificationsHandler.js b/services/web/app/src/Features/Notifications/NotificationsHandler.js new file mode 100644 index 0000000000..788e14b375 --- /dev/null +++ b/services/web/app/src/Features/Notifications/NotificationsHandler.js @@ -0,0 +1,104 @@ +const settings = require('@overleaf/settings') +const request = require('request') +const logger = require('logger-sharelatex') +const _ = require('lodash') + +const notificationsApi = _.get(settings, ['apis', 'notifications', 'url']) +const oneSecond = 1000 + +const makeRequest = function (opts, callback) { + if (notificationsApi) { + request(opts, callback) + } else { + callback(null, { statusCode: 200 }) + } +} + +module.exports = { + getUserNotifications(userId, callback) { + const opts = { + uri: `${notificationsApi}/user/${userId}`, + json: true, + timeout: oneSecond, + method: 'GET', + } + makeRequest(opts, function (err, res, unreadNotifications) { + const statusCode = res ? res.statusCode : 500 + if (err || statusCode !== 200) { + logger.err( + { err, statusCode }, + 'something went wrong getting notifications' + ) + callback(null, []) + } else { + if (unreadNotifications == null) { + unreadNotifications = [] + } + callback(null, unreadNotifications) + } + }) + }, + + createNotification( + userId, + key, + templateKey, + messageOpts, + expiryDateTime, + forceCreate, + callback + ) { + if (!callback) { + callback = forceCreate + forceCreate = true + } + const payload = { + key, + messageOpts, + templateKey, + forceCreate, + } + if (expiryDateTime) { + payload.expires = expiryDateTime + } + const opts = { + uri: `${notificationsApi}/user/${userId}`, + timeout: oneSecond, + method: 'POST', + json: payload, + } + makeRequest(opts, callback) + }, + + markAsReadWithKey(userId, key, callback) { + const opts = { + uri: `${notificationsApi}/user/${userId}`, + method: 'DELETE', + timeout: oneSecond, + json: { + key, + }, + } + makeRequest(opts, callback) + }, + + markAsRead(userId, notificationId, callback) { + const opts = { + method: 'DELETE', + uri: `${notificationsApi}/user/${userId}/notification/${notificationId}`, + timeout: oneSecond, + } + makeRequest(opts, callback) + }, + + // removes notification by key, without regard for user_id, + // should not be exposed to user via ui/router + markAsReadByKeyOnly(key, callback) { + const opts = { + uri: `${notificationsApi}/key/${key}`, + method: 'DELETE', + timeout: oneSecond, + } + makeRequest(opts, callback) + }, +} diff --git a/services/web/app/src/Features/PasswordReset/PasswordResetController.js b/services/web/app/src/Features/PasswordReset/PasswordResetController.js new file mode 100644 index 0000000000..a9a9f8119e --- /dev/null +++ b/services/web/app/src/Features/PasswordReset/PasswordResetController.js @@ -0,0 +1,114 @@ +const PasswordResetHandler = require('./PasswordResetHandler') +const AuthenticationController = require('../Authentication/AuthenticationController') +const SessionManager = require('../Authentication/SessionManager') +const UserGetter = require('../User/UserGetter') +const UserUpdater = require('../User/UserUpdater') +const UserSessionsManager = require('../User/UserSessionsManager') +const OError = require('@overleaf/o-error') +const EmailsHelper = require('../Helpers/EmailHelper') +const { expressify } = require('../../util/promises') + +async function setNewUserPassword(req, res, next) { + let user + let { passwordResetToken, password } = req.body + if (!passwordResetToken || !password) { + return res.sendStatus(400) + } + passwordResetToken = passwordResetToken.trim() + delete req.session.resetToken + + const initiatorId = SessionManager.getLoggedInUserId(req.session) + // password reset via tokens can be done while logged in, or not + const auditLog = { + initiatorId, + ip: req.ip, + } + + try { + const result = await PasswordResetHandler.promises.setNewUserPassword( + passwordResetToken, + password, + auditLog + ) + const { found, reset, userId } = result + if (!found) return res.sendStatus(404) + if (!reset) return res.sendStatus(500) + await UserSessionsManager.promises.revokeAllUserSessions( + { _id: userId }, + [] + ) + await UserUpdater.promises.removeReconfirmFlag(userId) + if (!req.session.doLoginAfterPasswordReset) { + return res.sendStatus(200) + } + user = await UserGetter.promises.getUser(userId) + } catch (error) { + if (error.name === 'NotFoundError') { + return res.sendStatus(404) + } else if (error.name === 'InvalidPasswordError') { + return res.sendStatus(400) + } else { + return res.sendStatus(500) + } + } + + AuthenticationController.finishLogin(user, req, res, next) +} + +module.exports = { + renderRequestResetForm(req, res) { + res.render('user/passwordReset', { title: 'reset_password' }) + }, + + requestReset(req, res, next) { + const email = EmailsHelper.parseEmail(req.body.email) + if (!email) { + return res.status(400).send({ + message: req.i18n.translate('must_be_email_address'), + }) + } + PasswordResetHandler.generateAndEmailResetToken(email, (err, status) => { + if (err != null) { + OError.tag(err, 'failed to generate and email password reset token', { + email, + }) + next(err) + } else if (status === 'primary') { + res.status(200).send({ + message: { text: req.i18n.translate('password_reset_email_sent') }, + }) + } else if (status === 'secondary') { + res.status(404).send({ + message: req.i18n.translate('secondary_email_password_reset'), + }) + } else { + res.status(404).send({ + message: req.i18n.translate('cant_find_email'), + }) + } + }) + }, + + renderSetPasswordForm(req, res) { + if (req.query.passwordResetToken != null) { + req.session.resetToken = req.query.passwordResetToken + let emailQuery = '' + if (typeof req.query.email === 'string') { + const email = EmailsHelper.parseEmail(req.query.email) + if (email) { + emailQuery = `?email=${encodeURIComponent(email)}` + } + } + return res.redirect('/user/password/set' + emailQuery) + } + if (req.session.resetToken == null) { + return res.redirect('/user/password/reset') + } + res.render('user/setPassword', { + title: 'set_password', + passwordResetToken: req.session.resetToken, + }) + }, + + setNewUserPassword: expressify(setNewUserPassword), +} diff --git a/services/web/app/src/Features/PasswordReset/PasswordResetHandler.js b/services/web/app/src/Features/PasswordReset/PasswordResetHandler.js new file mode 100644 index 0000000000..12195ef855 --- /dev/null +++ b/services/web/app/src/Features/PasswordReset/PasswordResetHandler.js @@ -0,0 +1,126 @@ +const settings = require('@overleaf/settings') +const UserAuditLogHandler = require('../User/UserAuditLogHandler') +const UserGetter = require('../User/UserGetter') +const OneTimeTokenHandler = require('../Security/OneTimeTokenHandler') +const EmailHandler = require('../Email/EmailHandler') +const AuthenticationManager = require('../Authentication/AuthenticationManager') +const { callbackify, promisify } = require('util') + +function generateAndEmailResetToken(email, callback) { + UserGetter.getUserByAnyEmail(email, (err, user) => { + if (err || !user) { + return callback(err, null) + } + if (user.email !== email) { + return callback(null, 'secondary') + } + const data = { user_id: user._id.toString(), email: email } + OneTimeTokenHandler.getNewToken('password', data, (err, token) => { + if (err) { + return callback(err) + } + const emailOptions = { + to: email, + setNewPasswordUrl: `${ + settings.siteUrl + }/user/password/set?passwordResetToken=${token}&email=${encodeURIComponent( + email + )}`, + } + EmailHandler.sendEmail('passwordResetRequested', emailOptions, err => { + if (err) { + return callback(err) + } + callback(null, 'primary') + }) + }) + }) +} + +function getUserForPasswordResetToken(token, callback) { + OneTimeTokenHandler.getValueFromTokenAndExpire( + 'password', + token, + (err, data) => { + if (err != null) { + if (err.name === 'NotFoundError') { + return callback(null, null) + } else { + return callback(err) + } + } + if (data == null || data.email == null) { + return callback(null, null) + } + UserGetter.getUserByMainEmail( + data.email, + { _id: 1, 'overleaf.id': 1, email: 1 }, + (err, user) => { + if (err != null) { + callback(err) + } else if (user == null) { + callback(null, null) + } else if ( + data.user_id != null && + data.user_id === user._id.toString() + ) { + callback(null, user) + } else if ( + data.v1_user_id != null && + user.overleaf != null && + data.v1_user_id === user.overleaf.id + ) { + callback(null, user) + } else { + callback(null, null) + } + } + ) + } + ) +} + +async function setNewUserPassword(token, password, auditLog) { + const user = await PasswordResetHandler.promises.getUserForPasswordResetToken( + token + ) + + if (!user) { + return { + found: false, + reset: false, + userId: null, + } + } + + const reset = await AuthenticationManager.promises.setUserPassword( + user, + password + ) + + await UserAuditLogHandler.promises.addEntry( + user._id, + 'reset-password', + auditLog.initiatorId, + auditLog.ip + ) + + return { found: true, reset, userId: user._id } +} + +const PasswordResetHandler = { + generateAndEmailResetToken, + + setNewUserPassword: callbackify(setNewUserPassword), + + getUserForPasswordResetToken, +} + +PasswordResetHandler.promises = { + getUserForPasswordResetToken: promisify( + PasswordResetHandler.getUserForPasswordResetToken + ), + setNewUserPassword, +} + +module.exports = PasswordResetHandler diff --git a/services/web/app/src/Features/PasswordReset/PasswordResetRouter.js b/services/web/app/src/Features/PasswordReset/PasswordResetRouter.js new file mode 100644 index 0000000000..1fed0c8fe7 --- /dev/null +++ b/services/web/app/src/Features/PasswordReset/PasswordResetRouter.js @@ -0,0 +1,62 @@ +const PasswordResetController = require('./PasswordResetController') +const AuthenticationController = require('../Authentication/AuthenticationController') +const CaptchaMiddleware = require('../../Features/Captcha/CaptchaMiddleware') +const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') +const { Joi, validate } = require('../../infrastructure/Validation') + +module.exports = { + apply(webRouter, apiRouter) { + const rateLimit = RateLimiterMiddleware.rateLimit({ + endpointName: 'password_reset_rate_limit', + ipOnly: true, + maxRequests: 6, + timeInterval: 60, + }) + + webRouter.get( + '/user/password/reset', + PasswordResetController.renderRequestResetForm + ) + webRouter.post( + '/user/password/reset', + validate({ + body: Joi.object({ + email: Joi.string().required(), + }), + }), + rateLimit, + CaptchaMiddleware.validateCaptcha('passwordReset'), + PasswordResetController.requestReset + ) + AuthenticationController.addEndpointToLoginWhitelist('/user/password/reset') + + webRouter.get( + '/user/password/set', + PasswordResetController.renderSetPasswordForm + ) + webRouter.post( + '/user/password/set', + validate({ + body: Joi.object({ + password: Joi.string().required(), + passwordResetToken: Joi.string().required(), + }), + }), + rateLimit, + PasswordResetController.setNewUserPassword + ) + AuthenticationController.addEndpointToLoginWhitelist('/user/password/set') + + webRouter.post( + '/user/reconfirm', + validate({ + body: Joi.object({ + email: Joi.string().required(), + }), + }), + rateLimit, + CaptchaMiddleware.validateCaptcha('passwordReset'), + PasswordResetController.requestReset + ) + }, +} diff --git a/services/web/app/src/Features/Project/DocLinesComparitor.js b/services/web/app/src/Features/Project/DocLinesComparitor.js new file mode 100644 index 0000000000..12eefd6f13 --- /dev/null +++ b/services/web/app/src/Features/Project/DocLinesComparitor.js @@ -0,0 +1,13 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +const _ = require('underscore') + +module.exports = { + areSame(lines1, lines2) { + if (!Array.isArray(lines1) || !Array.isArray(lines2)) { + return false + } + + return _.isEqual(lines1, lines2) + }, +} diff --git a/services/web/app/src/Features/Project/FolderStructureBuilder.js b/services/web/app/src/Features/Project/FolderStructureBuilder.js new file mode 100644 index 0000000000..c53e4f652b --- /dev/null +++ b/services/web/app/src/Features/Project/FolderStructureBuilder.js @@ -0,0 +1,73 @@ +const Path = require('path') +const OError = require('@overleaf/o-error') +const { ObjectId } = require('mongodb') + +module.exports = { buildFolderStructure } + +function buildFolderStructure(docEntries, fileEntries) { + const builder = new FolderStructureBuilder() + for (const docEntry of docEntries) { + builder.addDocEntry(docEntry) + } + for (const fileEntry of fileEntries) { + builder.addFileEntry(fileEntry) + } + return builder.rootFolder +} + +class FolderStructureBuilder { + constructor() { + this.foldersByPath = new Map() + this.entityPaths = new Set() + this.rootFolder = this.createFolder('rootFolder') + this.foldersByPath.set('/', this.rootFolder) + this.entityPaths.add('/') + } + + addDocEntry(docEntry) { + this.recordEntityPath(docEntry.path) + const folderPath = Path.dirname(docEntry.path) + const folder = this.mkdirp(folderPath) + folder.docs.push(docEntry.doc) + } + + addFileEntry(fileEntry) { + this.recordEntityPath(fileEntry.path) + const folderPath = Path.dirname(fileEntry.path) + const folder = this.mkdirp(folderPath) + folder.fileRefs.push(fileEntry.file) + } + + mkdirp(path) { + const existingFolder = this.foldersByPath.get(path) + if (existingFolder != null) { + return existingFolder + } + // Folder not found, create it. + this.recordEntityPath(path) + const dirname = Path.dirname(path) + const basename = Path.basename(path) + const parentFolder = this.mkdirp(dirname) + const newFolder = this.createFolder(basename) + parentFolder.folders.push(newFolder) + this.foldersByPath.set(path, newFolder) + return newFolder + } + + recordEntityPath(path) { + if (this.entityPaths.has(path)) { + throw new OError('entity already exists', { path }) + } + this.entityPaths.add(path) + } + + createFolder(name) { + return { + _id: ObjectId(), + name, + folders: [], + docs: [], + fileRefs: [], + } + } +} diff --git a/services/web/app/src/Features/Project/ProjectApiController.js b/services/web/app/src/Features/Project/ProjectApiController.js new file mode 100644 index 0000000000..e06a43346a --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectApiController.js @@ -0,0 +1,30 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const ProjectDetailsHandler = require('./ProjectDetailsHandler') +const logger = require('logger-sharelatex') + +module.exports = { + getProjectDetails(req, res, next) { + const { project_id } = req.params + return ProjectDetailsHandler.getDetails( + project_id, + function (err, projDetails) { + if (err != null) { + return next(err) + } + return res.json(projDetails) + } + ) + }, +} diff --git a/services/web/app/src/Features/Project/ProjectAuditLogHandler.js b/services/web/app/src/Features/Project/ProjectAuditLogHandler.js new file mode 100644 index 0000000000..99ab0aa0b3 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectAuditLogHandler.js @@ -0,0 +1,40 @@ +const OError = require('@overleaf/o-error') +const { Project } = require('../../models/Project') + +const MAX_AUDIT_LOG_ENTRIES = 200 + +module.exports = { + promises: { + addEntry, + }, +} + +/** + * Add an audit log entry + * + * The entry should include at least the following fields: + * + * - operation: a string identifying the type of operation + * - userId: the user on behalf of whom the operation was performed + * - message: a string detailing what happened + */ +async function addEntry(projectId, operation, initiatorId, info = {}) { + const timestamp = new Date() + const entry = { + operation, + initiatorId, + timestamp, + info, + } + const result = await Project.updateOne( + { _id: projectId }, + { + $push: { + auditLog: { $each: [entry], $slice: -MAX_AUDIT_LOG_ENTRIES }, + }, + } + ).exec() + if (result.nModified === 0) { + throw new OError('project not found', { projectId }) + } +} diff --git a/services/web/app/src/Features/Project/ProjectCollabratecDetailsHandler.js b/services/web/app/src/Features/Project/ProjectCollabratecDetailsHandler.js new file mode 100644 index 0000000000..0235942856 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectCollabratecDetailsHandler.js @@ -0,0 +1,165 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectCollabratecDetailsHandler +const { ObjectId } = require('mongodb') +const { Project } = require('../../models/Project') + +module.exports = ProjectCollabratecDetailsHandler = { + initializeCollabratecProject( + project_id, + user_id, + collabratec_document_id, + collabratec_privategroup_id, + callback + ) { + if (callback == null) { + callback = function (err) {} + } + return ProjectCollabratecDetailsHandler.setCollabratecUsers( + project_id, + [{ user_id, collabratec_document_id, collabratec_privategroup_id }], + callback + ) + }, + + isLinkedCollabratecUserProject(project_id, user_id, callback) { + if (callback == null) { + callback = function (err, isLinked) {} + } + try { + project_id = ObjectId(project_id) + user_id = ObjectId(user_id) + } catch (error) { + const err = error + return callback(err) + } + const query = { + _id: project_id, + collabratecUsers: { + $elemMatch: { + user_id, + }, + }, + } + return Project.findOne(query, { _id: 1 }, function (err, project) { + if (err != null) { + callback(err) + } + return callback(null, project != null) + }) + }, + + linkCollabratecUserProject( + project_id, + user_id, + collabratec_document_id, + callback + ) { + if (callback == null) { + callback = function (err) {} + } + try { + project_id = ObjectId(project_id) + user_id = ObjectId(user_id) + } catch (error) { + const err = error + return callback(err) + } + const query = { + _id: project_id, + collabratecUsers: { + $not: { + $elemMatch: { + collabratec_document_id, + user_id, + }, + }, + }, + } + const update = { + $push: { + collabratecUsers: { + collabratec_document_id, + user_id, + }, + }, + } + return Project.updateOne(query, update, callback) + }, + + setCollabratecUsers(project_id, collabratec_users, callback) { + let err + if (callback == null) { + callback = function (err) {} + } + try { + project_id = ObjectId(project_id) + } catch (error) { + err = error + return callback(err) + } + if (!Array.isArray(collabratec_users)) { + callback(new Error('collabratec_users must be array')) + } + for (const collabratec_user of Array.from(collabratec_users)) { + try { + collabratec_user.user_id = ObjectId(collabratec_user.user_id) + } catch (error1) { + err = error1 + return callback(err) + } + } + const update = { $set: { collabratecUsers: collabratec_users } } + return Project.updateOne({ _id: project_id }, update, callback) + }, + + unlinkCollabratecUserProject(project_id, user_id, callback) { + if (callback == null) { + callback = function (err) {} + } + try { + project_id = ObjectId(project_id) + user_id = ObjectId(user_id) + } catch (error) { + const err = error + return callback(err) + } + const query = { _id: project_id } + const update = { + $pull: { + collabratecUsers: { + user_id, + }, + }, + } + return Project.updateOne(query, update, callback) + }, + + updateCollabratecUserIds(old_user_id, new_user_id, callback) { + if (callback == null) { + callback = function (err) {} + } + try { + old_user_id = ObjectId(old_user_id) + new_user_id = ObjectId(new_user_id) + } catch (error) { + const err = error + return callback(err) + } + const query = { 'collabratecUsers.user_id': old_user_id } + const update = { $set: { 'collabratecUsers.$.user_id': new_user_id } } + return Project.updateMany(query, update, callback) + }, +} diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js new file mode 100644 index 0000000000..47cc292062 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -0,0 +1,1119 @@ +const _ = require('lodash') +const Path = require('path') +const OError = require('@overleaf/o-error') +const fs = require('fs') +const crypto = require('crypto') +const async = require('async') +const logger = require('logger-sharelatex') +const { ObjectId } = require('mongodb') +const ProjectDeleter = require('./ProjectDeleter') +const ProjectDuplicator = require('./ProjectDuplicator') +const ProjectCreationHandler = require('./ProjectCreationHandler') +const EditorController = require('../Editor/EditorController') +const ProjectHelper = require('./ProjectHelper') +const metrics = require('@overleaf/metrics') +const { User } = require('../../models/User') +const TagsHandler = require('../Tags/TagsHandler') +const SubscriptionLocator = require('../Subscription/SubscriptionLocator') +const NotificationsHandler = require('../Notifications/NotificationsHandler') +const LimitationsManager = require('../Subscription/LimitationsManager') +const Settings = require('@overleaf/settings') +const AuthorizationManager = require('../Authorization/AuthorizationManager') +const InactiveProjectManager = require('../InactiveData/InactiveProjectManager') +const ProjectUpdateHandler = require('./ProjectUpdateHandler') +const ProjectGetter = require('./ProjectGetter') +const PrivilegeLevels = require('../Authorization/PrivilegeLevels') +const SessionManager = require('../Authentication/SessionManager') +const Sources = require('../Authorization/Sources') +const TokenAccessHandler = require('../TokenAccess/TokenAccessHandler') +const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') +const ProjectEntityHandler = require('./ProjectEntityHandler') +const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher') +const UserGetter = require('../User/UserGetter') +const NotificationsBuilder = require('../Notifications/NotificationsBuilder') +const { V1ConnectionError } = require('../Errors/Errors') +const Features = require('../../infrastructure/Features') +const BrandVariationsHandler = require('../BrandVariations/BrandVariationsHandler') +const UserController = require('../User/UserController') +const AnalyticsManager = require('../Analytics/AnalyticsManager') +const Modules = require('../../infrastructure/Modules') +const SplitTestHandler = require('../SplitTests/SplitTestHandler') +const { getNewLogsUIVariantForUser } = require('../Helpers/NewLogsUI') + +const _ssoAvailable = (affiliation, session, linkedInstitutionIds) => { + if (!affiliation.institution) return false + + // institution.confirmed is for the domain being confirmed, not the email + // Do not show SSO UI for unconfirmed domains + if (!affiliation.institution.confirmed) return false + + // Could have multiple emails at the same institution, and if any are + // linked to the institution then do not show notification for others + if ( + linkedInstitutionIds.indexOf(affiliation.institution.id.toString()) === -1 + ) { + if (affiliation.institution.ssoEnabled) return true + if (affiliation.institution.ssoBeta && session.samlBeta) return true + return false + } + return false +} + +const ProjectController = { + _isInPercentageRollout(rolloutName, objectId, percentage) { + if (Settings.bypassPercentageRollouts === true) { + return true + } + const data = `${rolloutName}:${objectId.toString()}` + const md5hash = crypto.createHash('md5').update(data).digest('hex') + const counter = parseInt(md5hash.slice(26, 32), 16) + return counter % 100 < percentage + }, + + updateProjectSettings(req, res, next) { + const projectId = req.params.Project_id + + const jobs = [] + + if (req.body.compiler != null) { + jobs.push(callback => + EditorController.setCompiler(projectId, req.body.compiler, callback) + ) + } + + if (req.body.imageName != null) { + jobs.push(callback => + EditorController.setImageName(projectId, req.body.imageName, callback) + ) + } + + if (req.body.name != null) { + jobs.push(callback => + EditorController.renameProject(projectId, req.body.name, callback) + ) + } + + if (req.body.spellCheckLanguage != null) { + jobs.push(callback => + EditorController.setSpellCheckLanguage( + projectId, + req.body.spellCheckLanguage, + callback + ) + ) + } + + if (req.body.rootDocId != null) { + jobs.push(callback => + EditorController.setRootDoc(projectId, req.body.rootDocId, callback) + ) + } + + async.series(jobs, error => { + if (error != null) { + return next(error) + } + res.sendStatus(204) + }) + }, + + updateProjectAdminSettings(req, res, next) { + const projectId = req.params.Project_id + + const jobs = [] + if (req.body.publicAccessLevel != null) { + jobs.push(callback => + EditorController.setPublicAccessLevel( + projectId, + req.body.publicAccessLevel, + callback + ) + ) + } + + async.series(jobs, error => { + if (error != null) { + return next(error) + } + res.sendStatus(204) + }) + }, + + deleteProject(req, res) { + const projectId = req.params.Project_id + const user = SessionManager.getSessionUser(req.session) + const cb = err => { + if (err != null) { + res.sendStatus(500) + } else { + res.sendStatus(200) + } + } + ProjectDeleter.deleteProject( + projectId, + { deleterUser: user, ipAddress: req.ip }, + cb + ) + }, + + archiveProject(req, res, next) { + const projectId = req.params.Project_id + const userId = SessionManager.getLoggedInUserId(req.session) + + ProjectDeleter.archiveProject(projectId, userId, function (err) { + if (err != null) { + return next(err) + } else { + return res.sendStatus(200) + } + }) + }, + + unarchiveProject(req, res, next) { + const projectId = req.params.Project_id + const userId = SessionManager.getLoggedInUserId(req.session) + + ProjectDeleter.unarchiveProject(projectId, userId, function (err) { + if (err != null) { + return next(err) + } else { + return res.sendStatus(200) + } + }) + }, + + trashProject(req, res, next) { + const projectId = req.params.project_id + const userId = SessionManager.getLoggedInUserId(req.session) + + ProjectDeleter.trashProject(projectId, userId, function (err) { + if (err != null) { + return next(err) + } else { + return res.sendStatus(200) + } + }) + }, + + untrashProject(req, res, next) { + const projectId = req.params.project_id + const userId = SessionManager.getLoggedInUserId(req.session) + + ProjectDeleter.untrashProject(projectId, userId, function (err) { + if (err != null) { + return next(err) + } else { + return res.sendStatus(200) + } + }) + }, + + expireDeletedProjectsAfterDuration(req, res) { + ProjectDeleter.expireDeletedProjectsAfterDuration(err => { + if (err != null) { + res.sendStatus(500) + } else { + res.sendStatus(200) + } + }) + }, + + expireDeletedProject(req, res, next) { + const { projectId } = req.params + ProjectDeleter.expireDeletedProject(projectId, err => { + if (err != null) { + next(err) + } else { + res.sendStatus(200) + } + }) + }, + + restoreProject(req, res) { + const projectId = req.params.Project_id + ProjectDeleter.restoreProject(projectId, err => { + if (err != null) { + res.sendStatus(500) + } else { + res.sendStatus(200) + } + }) + }, + + cloneProject(req, res, next) { + res.setTimeout(5 * 60 * 1000) // allow extra time for the copy to complete + metrics.inc('cloned-project') + const projectId = req.params.Project_id + const { projectName } = req.body + logger.log({ projectId, projectName }, 'cloning project') + if (!SessionManager.isUserLoggedIn(req.session)) { + return res.send({ redir: '/register' }) + } + const currentUser = SessionManager.getSessionUser(req.session) + const { first_name: firstName, last_name: lastName, email } = currentUser + ProjectDuplicator.duplicate( + currentUser, + projectId, + projectName, + (err, project) => { + if (err != null) { + OError.tag(err, 'error cloning project', { + projectId, + userId: currentUser._id, + }) + return next(err) + } + res.send({ + name: project.name, + project_id: project._id, + owner_ref: project.owner_ref, + owner: { + first_name: firstName, + last_name: lastName, + email, + _id: currentUser._id, + }, + }) + } + ) + }, + + newProject(req, res, next) { + const currentUser = SessionManager.getSessionUser(req.session) + const { + first_name: firstName, + last_name: lastName, + email, + _id: userId, + } = currentUser + const projectName = + req.body.projectName != null ? req.body.projectName.trim() : undefined + const { template } = req.body + + async.waterfall( + [ + cb => { + if (template === 'example') { + ProjectCreationHandler.createExampleProject(userId, projectName, cb) + } else { + ProjectCreationHandler.createBasicProject(userId, projectName, cb) + } + }, + ], + (err, project) => { + if (err != null) { + return next(err) + } + res.send({ + project_id: project._id, + owner_ref: project.owner_ref, + owner: { + first_name: firstName, + last_name: lastName, + email, + _id: userId, + }, + }) + } + ) + }, + + renameProject(req, res, next) { + const projectId = req.params.Project_id + const newName = req.body.newProjectName + EditorController.renameProject(projectId, newName, err => { + if (err != null) { + return next(err) + } + res.sendStatus(200) + }) + }, + + userProjectsJson(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + ProjectGetter.findAllUsersProjects( + userId, + 'name lastUpdated publicAccesLevel archived trashed owner_ref tokens', + (err, projects) => { + if (err != null) { + return next(err) + } + + // _buildProjectList already converts archived/trashed to booleans so isArchivedOrTrashed should not be used here + projects = ProjectController._buildProjectList(projects, userId) + .filter(p => !(p.archived || p.trashed)) + .map(p => ({ _id: p.id, name: p.name, accessLevel: p.accessLevel })) + + res.json({ projects }) + } + ) + }, + + projectEntitiesJson(req, res, next) { + const projectId = req.params.Project_id + ProjectGetter.getProject(projectId, (err, project) => { + if (err != null) { + return next(err) + } + ProjectEntityHandler.getAllEntitiesFromProject( + project, + (err, docs, files) => { + if (err != null) { + return next(err) + } + const entities = docs + .concat(files) + // Sort by path ascending + .sort((a, b) => (a.path > b.path ? 1 : a.path < b.path ? -1 : 0)) + .map(e => ({ + path: e.path, + type: e.doc != null ? 'doc' : 'file', + })) + res.json({ project_id: projectId, entities }) + } + ) + }) + }, + + projectListPage(req, res, next) { + const timer = new metrics.Timer('project-list') + const userId = SessionManager.getLoggedInUserId(req.session) + const currentUser = SessionManager.getSessionUser(req.session) + async.parallel( + { + tags(cb) { + TagsHandler.getAllTags(userId, cb) + }, + notifications(cb) { + NotificationsHandler.getUserNotifications(userId, cb) + }, + projects(cb) { + ProjectGetter.findAllUsersProjects( + userId, + 'name lastUpdated lastUpdatedBy publicAccesLevel archived trashed owner_ref tokens', + cb + ) + }, + hasSubscription(cb) { + LimitationsManager.hasPaidSubscription( + currentUser, + (error, hasPaidSubscription) => { + if (error != null && error instanceof V1ConnectionError) { + return cb(null, true) + } + cb(error, hasPaidSubscription) + } + ) + }, + user(cb) { + User.findById( + userId, + 'emails featureSwitches overleaf awareOfV2 features lastLoginIp', + cb + ) + }, + userEmailsData(cb) { + const result = { list: [], allInReconfirmNotificationPeriods: [] } + + UserGetter.getUserFullEmails(userId, (error, fullEmails) => { + if (error && error instanceof V1ConnectionError) { + return cb(null, result) + } + + if (!Features.hasFeature('affiliations')) { + result.list = fullEmails + return cb(null, result) + } + Modules.hooks.fire( + 'allInReconfirmNotificationPeriodsForUser', + fullEmails, + (error, results) => { + // Module.hooks.fire accepts multiple methods + // and does async.series + const allInReconfirmNotificationPeriods = + (results && results[0]) || [] + return cb(null, { + list: fullEmails, + allInReconfirmNotificationPeriods, + }) + } + ) + }) + }, + }, + (err, results) => { + if (err != null) { + OError.tag(err, 'error getting data for project list page') + return next(err) + } + const { notifications, user, userEmailsData } = results + + const userEmails = userEmailsData.list || [] + + const userAffiliations = userEmails + .filter(emailData => !!emailData.affiliation) + .map(emailData => { + const result = emailData.affiliation + result.email = emailData.email + return result + }) + + const { allInReconfirmNotificationPeriods } = userEmailsData + + // Handle case of deleted user + if (user == null) { + UserController.logout(req, res, next) + return + } + const tags = results.tags + const notificationsInstitution = [] + for (const notification of notifications) { + notification.html = req.i18n.translate( + notification.templateKey, + notification.messageOpts + ) + } + + // Institution SSO Notifications + let reconfirmedViaSAML + if (Features.hasFeature('saml')) { + reconfirmedViaSAML = _.get(req.session, ['saml', 'reconfirmed']) + const samlSession = req.session.saml + // Notification: SSO Available + const linkedInstitutionIds = [] + user.emails.forEach(email => { + if (email.samlProviderId) { + linkedInstitutionIds.push(email.samlProviderId) + } + }) + if (Array.isArray(userAffiliations)) { + userAffiliations.forEach(affiliation => { + if ( + _ssoAvailable(affiliation, req.session, linkedInstitutionIds) + ) { + notificationsInstitution.push({ + email: affiliation.email, + institutionId: affiliation.institution.id, + institutionName: affiliation.institution.name, + templateKey: 'notification_institution_sso_available', + }) + } + }) + } + + if (samlSession) { + // Notification: After SSO Linked + if (samlSession.linked) { + notificationsInstitution.push({ + email: samlSession.institutionEmail, + institutionName: samlSession.linked.universityName, + templateKey: 'notification_institution_sso_linked', + }) + } + + // Notification: After SSO Linked or Logging in + // The requested email does not match primary email returned from + // the institution + if ( + samlSession.requestedEmail && + samlSession.emailNonCanonical && + !samlSession.error + ) { + notificationsInstitution.push({ + institutionEmail: samlSession.emailNonCanonical, + requestedEmail: samlSession.requestedEmail, + templateKey: 'notification_institution_sso_non_canonical', + }) + } + + // Notification: Tried to register, but account already existed + // registerIntercept is set before the institution callback. + // institutionEmail is set after institution callback. + // Check for both in case SSO flow was abandoned + if ( + samlSession.registerIntercept && + samlSession.institutionEmail && + !samlSession.error + ) { + notificationsInstitution.push({ + email: samlSession.institutionEmail, + templateKey: 'notification_institution_sso_already_registered', + }) + } + + // Notification: When there is a session error + if (samlSession.error) { + notificationsInstitution.push({ + templateKey: 'notification_institution_sso_error', + error: samlSession.error, + }) + } + } + delete req.session.saml + } + + const portalTemplates = ProjectController._buildPortalTemplatesList( + userAffiliations + ) + const projects = ProjectController._buildProjectList( + results.projects, + userId + ) + + // in v2 add notifications for matching university IPs + if (Settings.overleaf != null && req.ip !== user.lastLoginIp) { + NotificationsBuilder.ipMatcherAffiliation(user._id).create(req.ip) + } + + ProjectController._injectProjectUsers(projects, (error, projects) => { + if (error != null) { + return next(error) + } + const viewModel = { + title: 'your_projects', + priority_title: true, + projects, + tags, + notifications: notifications || [], + notificationsInstitution, + allInReconfirmNotificationPeriods, + portalTemplates, + user, + userAffiliations, + userEmails, + hasSubscription: results.hasSubscription, + reconfirmedViaSAML, + zipFileSizeLimit: Settings.maxUploadSize, + isOverleaf: !!Settings.overleaf, + } + + const paidUser = + (user.features != null ? user.features.github : undefined) && + (user.features != null ? user.features.dropbox : undefined) // use a heuristic for paid account + const freeUserProportion = 0.1 + const sampleFreeUser = + parseInt(user._id.toString().slice(-2), 16) < + freeUserProportion * 255 + const showFrontWidget = paidUser || sampleFreeUser + + if (showFrontWidget) { + viewModel.frontChatWidgetRoomId = + Settings.overleaf != null + ? Settings.overleaf.front_chat_widget_room_id + : undefined + } + + res.render('project/list', viewModel) + timer.done() + }) + } + ) + }, + + loadEditor(req, res, next) { + const timer = new metrics.Timer('load-editor') + if (!Settings.editorIsOpen) { + return res.render('general/closed', { title: 'updating_site' }) + } + + let anonymous, userId, sessionUser + if (SessionManager.isUserLoggedIn(req.session)) { + sessionUser = SessionManager.getSessionUser(req.session) + userId = SessionManager.getLoggedInUserId(req.session) + anonymous = false + } else { + sessionUser = null + anonymous = true + userId = null + } + + const projectId = req.params.Project_id + + async.auto( + { + project(cb) { + ProjectGetter.getProject( + projectId, + { + name: 1, + lastUpdated: 1, + track_changes: 1, + owner_ref: 1, + brandVariationId: 1, + overleaf: 1, + tokens: 1, + }, + (err, project) => { + if (err != null) { + return cb(err) + } + cb(null, project) + } + ) + }, + user(cb) { + if (userId == null) { + cb(null, defaultSettingsForAnonymousUser(userId)) + } else { + User.findById( + userId, + 'email first_name last_name referal_id signUpDate featureSwitches features refProviders alphaProgram betaProgram isAdmin ace', + (err, user) => { + // Handle case of deleted user + if (user == null) { + UserController.logout(req, res, next) + return + } + + logger.log({ projectId, userId }, 'got user') + cb(err, user) + } + ) + } + }, + subscription(cb) { + if (userId == null) { + return cb() + } + SubscriptionLocator.getUsersSubscription(userId, cb) + }, + activate(cb) { + InactiveProjectManager.reactivateProjectIfRequired(projectId, cb) + }, + markAsOpened(cb) { + // don't need to wait for this to complete + ProjectUpdateHandler.markAsOpened(projectId, () => {}) + cb() + }, + isTokenMember(cb) { + if (userId == null) { + return cb() + } + CollaboratorsGetter.userIsTokenMember(userId, projectId, cb) + }, + brandVariation: [ + 'project', + (cb, results) => { + if ( + (results.project != null + ? results.project.brandVariationId + : undefined) == null + ) { + return cb() + } + BrandVariationsHandler.getBrandVariationById( + results.project.brandVariationId, + (error, brandVariationDetails) => cb(error, brandVariationDetails) + ) + }, + ], + flushToTpds: cb => { + TpdsProjectFlusher.flushProjectToTpdsIfNeeded(projectId, cb) + }, + pdfCachingFeatureFlag(cb) { + if (!Settings.enablePdfCaching) return cb(null, '') + if (!userId) return cb(null, 'enable-caching-only') + SplitTestHandler.getTestSegmentation( + userId, + 'pdf_caching_full', + (err, segmentation) => { + if (err) { + // Do not fail loading the editor. + return cb(null, '') + } + cb(null, (segmentation && segmentation.variant) || '') + } + ) + }, + }, + (err, results) => { + if (err != null) { + OError.tag(err, 'error getting details for project page') + return next(err) + } + const { project } = results + const { user } = results + const { subscription } = results + const { brandVariation } = results + const { pdfCachingFeatureFlag } = results + + const anonRequestToken = TokenAccessHandler.getRequestToken( + req, + projectId + ) + const { isTokenMember } = results + const allowedImageNames = ProjectHelper.getAllowedImagesForUser( + sessionUser + ) + + AuthorizationManager.getPrivilegeLevelForProject( + userId, + projectId, + anonRequestToken, + (error, privilegeLevel) => { + let allowedFreeTrial = true + if (error != null) { + return next(error) + } + if ( + privilegeLevel == null || + privilegeLevel === PrivilegeLevels.NONE + ) { + return res.sendStatus(401) + } + + if (subscription != null) { + allowedFreeTrial = false + } + + let wsUrl = Settings.wsUrl + let metricName = 'load-editor-ws' + if (user.betaProgram && Settings.wsUrlBeta !== undefined) { + wsUrl = Settings.wsUrlBeta + metricName += '-beta' + } else if ( + Settings.wsUrlV2 && + Settings.wsUrlV2Percentage > 0 && + (ObjectId(projectId).getTimestamp() / 1000) % 100 < + Settings.wsUrlV2Percentage + ) { + wsUrl = Settings.wsUrlV2 + metricName += '-v2' + } + if (req.query && req.query.ws === 'fallback') { + // `?ws=fallback` will connect to the bare origin, and ignore + // the custom wsUrl. Hence it must load the client side + // javascript from there too. + // Not resetting it here would possibly load a socket.io v2 + // client and connect to a v0 endpoint. + wsUrl = undefined + metricName += '-fallback' + } + metrics.inc(metricName) + + if (userId) { + AnalyticsManager.recordEvent(userId, 'project-opened', { + projectId: project._id, + }) + } + + const logsUIVariant = getNewLogsUIVariantForUser(user) + + function shouldDisplayFeature(name, variantFlag) { + if (req.query && req.query[name]) { + return req.query[name] === 'true' + } else { + return variantFlag === true + } + } + + function partOfPdfCachingRollout(flag) { + if (!Settings.enablePdfCaching) { + // The feature is disabled globally. + return false + } + const canSeeFeaturePreview = pdfCachingFeatureFlag.includes(flag) + if (!canSeeFeaturePreview) { + // The user is not in the target group. + return false + } + // Optionally let the user opt-out. + // The will opt-out of both caching and metrics collection, + // as if this editing session never happened. + return shouldDisplayFeature('enable_pdf_caching', true) + } + + res.render('project/editor', { + title: project.name, + priority_title: true, + bodyClasses: ['editor'], + project_id: project._id, + user: { + id: userId, + email: user.email, + first_name: user.first_name, + last_name: user.last_name, + referal_id: user.referal_id, + signUpDate: user.signUpDate, + allowedFreeTrial: allowedFreeTrial, + featureSwitches: user.featureSwitches, + features: user.features, + refProviders: _.mapValues(user.refProviders, Boolean), + alphaProgram: user.alphaProgram, + betaProgram: user.betaProgram, + isAdmin: user.isAdmin, + }, + userSettings: { + mode: user.ace.mode, + editorTheme: user.ace.theme, + fontSize: user.ace.fontSize, + autoComplete: user.ace.autoComplete, + autoPairDelimiters: user.ace.autoPairDelimiters, + pdfViewer: user.ace.pdfViewer, + syntaxValidation: user.ace.syntaxValidation, + fontFamily: user.ace.fontFamily || 'lucida', + lineHeight: user.ace.lineHeight || 'normal', + overallTheme: user.ace.overallTheme, + }, + privilegeLevel, + anonymous, + anonymousAccessToken: anonymous ? anonRequestToken : null, + isTokenMember, + isRestrictedTokenMember: AuthorizationManager.isRestrictedUser( + userId, + privilegeLevel, + isTokenMember + ), + languages: Settings.languages, + editorThemes: THEME_LIST, + maxDocLength: Settings.max_doc_length, + useV2History: + project.overleaf && + project.overleaf.history && + Boolean(project.overleaf.history.display), + brandVariation, + allowedImageNames, + gitBridgePublicBaseUrl: Settings.gitBridgePublicBaseUrl, + wsUrl, + showSupport: Features.hasFeature('support'), + showNewLogsUI: shouldDisplayFeature( + 'new_logs_ui', + logsUIVariant.newLogsUI + ), + logsUISubvariant: logsUIVariant.subvariant, + showNewNavigationUI: shouldDisplayFeature( + 'new_navigation_ui', + true + ), + showNewFileViewUI: shouldDisplayFeature( + 'new_file_view', + user.alphaProgram || user.betaProgram + ), + showSymbolPalette: shouldDisplayFeature( + 'symbol_palette', + user.alphaProgram || user.betaProgram + ), + trackPdfDownload: partOfPdfCachingRollout('collect-metrics'), + enablePdfCaching: partOfPdfCachingRollout('enable-caching'), + resetServiceWorker: Boolean(Settings.resetServiceWorker), + }) + timer.done() + } + ) + } + ) + }, + + _buildProjectList(allProjects, userId) { + let project + const { + owned, + readAndWrite, + readOnly, + tokenReadAndWrite, + tokenReadOnly, + } = allProjects + const projects = [] + for (project of owned) { + projects.push( + ProjectController._buildProjectViewModel( + project, + 'owner', + Sources.OWNER, + userId + ) + ) + } + // Invite-access + for (project of readAndWrite) { + projects.push( + ProjectController._buildProjectViewModel( + project, + 'readWrite', + Sources.INVITE, + userId + ) + ) + } + for (project of readOnly) { + projects.push( + ProjectController._buildProjectViewModel( + project, + 'readOnly', + Sources.INVITE, + userId + ) + ) + } + // Token-access + // Only add these projects if they're not already present, this gives us cascading access + // from 'owner' => 'token-read-only' + for (project of tokenReadAndWrite) { + if ( + projects.filter(p => p.id.toString() === project._id.toString()) + .length === 0 + ) { + projects.push( + ProjectController._buildProjectViewModel( + project, + 'readAndWrite', + Sources.TOKEN, + userId + ) + ) + } + } + for (project of tokenReadOnly) { + if ( + projects.filter(p => p.id.toString() === project._id.toString()) + .length === 0 + ) { + projects.push( + ProjectController._buildProjectViewModel( + project, + 'readOnly', + Sources.TOKEN, + userId + ) + ) + } + } + + return projects + }, + + _buildProjectViewModel(project, accessLevel, source, userId) { + const archived = ProjectHelper.isArchived(project, userId) + // If a project is simultaneously trashed and archived, we will consider it archived but not trashed. + const trashed = ProjectHelper.isTrashed(project, userId) && !archived + + TokenAccessHandler.protectTokens(project, accessLevel) + const model = { + id: project._id, + name: project.name, + lastUpdated: project.lastUpdated, + lastUpdatedBy: project.lastUpdatedBy, + publicAccessLevel: project.publicAccesLevel, + accessLevel, + source, + archived, + trashed, + owner_ref: project.owner_ref, + isV1Project: false, + } + if (accessLevel === PrivilegeLevels.READ_ONLY && source === Sources.TOKEN) { + model.owner_ref = null + model.lastUpdatedBy = null + } + return model + }, + + _injectProjectUsers(projects, callback) { + const users = {} + for (const project of projects) { + if (project.owner_ref != null) { + users[project.owner_ref.toString()] = true + } + if (project.lastUpdatedBy != null) { + users[project.lastUpdatedBy.toString()] = true + } + } + + const userIds = Object.keys(users) + async.eachSeries( + userIds, + (userId, cb) => { + UserGetter.getUser( + userId, + { first_name: 1, last_name: 1, email: 1 }, + (error, user) => { + if (error != null) { + return cb(error) + } + users[userId] = user + cb() + } + ) + }, + error => { + if (error != null) { + return callback(error) + } + for (const project of projects) { + if (project.owner_ref != null) { + project.owner = users[project.owner_ref.toString()] + } + if (project.lastUpdatedBy != null) { + project.lastUpdatedBy = + users[project.lastUpdatedBy.toString()] || null + } + } + callback(null, projects) + } + ) + }, + + _buildPortalTemplatesList(affiliations) { + if (affiliations == null) { + affiliations = [] + } + const portalTemplates = [] + for (const aff of affiliations) { + if ( + aff.portal && + aff.portal.slug && + aff.portal.templates_count && + aff.portal.templates_count > 0 + ) { + const portalPath = aff.institution.isUniversity ? '/edu/' : '/org/' + portalTemplates.push({ + name: aff.institution.name, + url: Settings.siteUrl + portalPath + aff.portal.slug, + }) + } + } + return portalTemplates + }, +} + +var defaultSettingsForAnonymousUser = userId => ({ + id: userId, + ace: { + mode: 'none', + theme: 'textmate', + fontSize: '12', + autoComplete: true, + spellCheckLanguage: '', + pdfViewer: '', + syntaxValidation: true, + }, + subscription: { + freeTrial: { + allowed: true, + }, + }, + featureSwitches: { + github: false, + }, + alphaProgram: false, + betaProgram: false, +}) + +var THEME_LIST = [] +function generateThemeList() { + const files = fs.readdirSync( + Path.join(__dirname, '/../../../../node_modules/ace-builds/src-noconflict') + ) + const result = [] + for (const file of files) { + if (file.slice(-2) === 'js' && /^theme-/.test(file)) { + const cleanName = file.slice(0, -3).slice(6) + result.push(THEME_LIST.push(cleanName)) + } else { + result.push(undefined) + } + } +} +generateThemeList() + +module.exports = ProjectController diff --git a/services/web/app/src/Features/Project/ProjectCreationHandler.js b/services/web/app/src/Features/Project/ProjectCreationHandler.js new file mode 100644 index 0000000000..be9dff61f3 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectCreationHandler.js @@ -0,0 +1,250 @@ +const logger = require('logger-sharelatex') +const OError = require('@overleaf/o-error') +const metrics = require('@overleaf/metrics') +const Settings = require('@overleaf/settings') +const { ObjectId } = require('mongodb') +const Features = require('../../infrastructure/Features') +const { Project } = require('../../models/Project') +const { Folder } = require('../../models/Folder') +const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler') +const ProjectDetailsHandler = require('./ProjectDetailsHandler') +const HistoryManager = require('../History/HistoryManager') +const { User } = require('../../models/User') +const fs = require('fs') +const path = require('path') +const { callbackify } = require('util') +const _ = require('underscore') +const AnalyticsManager = require('../Analytics/AnalyticsManager') +const SplitTestV2Handler = require('../SplitTests/SplitTestV2Handler') + +const MONTH_NAMES = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', +] +const EXAMPLE_PROJECT_SPLITTEST_ID = 'example-project-v3' + +async function createBlankProject(ownerId, projectName, attributes = {}) { + const isImport = attributes && attributes.overleaf + const project = await _createBlankProject(ownerId, projectName, attributes) + if (isImport) { + AnalyticsManager.recordEvent(ownerId, 'project-imported', { + projectId: project._id, + attributes, + }) + } else { + AnalyticsManager.recordEvent(ownerId, 'project-created', { + projectId: project._id, + attributes, + }) + } + return project +} + +async function createProjectFromSnippet(ownerId, projectName, docLines) { + const project = await _createBlankProject(ownerId, projectName) + AnalyticsManager.recordEvent(ownerId, 'project-created', { + projectId: project._id, + }) + await _createRootDoc(project, ownerId, docLines) + return project +} + +async function createBasicProject(ownerId, projectName) { + const project = await _createBlankProject(ownerId, projectName) + AnalyticsManager.recordEvent(ownerId, 'project-created', { + projectId: project._id, + }) + const docLines = await _buildTemplate('mainbasic.tex', ownerId, projectName) + await _createRootDoc(project, ownerId, docLines) + return project +} + +async function createExampleProject(ownerId, projectName) { + const project = await _createBlankProject(ownerId, projectName) + + const assignment = await SplitTestV2Handler.promises.getAssignment( + ownerId, + EXAMPLE_PROJECT_SPLITTEST_ID + ) + + if (assignment.variant === 'example-frog') { + await _addSplitTestExampleProjectFiles(ownerId, projectName, project) + } else { + await _addDefaultExampleProjectFiles(ownerId, projectName, project) + } + + AnalyticsManager.recordEvent(ownerId, 'project-created', { + projectId: project._id, + ...assignment.analytics.segmentation, + }) + + return project +} + +async function _addDefaultExampleProjectFiles(ownerId, projectName, project) { + const mainDocLines = await _buildTemplate('main.tex', ownerId, projectName) + await _createRootDoc(project, ownerId, mainDocLines) + + const referenceDocLines = await _buildTemplate( + 'references.bib', + ownerId, + projectName + ) + await ProjectEntityUpdateHandler.promises.addDoc( + project._id, + project.rootFolder[0]._id, + 'references.bib', + referenceDocLines, + ownerId + ) + + const universePath = path.resolve( + __dirname + '/../../../templates/project_files/universe.jpg' + ) + await ProjectEntityUpdateHandler.promises.addFile( + project._id, + project.rootFolder[0]._id, + 'universe.jpg', + universePath, + null, + ownerId + ) +} + +async function _addSplitTestExampleProjectFiles(ownerId, projectName, project) { + const mainDocLines = await _buildTemplate( + 'test-example-project/main.tex', + ownerId, + projectName + ) + await _createRootDoc(project, ownerId, mainDocLines) + + const bibDocLines = await _buildTemplate( + 'test-example-project/sample.bib', + ownerId, + projectName + ) + await ProjectEntityUpdateHandler.promises.addDoc( + project._id, + project.rootFolder[0]._id, + 'sample.bib', + bibDocLines, + ownerId + ) + + const frogPath = path.resolve( + __dirname + + '/../../../templates/project_files/test-example-project/frog.jpg' + ) + await ProjectEntityUpdateHandler.promises.addFile( + project._id, + project.rootFolder[0]._id, + 'frog.jpg', + frogPath, + null, + ownerId + ) +} + +async function _createBlankProject(ownerId, projectName, attributes = {}) { + metrics.inc('project-creation') + await ProjectDetailsHandler.promises.validateProjectName(projectName) + + if (!attributes.overleaf) { + const history = await HistoryManager.promises.initializeProject() + attributes.overleaf = { + history: { id: history ? history.overleaf_id : undefined }, + } + } + + const rootFolder = new Folder({ name: 'rootFolder' }) + + attributes.lastUpdatedBy = attributes.owner_ref = new ObjectId(ownerId) + attributes.name = projectName + const project = new Project(attributes) + + Object.assign(project, attributes) + + // only display full project history when the project has the overleaf history id attribute + // (to allow scripted creation of projects without full project history) + const historyId = _.get(attributes, ['overleaf', 'history', 'id']) + if ( + Features.hasFeature('history-v1') && + Settings.apis.project_history.displayHistoryForNewProjects && + historyId + ) { + project.overleaf.history.display = true + } + if (Settings.currentImageName) { + // avoid clobbering any imageName already set in attributes (e.g. importedImageName) + if (!project.imageName) { + project.imageName = Settings.currentImageName + } + } + project.rootFolder[0] = rootFolder + const user = await User.findById(ownerId, 'ace.spellCheckLanguage') + project.spellCheckLanguage = user.ace.spellCheckLanguage + return await project.save() +} + +async function _createRootDoc(project, ownerId, docLines) { + try { + const { doc } = await ProjectEntityUpdateHandler.promises.addDoc( + project._id, + project.rootFolder[0]._id, + 'main.tex', + docLines, + ownerId + ) + await ProjectEntityUpdateHandler.promises.setRootDoc(project._id, doc._id) + } catch (error) { + throw OError.tag(error, 'error adding root doc when creating project') + } +} + +async function _buildTemplate(templateName, userId, projectName) { + const user = await User.findById(userId, 'first_name last_name') + + const templatePath = path.resolve( + __dirname + `/../../../templates/project_files/${templateName}` + ) + const template = fs.readFileSync(templatePath) + const data = { + project_name: projectName, + user, + year: new Date().getUTCFullYear(), + month: MONTH_NAMES[new Date().getUTCMonth()], + } + const output = _.template(template.toString())(data) + return output.split('\n') +} + +module.exports = { + createBlankProject: callbackify(createBlankProject), + createProjectFromSnippet: callbackify(createProjectFromSnippet), + createBasicProject: callbackify(createBasicProject), + createExampleProject: callbackify(createExampleProject), + promises: { + createBlankProject, + createProjectFromSnippet, + createBasicProject, + createExampleProject, + }, +} + +metrics.timeAsyncMethod( + module.exports, + 'createBlankProject', + 'mongo.ProjectCreationHandler', + logger +) diff --git a/services/web/app/src/Features/Project/ProjectDeleter.js b/services/web/app/src/Features/Project/ProjectDeleter.js new file mode 100644 index 0000000000..0800fa6ab9 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectDeleter.js @@ -0,0 +1,436 @@ +const Features = require('../../infrastructure/Features') +const _ = require('lodash') +const { db, ObjectId } = require('../../infrastructure/mongodb') +const { callbackify } = require('util') +const { Project } = require('../../models/Project') +const { DeletedProject } = require('../../models/DeletedProject') +const Errors = require('../Errors/Errors') +const logger = require('logger-sharelatex') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const TagsHandler = require('../Tags/TagsHandler') +const ProjectHelper = require('./ProjectHelper') +const ProjectDetailsHandler = require('./ProjectDetailsHandler') +const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') +const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') +const DocstoreManager = require('../Docstore/DocstoreManager') +const EditorRealTimeController = require('../Editor/EditorRealTimeController') +const HistoryManager = require('../History/HistoryManager') +const FilestoreHandler = require('../FileStore/FileStoreHandler') +const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') +const moment = require('moment') +const { promiseMapWithLimit } = require('../../util/promises') + +const EXPIRE_PROJECTS_AFTER_DAYS = 90 +const PROJECT_EXPIRATION_BATCH_SIZE = 10000 + +module.exports = { + markAsDeletedByExternalSource: callbackify(markAsDeletedByExternalSource), + unmarkAsDeletedByExternalSource: callbackify(unmarkAsDeletedByExternalSource), + deleteUsersProjects: callbackify(deleteUsersProjects), + expireDeletedProjectsAfterDuration: callbackify( + expireDeletedProjectsAfterDuration + ), + restoreProject: callbackify(restoreProject), + archiveProject: callbackify(archiveProject), + unarchiveProject: callbackify(unarchiveProject), + trashProject: callbackify(trashProject), + untrashProject: callbackify(untrashProject), + deleteProject: callbackify(deleteProject), + undeleteProject: callbackify(undeleteProject), + expireDeletedProject: callbackify(expireDeletedProject), + promises: { + archiveProject, + unarchiveProject, + trashProject, + untrashProject, + deleteProject, + undeleteProject, + expireDeletedProject, + markAsDeletedByExternalSource, + unmarkAsDeletedByExternalSource, + deleteUsersProjects, + expireDeletedProjectsAfterDuration, + restoreProject, + }, +} + +async function markAsDeletedByExternalSource(projectId) { + logger.log( + { project_id: projectId }, + 'marking project as deleted by external data source' + ) + await Project.updateOne( + { _id: projectId }, + { deletedByExternalDataSource: true } + ).exec() + EditorRealTimeController.emitToRoom( + projectId, + 'projectRenamedOrDeletedByExternalSource' + ) +} + +async function unmarkAsDeletedByExternalSource(projectId) { + await Project.updateOne( + { _id: projectId }, + { deletedByExternalDataSource: false } + ).exec() +} + +async function deleteUsersProjects(userId) { + const projects = await Project.find({ owner_ref: userId }).exec() + await promiseMapWithLimit(5, projects, project => deleteProject(project._id)) + await CollaboratorsHandler.promises.removeUserFromAllProjects(userId) +} + +async function expireDeletedProjectsAfterDuration() { + const deletedProjects = await DeletedProject.find( + { + 'deleterData.deletedAt': { + $lt: new Date(moment().subtract(EXPIRE_PROJECTS_AFTER_DAYS, 'days')), + }, + project: { $ne: null }, + }, + { 'deleterData.deletedProjectId': 1 } + ) + .limit(PROJECT_EXPIRATION_BATCH_SIZE) + .read('secondary') + const projectIds = _.shuffle( + deletedProjects.map( + deletedProject => deletedProject.deleterData.deletedProjectId + ) + ) + for (const projectId of projectIds) { + await expireDeletedProject(projectId) + } +} + +async function restoreProject(projectId) { + await Project.updateOne( + { _id: projectId }, + { $unset: { archived: true } } + ).exec() +} + +async function archiveProject(projectId, userId) { + try { + const project = await Project.findOne({ _id: projectId }).exec() + if (!project) { + throw new Errors.NotFoundError('project not found') + } + const archived = ProjectHelper.calculateArchivedArray( + project, + userId, + 'ARCHIVE' + ) + + await Project.updateOne( + { _id: projectId }, + { $set: { archived: archived }, $pull: { trashed: ObjectId(userId) } } + ) + } catch (err) { + logger.warn({ err }, 'problem archiving project') + throw err + } +} + +async function unarchiveProject(projectId, userId) { + try { + const project = await Project.findOne({ _id: projectId }).exec() + if (!project) { + throw new Errors.NotFoundError('project not found') + } + + const archived = ProjectHelper.calculateArchivedArray( + project, + userId, + 'UNARCHIVE' + ) + + await Project.updateOne( + { _id: projectId }, + { $set: { archived: archived } } + ) + } catch (err) { + logger.warn({ err }, 'problem unarchiving project') + throw err + } +} + +async function trashProject(projectId, userId) { + try { + const project = await Project.findOne({ _id: projectId }).exec() + if (!project) { + throw new Errors.NotFoundError('project not found') + } + + const archived = ProjectHelper.calculateArchivedArray( + project, + userId, + 'UNARCHIVE' + ) + + await Project.updateOne( + { _id: projectId }, + { + $addToSet: { trashed: ObjectId(userId) }, + $set: { archived: archived }, + } + ) + } catch (err) { + logger.warn({ err }, 'problem trashing project') + throw err + } +} + +async function untrashProject(projectId, userId) { + try { + const project = await Project.findOne({ _id: projectId }).exec() + if (!project) { + throw new Errors.NotFoundError('project not found') + } + + await Project.updateOne( + { _id: projectId }, + { $pull: { trashed: ObjectId(userId) } } + ) + } catch (err) { + logger.warn({ err }, 'problem untrashing project') + throw err + } +} + +async function deleteProject(projectId, options = {}) { + try { + const project = await Project.findOne({ _id: projectId }).exec() + if (!project) { + throw new Errors.NotFoundError('project not found') + } + + await DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete( + projectId + ) + + try { + // OPTIMIZATION: flush docs out of mongo + await DocstoreManager.promises.archiveProject(projectId) + } catch (err) { + // It is OK to fail here, the docs will get hard-deleted eventually after + // the grace-period for soft-deleted projects has passed. + logger.warn( + { projectId, err }, + 'failed archiving doc via docstore as part of project soft-deletion' + ) + } + + const memberIds = await CollaboratorsGetter.promises.getMemberIds(projectId) + + // fire these jobs in the background + for (const memberId of memberIds) { + TagsHandler.promises + .removeProjectFromAllTags(memberId, projectId) + .catch(err => { + logger.err( + { err, memberId, projectId }, + 'failed to remove project from tags' + ) + }) + } + + const deleterData = { + deletedAt: new Date(), + deleterId: + options.deleterUser != null ? options.deleterUser._id : undefined, + deleterIpAddress: options.ipAddress, + deletedProjectId: project._id, + deletedProjectOwnerId: project.owner_ref, + deletedProjectCollaboratorIds: project.collaberator_refs, + deletedProjectReadOnlyIds: project.readOnly_refs, + deletedProjectReadWriteTokenAccessIds: + project.tokenAccessReadAndWrite_refs, + deletedProjectOverleafId: project.overleaf + ? project.overleaf.id + : undefined, + deletedProjectOverleafHistoryId: + project.overleaf && project.overleaf.history + ? project.overleaf.history.id + : undefined, + deletedProjectReadOnlyTokenAccessIds: project.tokenAccessReadOnly_refs, + deletedProjectReadWriteToken: project.tokens.readAndWrite, + deletedProjectReadOnlyToken: project.tokens.readOnly, + deletedProjectLastUpdatedAt: project.lastUpdated, + } + + Object.keys(deleterData).forEach(key => + deleterData[key] === undefined ? delete deleterData[key] : '' + ) + + await DeletedProject.updateOne( + { 'deleterData.deletedProjectId': projectId }, + { project, deleterData }, + { upsert: true } + ) + + await Project.deleteOne({ _id: projectId }).exec() + } catch (err) { + logger.warn({ err }, 'problem deleting project') + throw err + } + + logger.log({ project_id: projectId }, 'successfully deleted project') +} + +async function undeleteProject(projectId, options = {}) { + projectId = ObjectId(projectId) + const deletedProject = await DeletedProject.findOne({ + 'deleterData.deletedProjectId': projectId, + }).exec() + + if (!deletedProject) { + throw new Errors.NotFoundError('project_not_found') + } + + if (!deletedProject.project) { + throw new Errors.NotFoundError('project_too_old_to_restore') + } + + const restored = new Project(deletedProject.project) + + if (options.userId) { + restored.owner_ref = options.userId + } + + // if we're undeleting, we want the document to show up + restored.name = await ProjectDetailsHandler.promises.generateUniqueName( + deletedProject.deleterData.deletedProjectOwnerId, + restored.name + ' (Restored)' + ) + restored.archived = undefined + + if (restored.deletedDocs && restored.deletedDocs.length > 0) { + await promiseMapWithLimit(10, restored.deletedDocs, async deletedDoc => { + // back fill context of deleted docs + const { _id: docId, name, deletedAt } = deletedDoc + await DocstoreManager.promises.deleteDoc( + projectId, + docId, + name, + deletedAt + ) + }) + restored.deletedDocs = [] + } + if (restored.deletedFiles && restored.deletedFiles.length > 0) { + filterDuplicateDeletedFilesInPlace(restored) + const deletedFiles = restored.deletedFiles.map(file => { + // break free from the model + file = file.toObject() + + // add projectId + file.projectId = projectId + return file + }) + await db.deletedFiles.insertMany(deletedFiles) + restored.deletedFiles = [] + } + + // we can't use Mongoose to re-insert the project, as it won't + // create a new document with an _id already specified. We need to + // insert it directly into the collection + + await db.projects.insertOne(restored) + await DeletedProject.deleteOne({ _id: deletedProject._id }).exec() +} + +async function expireDeletedProject(projectId) { + try { + const activeProject = await Project.findById(projectId).exec() + if (activeProject) { + // That project is active. The deleted project record might be there + // because of an incomplete delete or undelete operation. Clean it up and + // return. + await DeletedProject.deleteOne({ + 'deleterData.deletedProjectId': projectId, + }) + return + } + const deletedProject = await DeletedProject.findOne({ + 'deleterData.deletedProjectId': projectId, + }).exec() + if (!deletedProject) { + throw new Errors.NotFoundError( + `No deleted project found for project id ${projectId}` + ) + } + if (!deletedProject.project) { + logger.warn( + { projectId }, + `Attempted to expire already-expired deletedProject` + ) + return + } + + const historyId = + deletedProject.project.overleaf && + deletedProject.project.overleaf.history && + deletedProject.project.overleaf.history.id + + await Promise.all([ + DocstoreManager.promises.destroyProject(deletedProject.project._id), + Features.hasFeature('history-v1') + ? HistoryManager.promises.deleteProject( + deletedProject.project._id, + historyId + ) + : Promise.resolve(), + FilestoreHandler.promises.deleteProject(deletedProject.project._id), + TpdsUpdateSender.promises.deleteProject({ + project_id: deletedProject.project._id, + }), + hardDeleteDeletedFiles(deletedProject.project._id), + ]) + + await DeletedProject.updateOne( + { + _id: deletedProject._id, + }, + { + $set: { + 'deleterData.deleterIpAddress': null, + project: null, + }, + } + ).exec() + } catch (error) { + logger.warn({ projectId, error }, 'error expiring deleted project') + throw error + } +} + +function filterDuplicateDeletedFilesInPlace(project) { + const fileIds = new Set() + project.deletedFiles = project.deletedFiles.filter(file => { + const id = file._id.toString() + if (fileIds.has(id)) return false + fileIds.add(id) + return true + }) +} + +let deletedFilesProjectIdIndexExist +async function doesDeletedFilesProjectIdIndexExist() { + if (typeof deletedFilesProjectIdIndexExist !== 'boolean') { + // Resolve this about once. No need for locking or retry handling. + deletedFilesProjectIdIndexExist = await db.deletedFiles.indexExists( + 'projectId_1' + ) + } + return deletedFilesProjectIdIndexExist +} + +async function hardDeleteDeletedFiles(projectId) { + if (!(await doesDeletedFilesProjectIdIndexExist())) { + // Running the deletion command w/o index would kill mongo performance + return + } + return db.deletedFiles.deleteMany({ projectId }) +} diff --git a/services/web/app/src/Features/Project/ProjectDetailsHandler.js b/services/web/app/src/Features/Project/ProjectDetailsHandler.js new file mode 100644 index 0000000000..a52be57439 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectDetailsHandler.js @@ -0,0 +1,246 @@ +const _ = require('underscore') +const ProjectGetter = require('./ProjectGetter') +const UserGetter = require('../User/UserGetter') +const { Project } = require('../../models/Project') +const logger = require('logger-sharelatex') +const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') +const PublicAccessLevels = require('../Authorization/PublicAccessLevels') +const Errors = require('../Errors/Errors') +const TokenGenerator = require('../TokenGenerator/TokenGenerator') +const ProjectHelper = require('./ProjectHelper') +const settings = require('@overleaf/settings') +const { callbackify } = require('util') + +const MAX_PROJECT_NAME_LENGTH = 150 + +module.exports = { + MAX_PROJECT_NAME_LENGTH, + getDetails: callbackify(getDetails), + getProjectDescription: callbackify(getProjectDescription), + setProjectDescription: callbackify(setProjectDescription), + renameProject: callbackify(renameProject), + validateProjectName: callbackify(validateProjectName), + generateUniqueName: callbackify(generateUniqueName), + setPublicAccessLevel: callbackify(setPublicAccessLevel), + ensureTokensArePresent: callbackify(ensureTokensArePresent), + clearTokens: callbackify(clearTokens), + fixProjectName, + promises: { + getDetails, + getProjectDescription, + setProjectDescription, + renameProject, + validateProjectName, + generateUniqueName, + setPublicAccessLevel, + ensureTokensArePresent, + clearTokens, + }, +} + +async function getDetails(projectId) { + let project + try { + project = await ProjectGetter.promises.getProject(projectId, { + name: true, + description: true, + compiler: true, + features: true, + owner_ref: true, + overleaf: true, + }) + } catch (err) { + logger.warn({ err, projectId }, 'error getting project') + throw err + } + if (project == null) { + throw new Errors.NotFoundError('project not found') + } + const user = await UserGetter.promises.getUser(project.owner_ref) + const details = { + name: project.name, + description: project.description, + compiler: project.compiler, + features: + user != null && user.features != null + ? user.features + : settings.defaultFeatures, + } + if (project.overleaf != null) { + details.overleaf = project.overleaf + } + return details +} + +async function getProjectDescription(projectId) { + const project = await ProjectGetter.promises.getProject(projectId, { + description: true, + }) + if (project == null) { + return undefined + } + return project.description +} + +async function setProjectDescription(projectId, description) { + const conditions = { _id: projectId } + const update = { description } + logger.log( + { conditions, update, projectId, description }, + 'setting project description' + ) + try { + await Project.updateOne(conditions, update).exec() + } catch (err) { + logger.warn({ err }, 'something went wrong setting project description') + throw err + } +} +async function renameProject(projectId, newName) { + await validateProjectName(newName) + logger.log({ projectId, newName }, 'renaming project') + let project + try { + project = await ProjectGetter.promises.getProject(projectId, { name: true }) + } catch (err) { + logger.warn({ err, projectId }, 'error getting project') + throw err + } + if (project == null) { + logger.warn({ projectId }, 'could not find project to rename') + return + } + const oldProjectName = project.name + await Project.updateOne({ _id: projectId }, { name: newName }).exec() + await TpdsUpdateSender.promises.moveEntity({ + project_id: projectId, + project_name: oldProjectName, + newProjectName: newName, + }) +} + +async function validateProjectName(name) { + if (name == null || name.length === 0) { + throw new Errors.InvalidNameError('Project name cannot be blank') + } + if (name.length > MAX_PROJECT_NAME_LENGTH) { + throw new Errors.InvalidNameError('Project name is too long') + } + if (name.indexOf('/') > -1) { + throw new Errors.InvalidNameError( + 'Project name cannot contain / characters' + ) + } + if (name.indexOf('\\') > -1) { + throw new Errors.InvalidNameError( + 'Project name cannot contain \\ characters' + ) + } +} + +// FIXME: we should put a lock around this to make it completely safe, but we would need to do that at +// the point of project creation, rather than just checking the name at the start of the import. +// If we later move this check into ProjectCreationHandler we can ensure all new projects are created +// with a unique name. But that requires thinking through how we would handle incoming projects from +// dropbox for example. +async function generateUniqueName(userId, name, suffixes = []) { + const allUsersProjectNames = await ProjectGetter.promises.findAllUsersProjects( + userId, + { name: 1 } + ) + // allUsersProjectNames is returned as a hash {owned: [name1, name2, ...], readOnly: [....]} + // collect all of the names and flatten them into a single array + const projectNameList = _.pluck( + _.flatten(_.values(allUsersProjectNames)), + 'name' + ) + const uniqueName = await ProjectHelper.promises.ensureNameIsUnique( + projectNameList, + name, + suffixes, + MAX_PROJECT_NAME_LENGTH + ) + return uniqueName +} + +function fixProjectName(name) { + if (name === '' || !name) { + name = 'Untitled' + } + if (name.indexOf('/') > -1) { + // v2 does not allow / in a project name + name = name.replace(/\//g, '-') + } + if (name.indexOf('\\') > -1) { + // backslashes in project name will prevent syncing to dropbox + name = name.replace(/\\/g, '') + } + if (name.length > MAX_PROJECT_NAME_LENGTH) { + name = name.substr(0, MAX_PROJECT_NAME_LENGTH) + } + return name +} + +async function setPublicAccessLevel(projectId, newAccessLevel) { + // DEPRECATED: `READ_ONLY` and `READ_AND_WRITE` are still valid in, but should no longer + // be passed here. Remove after token-based access has been live for a while + if ( + projectId != null && + newAccessLevel != null && + _.include( + [ + PublicAccessLevels.READ_ONLY, + PublicAccessLevels.READ_AND_WRITE, + PublicAccessLevels.PRIVATE, + PublicAccessLevels.TOKEN_BASED, + ], + newAccessLevel + ) + ) { + await Project.updateOne( + { _id: projectId }, + { publicAccesLevel: newAccessLevel } + ).exec() + } +} + +async function ensureTokensArePresent(projectId) { + const project = await ProjectGetter.promises.getProject(projectId, { + tokens: 1, + }) + if ( + project.tokens != null && + project.tokens.readOnly != null && + project.tokens.readAndWrite != null + ) { + return project.tokens + } + await _generateTokens(project) + await Project.updateOne( + { _id: projectId }, + { $set: { tokens: project.tokens } } + ).exec() + return project.tokens +} + +async function clearTokens(projectId) { + await Project.updateOne( + { _id: projectId }, + { $unset: { tokens: 1 }, $set: { publicAccesLevel: 'private' } } + ).exec() +} + +async function _generateTokens(project, callback) { + if (!project.tokens) { + project.tokens = {} + } + const { tokens } = project + if (tokens.readAndWrite == null) { + const { token, numericPrefix } = TokenGenerator.readAndWriteToken() + tokens.readAndWrite = token + tokens.readAndWritePrefix = numericPrefix + } + if (tokens.readOnly == null) { + tokens.readOnly = await TokenGenerator.promises.generateUniqueReadOnlyToken() + } +} diff --git a/services/web/app/src/Features/Project/ProjectDuplicator.js b/services/web/app/src/Features/Project/ProjectDuplicator.js new file mode 100644 index 0000000000..d68b550788 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectDuplicator.js @@ -0,0 +1,198 @@ +const { callbackify } = require('util') +const Path = require('path') +const OError = require('@overleaf/o-error') +const { promiseMapWithLimit } = require('../../util/promises') +const { Doc } = require('../../models/Doc') +const { File } = require('../../models/File') +const DocstoreManager = require('../Docstore/DocstoreManager') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const FileStoreHandler = require('../FileStore/FileStoreHandler') +const ProjectCreationHandler = require('./ProjectCreationHandler') +const ProjectDeleter = require('./ProjectDeleter') +const ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler') +const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler') +const ProjectGetter = require('./ProjectGetter') +const ProjectLocator = require('./ProjectLocator') +const ProjectOptionsHandler = require('./ProjectOptionsHandler') +const SafePath = require('./SafePath') +const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher') + +module.exports = { + duplicate: callbackify(duplicate), + promises: { + duplicate, + }, +} + +async function duplicate(owner, originalProjectId, newProjectName) { + await DocumentUpdaterHandler.promises.flushProjectToMongo(originalProjectId) + const originalProject = await ProjectGetter.promises.getProject( + originalProjectId, + { + compiler: true, + rootFolder: true, + rootDoc_id: true, + } + ) + const { path: rootDocPath } = await ProjectLocator.promises.findRootDoc({ + project_id: originalProjectId, + }) + + const originalEntries = _getFolderEntries(originalProject.rootFolder[0]) + + // Now create the new project, cleaning it up on failure if necessary + const newProject = await ProjectCreationHandler.promises.createBlankProject( + owner._id, + newProjectName + ) + + try { + await ProjectOptionsHandler.promises.setCompiler( + newProject._id, + originalProject.compiler + ) + const [docEntries, fileEntries] = await Promise.all([ + _copyDocs(originalEntries.docEntries, originalProject, newProject), + _copyFiles(originalEntries.fileEntries, originalProject, newProject), + ]) + const projectVersion = await ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure( + newProject._id, + docEntries, + fileEntries + ) + // Silently ignore the rootDoc in case it's not valid per the new limits. + if ( + rootDocPath && + ProjectEntityUpdateHandler.isPathValidForRootDoc(rootDocPath.fileSystem) + ) { + await _setRootDoc(newProject._id, rootDocPath.fileSystem) + } + await _notifyDocumentUpdater(newProject, owner._id, { + newFiles: fileEntries, + newDocs: docEntries, + newProject: { version: projectVersion }, + }) + await TpdsProjectFlusher.promises.flushProjectToTpds(newProject._id) + } catch (err) { + // Clean up broken clone on error. + // Make sure we delete the new failed project, not the original one! + await ProjectDeleter.promises.deleteProject(newProject._id) + throw OError.tag(err, 'error cloning project, broken clone deleted', { + originalProjectId, + newProjectName, + newProjectId: newProject._id, + }) + } + return newProject +} + +function _getFolderEntries(folder, folderPath = '/') { + const docEntries = [] + const fileEntries = [] + const docs = folder.docs || [] + const files = folder.fileRefs || [] + const subfolders = folder.folders || [] + + for (const doc of docs) { + if (doc == null || doc._id == null) { + continue + } + const path = Path.join(folderPath, doc.name) + docEntries.push({ doc, path }) + } + + for (const file of files) { + if (file == null || file._id == null) { + continue + } + const path = Path.join(folderPath, file.name) + fileEntries.push({ file, path }) + } + + for (const subfolder of subfolders) { + if (subfolder == null || subfolder._id == null) { + continue + } + const subfolderPath = Path.join(folderPath, subfolder.name) + const subfolderEntries = _getFolderEntries(subfolder, subfolderPath) + for (const docEntry of subfolderEntries.docEntries) { + docEntries.push(docEntry) + } + for (const fileEntry of subfolderEntries.fileEntries) { + fileEntries.push(fileEntry) + } + } + return { docEntries, fileEntries } +} + +async function _copyDocs(sourceEntries, sourceProject, targetProject) { + const docLinesById = await _getDocLinesForProject(sourceProject._id) + const targetEntries = [] + for (const sourceEntry of sourceEntries) { + const sourceDoc = sourceEntry.doc + const path = sourceEntry.path + const doc = new Doc({ name: sourceDoc.name }) + const docLines = docLinesById.get(sourceDoc._id.toString()) + await DocstoreManager.promises.updateDoc( + targetProject._id.toString(), + doc._id.toString(), + docLines, + 0, + {} + ) + targetEntries.push({ doc, path, docLines: docLines.join('\n') }) + } + return targetEntries +} + +async function _getDocLinesForProject(projectId) { + const docs = await DocstoreManager.promises.getAllDocs(projectId) + const docLinesById = new Map(docs.map(doc => [doc._id, doc.lines])) + return docLinesById +} + +async function _copyFiles(sourceEntries, sourceProject, targetProject) { + const targetEntries = await promiseMapWithLimit( + 5, + sourceEntries, + async sourceEntry => { + const sourceFile = sourceEntry.file + const path = sourceEntry.path + const file = new File({ name: SafePath.clean(sourceFile.name) }) + if (sourceFile.linkedFileData != null) { + file.linkedFileData = sourceFile.linkedFileData + } + if (sourceFile.hash != null) { + file.hash = sourceFile.hash + } + const url = await FileStoreHandler.promises.copyFile( + sourceProject._id, + sourceFile._id, + targetProject._id, + file._id + ) + return { file, path, url } + } + ) + return targetEntries +} + +async function _setRootDoc(projectId, path) { + const { element: rootDoc } = await ProjectLocator.promises.findElementByPath({ + project_id: projectId, + path, + exactCaseMatch: true, + }) + await ProjectEntityUpdateHandler.promises.setRootDoc(projectId, rootDoc._id) +} + +async function _notifyDocumentUpdater(project, userId, changes) { + const projectHistoryId = + project.overleaf && project.overleaf.history && project.overleaf.history.id + await DocumentUpdaterHandler.promises.updateProjectStructure( + project._id, + projectHistoryId, + userId, + changes + ) +} diff --git a/services/web/app/src/Features/Project/ProjectEditorHandler.js b/services/web/app/src/Features/Project/ProjectEditorHandler.js new file mode 100644 index 0000000000..3645e11dff --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectEditorHandler.js @@ -0,0 +1,168 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectEditorHandler +const _ = require('underscore') +const Path = require('path') + +function mergeDeletedDocs(a, b) { + const docIdsInA = new Set(a.map(doc => doc._id.toString())) + return a.concat(b.filter(doc => !docIdsInA.has(doc._id.toString()))) +} + +module.exports = ProjectEditorHandler = { + trackChangesAvailable: false, + + buildProjectModelView(project, members, invites, deletedDocsFromDocstore) { + let owner, ownerFeatures + if (!Array.isArray(project.deletedDocs)) { + project.deletedDocs = [] + } + project.deletedDocs.forEach(doc => { + // The frontend does not use this field. + delete doc.deletedAt + }) + const result = { + _id: project._id, + name: project.name, + rootDoc_id: project.rootDoc_id, + rootFolder: [this.buildFolderModelView(project.rootFolder[0])], + publicAccesLevel: project.publicAccesLevel, + dropboxEnabled: !!project.existsInDropbox, + compiler: project.compiler, + description: project.description, + spellCheckLanguage: project.spellCheckLanguage, + deletedByExternalDataSource: project.deletedByExternalDataSource || false, + deletedDocs: mergeDeletedDocs( + project.deletedDocs, + deletedDocsFromDocstore + ), + members: [], + invites, + tokens: project.tokens, + imageName: + project.imageName != null + ? Path.basename(project.imageName) + : undefined, + } + + if (result.invites == null) { + result.invites = [] + } + result.invites.forEach(invite => { + delete invite.token + }) + ;({ owner, ownerFeatures, members } = this.buildOwnerAndMembersViews( + members + )) + result.owner = owner + result.members = members + + result.features = _.defaults(ownerFeatures || {}, { + collaborators: -1, // Infinite + versioning: false, + dropbox: false, + compileTimeout: 60, + compileGroup: 'standard', + templates: false, + references: false, + referencesSearch: false, + mendeley: false, + trackChanges: false, + trackChangesVisible: ProjectEditorHandler.trackChangesAvailable, + }) + + if (result.features.trackChanges) { + result.trackChangesState = project.track_changes || false + } + + // Originally these two feature flags were both signalled by the now-deprecated `references` flag. + // For older users, the presence of the `references` feature flag should still turn on these features. + result.features.referencesSearch = + result.features.referencesSearch || result.features.references + result.features.mendeley = + result.features.mendeley || result.features.references + + return result + }, + + buildOwnerAndMembersViews(members) { + let owner = null + let ownerFeatures = null + const filteredMembers = [] + for (const member of Array.from(members || [])) { + if (member.privilegeLevel === 'owner') { + ownerFeatures = member.user.features + owner = this.buildUserModelView(member.user, 'owner') + } else { + filteredMembers.push( + this.buildUserModelView(member.user, member.privilegeLevel) + ) + } + } + return { + owner, + ownerFeatures, + members: filteredMembers, + } + }, + + buildUserModelView(user, privileges) { + return { + _id: user._id, + first_name: user.first_name, + last_name: user.last_name, + email: user.email, + privileges, + signUpDate: user.signUpDate, + } + }, + + buildFolderModelView(folder) { + let file + const fileRefs = _.filter(folder.fileRefs || [], file => file != null) + return { + _id: folder._id, + name: folder.name, + folders: Array.from(folder.folders || []).map(childFolder => + this.buildFolderModelView(childFolder) + ), + fileRefs: (() => { + const result = [] + for (file of Array.from(fileRefs)) { + result.push(this.buildFileModelView(file)) + } + return result + })(), + docs: Array.from(folder.docs || []).map(doc => + this.buildDocModelView(doc) + ), + } + }, + + buildFileModelView(file) { + return { + _id: file._id, + name: file.name, + linkedFileData: file.linkedFileData, + created: file.created, + } + }, + + buildDocModelView(doc) { + return { + _id: doc._id, + name: doc.name, + } + }, +} diff --git a/services/web/app/src/Features/Project/ProjectEntityHandler.js b/services/web/app/src/Features/Project/ProjectEntityHandler.js new file mode 100644 index 0000000000..fb219382ee --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectEntityHandler.js @@ -0,0 +1,251 @@ +const path = require('path') +const DocstoreManager = require('../Docstore/DocstoreManager') +const Errors = require('../Errors/Errors') +const ProjectGetter = require('./ProjectGetter') +const { promisifyAll } = require('../../util/promises') + +const ProjectEntityHandler = { + getAllDocs(projectId, callback) { + // We get the path and name info from the project, and the lines and + // version info from the doc store. + DocstoreManager.getAllDocs(projectId, (error, docContentsArray) => { + if (error != null) { + return callback(error) + } + + // Turn array from docstore into a dictionary based on doc id + const docContents = {} + for (const docContent of docContentsArray) { + docContents[docContent._id] = docContent + } + + ProjectEntityHandler._getAllFolders(projectId, (error, folders) => { + if (folders == null) { + folders = {} + } + if (error != null) { + return callback(error) + } + const docs = {} + for (const folderPath in folders) { + const folder = folders[folderPath] + for (const doc of folder.docs || []) { + const content = docContents[doc._id.toString()] + if (content != null) { + docs[path.join(folderPath, doc.name)] = { + _id: doc._id, + name: doc.name, + lines: content.lines, + rev: content.rev, + } + } + } + } + + callback(null, docs) + }) + }) + }, + + getAllFiles(projectId, callback) { + ProjectEntityHandler._getAllFolders(projectId, (err, folders) => { + if (folders == null) { + folders = {} + } + if (err != null) { + return callback(err) + } + const files = {} + for (const folderPath in folders) { + const folder = folders[folderPath] + for (const file of folder.fileRefs || []) { + if (file != null) { + files[path.join(folderPath, file.name)] = file + } + } + } + callback(null, files) + }) + }, + + getAllEntities(projectId, callback) { + ProjectGetter.getProject(projectId, (err, project) => { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(new Errors.NotFoundError('project not found')) + } + + ProjectEntityHandler.getAllEntitiesFromProject(project, callback) + }) + }, + + getAllEntitiesFromProject(project, callback) { + ProjectEntityHandler._getAllFoldersFromProject(project, (err, folders) => { + if (folders == null) { + folders = {} + } + if (err != null) { + return callback(err) + } + const docs = [] + const files = [] + for (const folderPath in folders) { + const folder = folders[folderPath] + for (const doc of folder.docs || []) { + if (doc != null) { + docs.push({ path: path.join(folderPath, doc.name), doc }) + } + } + for (const file of folder.fileRefs || []) { + if (file != null) { + files.push({ path: path.join(folderPath, file.name), file }) + } + } + } + callback(null, docs, files) + }) + }, + + getAllDocPathsFromProjectById(projectId, callback) { + ProjectGetter.getProjectWithoutDocLines(projectId, (err, project) => { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(Errors.NotFoundError('no project')) + } + ProjectEntityHandler.getAllDocPathsFromProject(project, callback) + }) + }, + + getAllDocPathsFromProject(project, callback) { + ProjectEntityHandler._getAllFoldersFromProject(project, (err, folders) => { + if (folders == null) { + folders = {} + } + if (err != null) { + return callback(err) + } + const docPath = {} + for (const folderPath in folders) { + const folder = folders[folderPath] + for (const doc of folder.docs || []) { + docPath[doc._id] = path.join(folderPath, doc.name) + } + } + callback(null, docPath) + }) + }, + + getDoc(projectId, docId, options, callback) { + if (options == null) { + options = {} + } + if (typeof options === 'function') { + callback = options + options = {} + } + + DocstoreManager.getDoc(projectId, docId, options, callback) + }, + + /** + * @param {ObjectID | string} projectId + * @param {ObjectID | string} docId + * @param {Function} callback + */ + getDocPathByProjectIdAndDocId(projectId, docId, callback) { + ProjectGetter.getProjectWithoutDocLines(projectId, (err, project) => { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(new Errors.NotFoundError('no project')) + } + ProjectEntityHandler.getDocPathFromProjectByDocId( + project, + docId, + (err, docPath) => { + if (err) return callback(Errors.OError.tag(err)) + if (docPath == null) { + return callback(new Errors.NotFoundError('no doc')) + } + callback(null, docPath) + } + ) + }) + }, + + /** + * @param {Project} project + * @param {ObjectID | string} docId + * @param {Function} callback + */ + getDocPathFromProjectByDocId(project, docId, callback) { + function recursivelyFindDocInFolder(basePath, docId, folder) { + const docInCurrentFolder = (folder.docs || []).find( + currentDoc => currentDoc._id.toString() === docId.toString() + ) + if (docInCurrentFolder != null) { + return path.join(basePath, docInCurrentFolder.name) + } else { + let docPath, childFolder + for (childFolder of folder.folders || []) { + docPath = recursivelyFindDocInFolder( + path.join(basePath, childFolder.name), + docId, + childFolder + ) + if (docPath != null) { + return docPath + } + } + return null + } + } + const docPath = recursivelyFindDocInFolder( + '/', + docId, + project.rootFolder[0] + ) + callback(null, docPath) + }, + + _getAllFolders(projectId, callback) { + ProjectGetter.getProjectWithoutDocLines(projectId, (err, project) => { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(new Errors.NotFoundError('no project')) + } + ProjectEntityHandler._getAllFoldersFromProject(project, callback) + }) + }, + + _getAllFoldersFromProject(project, callback) { + const folders = {} + function processFolder(basePath, folder) { + folders[basePath] = folder + for (const childFolder of folder.folders || []) { + if (childFolder.name != null) { + processFolder(path.join(basePath, childFolder.name), childFolder) + } + } + } + + processFolder('/', project.rootFolder[0]) + callback(null, folders) + }, +} + +module.exports = ProjectEntityHandler +module.exports.promises = promisifyAll(ProjectEntityHandler, { + multiResult: { + getAllEntities: ['docs', 'files'], + getAllEntitiesFromProject: ['docs', 'files'], + getDoc: ['lines', 'rev', 'version', 'ranges'], + }, +}) diff --git a/services/web/app/src/Features/Project/ProjectEntityMongoUpdateHandler.js b/services/web/app/src/Features/Project/ProjectEntityMongoUpdateHandler.js new file mode 100644 index 0000000000..461cd726e3 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectEntityMongoUpdateHandler.js @@ -0,0 +1,698 @@ +const { callbackify } = require('util') +const { callbackifyMultiResult } = require('../../util/promises') +const _ = require('underscore') +const logger = require('logger-sharelatex') +const path = require('path') +const { ObjectId } = require('mongodb') +const Settings = require('@overleaf/settings') +const OError = require('@overleaf/o-error') +const CooldownManager = require('../Cooldown/CooldownManager') +const Errors = require('../Errors/Errors') +const { Folder } = require('../../models/Folder') +const LockManager = require('../../infrastructure/LockManager') +const { Project } = require('../../models/Project') +const ProjectEntityHandler = require('./ProjectEntityHandler') +const ProjectGetter = require('./ProjectGetter') +const ProjectLocator = require('./ProjectLocator') +const FolderStructureBuilder = require('./FolderStructureBuilder') +const SafePath = require('./SafePath') +const { DeletedFile } = require('../../models/DeletedFile') + +const LOCK_NAMESPACE = 'mongoTransaction' +const ENTITY_TYPE_TO_MONGO_PATH_SEGMENT = { + doc: 'docs', + docs: 'docs', + file: 'fileRefs', + files: 'fileRefs', + fileRefs: 'fileRefs', + folder: 'folders', + folders: 'folders', +} + +module.exports = { + LOCK_NAMESPACE, + addDoc: callbackifyMultiResult(wrapWithLock(addDoc), ['result', 'project']), + addFile: callbackifyMultiResult(wrapWithLock(addFile), ['result', 'project']), + addFolder: callbackifyMultiResult(wrapWithLock(addFolder), [ + 'folder', + 'parentFolderId', + ]), + replaceFileWithNew: callbackifyMultiResult(wrapWithLock(replaceFileWithNew), [ + 'oldFileRef', + 'project', + 'path', + 'newProject', + ]), + replaceDocWithFile: callbackify(replaceDocWithFile), + replaceFileWithDoc: callbackify(replaceFileWithDoc), + mkdirp: callbackifyMultiResult(wrapWithLock(mkdirp), [ + 'newFolders', + 'folder', + ]), + moveEntity: callbackifyMultiResult(wrapWithLock(moveEntity), [ + 'project', + 'startPath', + 'endPath', + 'rev', + 'changes', + ]), + deleteEntity: callbackifyMultiResult(wrapWithLock(deleteEntity), [ + 'entity', + 'path', + 'projectBeforeDeletion', + 'newProject', + ]), + renameEntity: callbackifyMultiResult(wrapWithLock(renameEntity), [ + 'project', + 'startPath', + 'endPath', + 'rev', + 'changes', + ]), + createNewFolderStructure: callbackify(wrapWithLock(createNewFolderStructure)), + _insertDeletedFileReference: callbackify(_insertDeletedFileReference), + _putElement: callbackifyMultiResult(_putElement, ['result', 'project']), + _confirmFolder, + promises: { + addDoc: wrapWithLock(addDoc), + addFile: wrapWithLock(addFile), + addFolder: wrapWithLock(addFolder), + replaceFileWithNew: wrapWithLock(replaceFileWithNew), + replaceDocWithFile: wrapWithLock(replaceDocWithFile), + replaceFileWithDoc: wrapWithLock(replaceFileWithDoc), + mkdirp: wrapWithLock(mkdirp), + moveEntity: wrapWithLock(moveEntity), + deleteEntity: wrapWithLock(deleteEntity), + renameEntity: wrapWithLock(renameEntity), + createNewFolderStructure: wrapWithLock(createNewFolderStructure), + _insertDeletedFileReference, + _putElement, + }, +} + +function wrapWithLock(methodWithoutLock) { + // This lock is used whenever we read or write to an existing project's + // structure. Some operations to project structure cannot be done atomically + // in mongo, this lock is used to prevent reading the structure between two + // parts of a staged update. + async function methodWithLock(projectId, ...rest) { + return LockManager.promises.runWithLock(LOCK_NAMESPACE, projectId, () => + methodWithoutLock(projectId, ...rest) + ) + } + return methodWithLock +} + +async function addDoc(projectId, folderId, doc) { + const project = await ProjectGetter.promises.getProjectWithoutLock( + projectId, + { + rootFolder: true, + name: true, + overleaf: true, + } + ) + folderId = _confirmFolder(project, folderId) + const { result, project: newProject } = await _putElement( + project, + folderId, + doc, + 'doc' + ) + return { result, project: newProject } +} + +async function addFile(projectId, folderId, fileRef) { + const project = await ProjectGetter.promises.getProjectWithoutLock( + projectId, + { rootFolder: true, name: true, overleaf: true } + ) + folderId = _confirmFolder(project, folderId) + const { result, project: newProject } = await _putElement( + project, + folderId, + fileRef, + 'file' + ) + return { result, project: newProject } +} + +async function addFolder(projectId, parentFolderId, folderName) { + const project = await ProjectGetter.promises.getProjectWithoutLock( + projectId, + { rootFolder: true, name: true, overleaf: true } + ) + parentFolderId = _confirmFolder(project, parentFolderId) + const folder = new Folder({ name: folderName }) + await _putElement(project, parentFolderId, folder, 'folder') + return { folder, parentFolderId } +} + +async function replaceFileWithNew(projectId, fileId, newFileRef) { + const project = await ProjectGetter.promises.getProjectWithoutLock( + projectId, + { rootFolder: true, name: true, overleaf: true } + ) + const { element: fileRef, path } = await ProjectLocator.promises.findElement({ + project, + element_id: fileId, + type: 'file', + }) + await _insertDeletedFileReference(projectId, fileRef) + const newProject = await Project.findOneAndUpdate( + { _id: project._id }, + { + $set: { + [`${path.mongo}._id`]: newFileRef._id, + [`${path.mongo}.created`]: new Date(), + [`${path.mongo}.linkedFileData`]: newFileRef.linkedFileData, + [`${path.mongo}.hash`]: newFileRef.hash, + }, + $inc: { + version: 1, + [`${path.mongo}.rev`]: 1, + }, + }, + { new: true } + ).exec() + // Note: Mongoose uses new:true to return the modified document + // https://mongoosejs.com/docs/api.html#model_Model.findOneAndUpdate + // but Mongo uses returnNewDocument:true instead + // https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndUpdate/ + // We are using Mongoose here, but if we ever switch to a direct mongo call + // the next line will need to be updated. + return { oldFileRef: fileRef, project, path, newProject } +} + +async function replaceDocWithFile(projectId, docId, fileRef) { + const project = await ProjectGetter.promises.getProjectWithoutLock( + projectId, + { rootFolder: true, name: true, overleaf: true } + ) + const { path } = await ProjectLocator.promises.findElement({ + project, + element_id: docId, + type: 'doc', + }) + const folderMongoPath = _getParentMongoPath(path.mongo) + const newProject = await Project.findOneAndUpdate( + { _id: project._id }, + { + $pull: { + [`${folderMongoPath}.docs`]: { _id: docId }, + }, + $push: { + [`${folderMongoPath}.fileRefs`]: fileRef, + }, + $inc: { version: 1 }, + }, + { new: true } + ).exec() + return newProject +} + +async function replaceFileWithDoc(projectId, fileId, newDoc) { + const project = await ProjectGetter.promises.getProjectWithoutLock( + projectId, + { rootFolder: true, name: true, overleaf: true } + ) + const { path } = await ProjectLocator.promises.findElement({ + project, + element_id: fileId, + type: 'file', + }) + const folderMongoPath = _getParentMongoPath(path.mongo) + const newProject = await Project.findOneAndUpdate( + { _id: project._id }, + { + $pull: { + [`${folderMongoPath}.fileRefs`]: { _id: fileId }, + }, + $push: { + [`${folderMongoPath}.docs`]: newDoc, + }, + $inc: { version: 1 }, + }, + { new: true } + ).exec() + return newProject +} + +async function mkdirp(projectId, path, options = {}) { + // defaults to case insensitive paths, use options {exactCaseMatch:true} + // to make matching case-sensitive + let folders = path.split('/') + folders = _.select(folders, folder => folder.length !== 0) + + const project = await ProjectGetter.promises.getProjectWithOnlyFolders( + projectId + ) + if (path === '/') { + return { newFolders: [], folder: project.rootFolder[0] } + } + + const newFolders = [] + let builtUpPath = '' + let lastFolder = null + for (const folderName of folders) { + builtUpPath += `/${folderName}` + try { + const { + element: foundFolder, + } = await ProjectLocator.promises.findElementByPath({ + project, + path: builtUpPath, + exactCaseMatch: options.exactCaseMatch, + }) + lastFolder = foundFolder + } catch (err) { + // Folder couldn't be found. Create it. + const parentFolderId = lastFolder && lastFolder._id + const { + folder: newFolder, + parentFolderId: newParentFolderId, + } = await addFolder(projectId, parentFolderId, folderName) + newFolder.parentFolder_id = newParentFolderId + lastFolder = newFolder + newFolders.push(newFolder) + } + } + return { folder: lastFolder, newFolders } +} + +async function moveEntity(projectId, entityId, destFolderId, entityType) { + const project = await ProjectGetter.promises.getProjectWithoutLock( + projectId, + { rootFolder: true, name: true, overleaf: true } + ) + const { + element: entity, + path: entityPath, + } = await ProjectLocator.promises.findElement({ + project, + element_id: entityId, + type: entityType, + }) + // Prevent top-level docs/files with reserved names (to match v1 behaviour) + if (_blockedFilename(entityPath, entityType)) { + throw new Errors.InvalidNameError('blocked element name') + } + await _checkValidMove(project, entityType, entity, entityPath, destFolderId) + const { + docs: oldDocs, + files: oldFiles, + } = await ProjectEntityHandler.promises.getAllEntitiesFromProject(project) + // For safety, insert the entity in the destination + // location first, and then remove the original. If + // there is an error the entity may appear twice. This + // will cause some breakage but is better than being + // lost, which is what happens if this is done in the + // opposite order. + const { result } = await _putElement( + project, + destFolderId, + entity, + entityType + ) + // Note: putElement always pushes onto the end of an + // array so it will never change an existing mongo + // path. Therefore it is safe to remove an element + // from the project with an existing path after + // calling putElement. But we must be sure that we + // have not moved a folder subfolder of itself (which + // is done by _checkValidMove above) because that + // would lead to it being deleted. + const newProject = await _removeElementFromMongoArray( + Project, + projectId, + entityPath.mongo, + entityId + ) + const { + docs: newDocs, + files: newFiles, + } = await ProjectEntityHandler.promises.getAllEntitiesFromProject(newProject) + const startPath = entityPath.fileSystem + const endPath = result.path.fileSystem + const changes = { + oldDocs, + newDocs, + oldFiles, + newFiles, + newProject, + } + // check that no files have been lost (or duplicated) + if ( + oldFiles.length !== newFiles.length || + oldDocs.length !== newDocs.length + ) { + logger.warn( + { + projectId, + oldDocs: oldDocs.length, + newDocs: newDocs.length, + oldFiles: oldFiles.length, + newFiles: newFiles.length, + origProject: project, + newProject, + }, + "project corrupted moving files - shouldn't happen" + ) + throw new Error('unexpected change in project structure') + } + return { project, startPath, endPath, rev: entity.rev, changes } +} + +async function deleteEntity(projectId, entityId, entityType, callback) { + const project = await ProjectGetter.promises.getProjectWithoutLock( + projectId, + { name: true, rootFolder: true, overleaf: true, rootDoc_id: true } + ) + const deleteRootDoc = + project.rootDoc_id && + entityId && + project.rootDoc_id.toString() === entityId.toString() + const { element: entity, path } = await ProjectLocator.promises.findElement({ + project, + element_id: entityId, + type: entityType, + }) + const newProject = await _removeElementFromMongoArray( + Project, + projectId, + path.mongo, + entityId, + deleteRootDoc + ) + return { entity, path, projectBeforeDeletion: project, newProject } +} + +async function renameEntity( + projectId, + entityId, + entityType, + newName, + callback +) { + const project = await ProjectGetter.promises.getProjectWithoutLock( + projectId, + { rootFolder: true, name: true, overleaf: true } + ) + const { + element: entity, + path: entPath, + folder: parentFolder, + } = await ProjectLocator.promises.findElement({ + project, + element_id: entityId, + type: entityType, + }) + const startPath = entPath.fileSystem + const endPath = path.join(path.dirname(entPath.fileSystem), newName) + + // Prevent top-level docs/files with reserved names (to match v1 behaviour) + if (_blockedFilename({ fileSystem: endPath }, entityType)) { + throw new Errors.InvalidNameError('blocked element name') + } + + // check if the new name already exists in the current folder + _checkValidElementName(parentFolder, newName) + + const { + docs: oldDocs, + files: oldFiles, + } = await ProjectEntityHandler.promises.getAllEntitiesFromProject(project) + + // we need to increment the project version number for any structure change + const newProject = await Project.findOneAndUpdate( + { _id: projectId }, + { $set: { [`${entPath.mongo}.name`]: newName }, $inc: { version: 1 } }, + { new: true } + ).exec() + + const { + docs: newDocs, + files: newFiles, + } = await ProjectEntityHandler.promises.getAllEntitiesFromProject(newProject) + return { + project, + startPath, + endPath, + rev: entity.rev, + changes: { oldDocs, newDocs, oldFiles, newFiles, newProject }, + } +} + +async function _insertDeletedFileReference(projectId, fileRef) { + await DeletedFile.create({ + projectId, + _id: fileRef._id, + name: fileRef.name, + linkedFileData: fileRef.linkedFileData, + hash: fileRef.hash, + deletedAt: new Date(), + }) +} + +async function _removeElementFromMongoArray( + model, + modelId, + path, + elementId, + deleteRootDoc = false +) { + const nonArrayPath = path.slice(0, path.lastIndexOf('.')) + const options = { new: true } + const query = { _id: modelId } + const update = { + $pull: { [nonArrayPath]: { _id: elementId } }, + $inc: { version: 1 }, + } + if (deleteRootDoc) { + update.$unset = { rootDoc_id: 1 } + } + return model.findOneAndUpdate(query, update, options).exec() +} + +function _countElements(project) { + function countFolder(folder) { + if (folder == null) { + return 0 + } + + let total = 0 + if (folder.folders) { + total += folder.folders.length + for (const subfolder of folder.folders) { + total += countFolder(subfolder) + } + } + if (folder.docs) { + total += folder.docs.length + } + if (folder.fileRefs) { + total += folder.fileRefs.length + } + return total + } + + return countFolder(project.rootFolder[0]) +} + +async function _putElement(project, folderId, element, type) { + if (element == null || element._id == null) { + logger.warn( + { projectId: project._id, folderId, element, type }, + 'failed trying to insert element as it was null' + ) + throw new Error('no element passed to be inserted') + } + + const pathSegment = _getMongoPathSegmentFromType(type) + + // original check path.resolve("/", element.name) isnt "/#{element.name}" or element.name.match("/") + // check if name is allowed + if (!SafePath.isCleanFilename(element.name)) { + logger.warn( + { projectId: project._id, folderId, element, type }, + 'failed trying to insert element as name was invalid' + ) + throw new Errors.InvalidNameError('invalid element name') + } + + if (folderId == null) { + folderId = project.rootFolder[0]._id + } + + if (_countElements(project) > Settings.maxEntitiesPerProject) { + logger.warn( + { projectId: project._id }, + 'project too big, stopping insertions' + ) + CooldownManager.putProjectOnCooldown(project._id) + throw new Error('project_has_too_many_files') + } + + const { element: folder, path } = await ProjectLocator.promises.findElement({ + project, + element_id: folderId, + type: 'folder', + }) + const newPath = { + fileSystem: `${path.fileSystem}/${element.name}`, + mongo: path.mongo, + } + // check if the path would be too long + if (!SafePath.isAllowedLength(newPath.fileSystem)) { + throw new Errors.InvalidNameError('path too long') + } + // Prevent top-level docs/files with reserved names (to match v1 behaviour) + if (_blockedFilename(newPath, type)) { + throw new Errors.InvalidNameError('blocked element name') + } + _checkValidElementName(folder, element.name) + element._id = ObjectId(element._id.toString()) + const mongoPath = `${path.mongo}.${pathSegment}` + const newProject = await Project.findOneAndUpdate( + { _id: project._id }, + { $push: { [mongoPath]: element }, $inc: { version: 1 } }, + { new: true } + ).exec() + return { result: { path: newPath }, project: newProject } +} + +function _blockedFilename(entityPath, entityType) { + // check if name would be blocked in v1 + // javascript reserved names are forbidden for docs and files + // at the top-level (but folders with reserved names are allowed). + const isFolder = entityType === 'folder' + const dir = path.dirname(entityPath.fileSystem) + const file = path.basename(entityPath.fileSystem) + const isTopLevel = dir === '/' + if (isTopLevel && !isFolder && SafePath.isBlockedFilename(file)) { + return true + } else { + return false + } +} + +function _getMongoPathSegmentFromType(type) { + const pathSegment = ENTITY_TYPE_TO_MONGO_PATH_SEGMENT[type] + if (pathSegment == null) { + throw new Error(`Unknown entity type: ${type}`) + } + return pathSegment +} + +/** + * Check if the name is already taken by a doc, file or folder. If so, return an + * error "file already exists". + */ +function _checkValidElementName(folder, name) { + if (folder == null) { + return + } + const elements = [] + .concat(folder.docs || []) + .concat(folder.fileRefs || []) + .concat(folder.folders || []) + for (const element of elements) { + if (element.name === name) { + throw new Errors.InvalidNameError('file already exists') + } + } +} + +function _confirmFolder(project, folderId) { + if (folderId == null) { + return project.rootFolder[0]._id + } else { + return folderId + } +} + +async function _checkValidMove( + project, + entityType, + entity, + entityPath, + destFolderId +) { + const { + element: destEntity, + path: destFolderPath, + } = await ProjectLocator.promises.findElement({ + project, + element_id: destFolderId, + type: 'folder', + }) + // check if there is already a doc/file/folder with the same name + // in the destination folder + _checkValidElementName(destEntity, entity.name) + if (/folder/.test(entityType)) { + const isNestedFolder = + destFolderPath.fileSystem.slice(0, entityPath.fileSystem.length) === + entityPath.fileSystem + if (isNestedFolder) { + throw new Errors.InvalidNameError( + 'destination folder is a child folder of me' + ) + } + } +} + +/** + * Create an initial file tree out of a list of doc and file entries + * + * Each entry specifies a path to the doc or file. Folders are automatically + * created. + * + * @param {ObjectId} projectId - id of the project + * @param {DocEntry[]} docEntries - list of docs to add + * @param {FileEntry[]} fileEntries - list of files to add + * @return {Promise} the project version after the operation + */ +async function createNewFolderStructure(projectId, docEntries, fileEntries) { + try { + const rootFolder = FolderStructureBuilder.buildFolderStructure( + docEntries, + fileEntries + ) + const project = await Project.findOneAndUpdate( + { + _id: projectId, + 'rootFolder.0.folders.0': { $exists: false }, + 'rootFolder.0.docs.0': { $exists: false }, + 'rootFolder.0.files.0': { $exists: false }, + }, + { + $set: { rootFolder: [rootFolder] }, + $inc: { version: 1 }, + }, + { + new: true, + lean: true, + fields: { version: 1 }, + } + ).exec() + if (project == null) { + throw new OError('project not found or folder structure already exists', { + projectId, + }) + } + return project.version + } catch (err) { + throw OError.tag(err, 'failed to create folder structure', { projectId }) + } +} + +/** + * Given a Mongo path to an entity, return the Mongo path to the parent folder + */ +function _getParentMongoPath(mongoPath) { + const segments = mongoPath.split('.') + if (segments.length <= 2) { + throw new Error('Root folder has no parents') + } + return segments.slice(0, -2).join('.') +} diff --git a/services/web/app/src/Features/Project/ProjectEntityUpdateHandler.js b/services/web/app/src/Features/Project/ProjectEntityUpdateHandler.js new file mode 100644 index 0000000000..58aa025ed8 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectEntityUpdateHandler.js @@ -0,0 +1,1725 @@ +const _ = require('lodash') +const OError = require('@overleaf/o-error') +const async = require('async') +const logger = require('logger-sharelatex') +const Settings = require('@overleaf/settings') +const Path = require('path') +const fs = require('fs') +const { Doc } = require('../../models/Doc') +const DocstoreManager = require('../Docstore/DocstoreManager') +const DocumentUpdaterHandler = require('../../Features/DocumentUpdater/DocumentUpdaterHandler') +const Errors = require('../Errors/Errors') +const FileStoreHandler = require('../FileStore/FileStoreHandler') +const LockManager = require('../../infrastructure/LockManager') +const { Project } = require('../../models/Project') +const ProjectEntityHandler = require('./ProjectEntityHandler') +const ProjectGetter = require('./ProjectGetter') +const ProjectLocator = require('./ProjectLocator') +const ProjectUpdateHandler = require('./ProjectUpdateHandler') +const ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler') +const SafePath = require('./SafePath') +const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') +const FileWriter = require('../../infrastructure/FileWriter') +const EditorRealTimeController = require('../Editor/EditorRealTimeController') +const { promisifyAll } = require('../../util/promises') + +const LOCK_NAMESPACE = 'sequentialProjectStructureUpdateLock' +const VALID_ROOT_DOC_EXTENSIONS = Settings.validRootDocExtensions +const VALID_ROOT_DOC_REGEXP = new RegExp( + `^\\.(${VALID_ROOT_DOC_EXTENSIONS.join('|')})$`, + 'i' +) + +function wrapWithLock(methodWithoutLock) { + // This lock is used to make sure that the project structure updates are made + // sequentially. In particular the updates must be made in mongo and sent to + // the doc-updater in the same order. + if (typeof methodWithoutLock === 'function') { + const methodWithLock = (projectId, ...rest) => { + const adjustedLength = Math.max(rest.length, 1) + const args = rest.slice(0, adjustedLength - 1) + const callback = rest[adjustedLength - 1] + LockManager.runWithLock( + LOCK_NAMESPACE, + projectId, + cb => methodWithoutLock(projectId, ...args, cb), + callback + ) + } + methodWithLock.withoutLock = methodWithoutLock + return methodWithLock + } else { + // handle case with separate setup and locked stages + const wrapWithSetup = methodWithoutLock.beforeLock // a function to set things up before the lock + const mainTask = methodWithoutLock.withLock // function to execute inside the lock + const methodWithLock = wrapWithSetup((projectId, ...rest) => { + const adjustedLength = Math.max(rest.length, 1) + const args = rest.slice(0, adjustedLength - 1) + const callback = rest[adjustedLength - 1] + LockManager.runWithLock( + LOCK_NAMESPACE, + projectId, + cb => mainTask(projectId, ...args, cb), + callback + ) + }) + methodWithLock.withoutLock = wrapWithSetup(mainTask) + methodWithLock.beforeLock = methodWithoutLock.beforeLock + methodWithLock.mainTask = methodWithoutLock.withLock + return methodWithLock + } +} + +function getDocContext(projectId, docId, callback) { + ProjectGetter.getProject( + projectId, + { name: true, rootFolder: true }, + (err, project) => { + if (err) { + return callback( + OError.tag(err, 'error fetching project', { + projectId, + }) + ) + } + if (!project) { + return callback(new Errors.NotFoundError('project not found')) + } + ProjectLocator.findElement( + { project, element_id: docId, type: 'docs' }, + (err, doc, path) => { + if (err && err instanceof Errors.NotFoundError) { + // (Soft-)Deleted docs are removed from the file-tree (rootFolder). + // docstore can tell whether it exists and is (soft)-deleted. + DocstoreManager.isDocDeleted( + projectId, + docId, + (err, isDeletedDoc) => { + if (err && err instanceof Errors.NotFoundError) { + logger.warn( + { projectId, docId }, + 'doc not found while updating doc lines' + ) + callback(err) + } else if (err) { + callback( + OError.tag( + err, + 'error checking deletion status with docstore', + { projectId, docId } + ) + ) + } else { + if (!isDeletedDoc) { + // NOTE: This can happen while we delete a doc: + // 1. web will update the projects entry + // 2. web triggers flushes to tpds/doc-updater + // 3. web triggers (soft)-delete in docstore + // Specifically when an update comes in after 1 + // and before 3 completes. + logger.info( + { projectId, docId }, + 'updating doc that is in process of getting soft-deleted' + ) + } + callback(null, { + projectName: project.name, + isDeletedDoc: true, + path: null, + }) + } + } + ) + } else if (err) { + callback( + OError.tag(err, 'error finding doc in rootFolder', { + docId, + projectId, + }) + ) + } else { + callback(null, { + projectName: project.name, + isDeletedDoc: false, + path: path.fileSystem, + }) + } + } + ) + } + ) +} + +const ProjectEntityUpdateHandler = { + updateDocLines( + projectId, + docId, + lines, + version, + ranges, + lastUpdatedAt, + lastUpdatedBy, + callback + ) { + getDocContext(projectId, docId, (err, ctx) => { + if (err && err instanceof Errors.NotFoundError) { + // Do not allow an update to a doc which has never exist on this project + logger.warn( + { docId, projectId }, + 'project or doc not found while updating doc lines' + ) + return callback(err) + } + if (err) { + return callback(err) + } + const { projectName, isDeletedDoc, path } = ctx + logger.log({ projectId, docId }, 'telling docstore manager to update doc') + DocstoreManager.updateDoc( + projectId, + docId, + lines, + version, + ranges, + (err, modified, rev) => { + if (err != null) { + OError.tag(err, 'error sending doc to docstore', { + docId, + projectId, + }) + return callback(err) + } + logger.log( + { projectId, docId, modified }, + 'finished updating doc lines' + ) + // path will only be present if the doc is not deleted + if (!modified || isDeletedDoc) { + return callback() + } + // Don't need to block for marking as updated + ProjectUpdateHandler.markAsUpdated( + projectId, + lastUpdatedAt, + lastUpdatedBy + ) + TpdsUpdateSender.addDoc( + { + project_id: projectId, + path, + doc_id: docId, + project_name: projectName, + rev, + }, + callback + ) + } + ) + }) + }, + + setRootDoc(projectId, newRootDocID, callback) { + logger.log({ projectId, rootDocId: newRootDocID }, 'setting root doc') + if (projectId == null || newRootDocID == null) { + return callback( + new Errors.InvalidError('missing arguments (project or doc)') + ) + } + ProjectEntityHandler.getDocPathByProjectIdAndDocId( + projectId, + newRootDocID, + (err, docPath) => { + if (err != null) { + return callback(err) + } + if (ProjectEntityUpdateHandler.isPathValidForRootDoc(docPath)) { + Project.updateOne( + { _id: projectId }, + { rootDoc_id: newRootDocID }, + {}, + callback + ) + } else { + callback( + new Errors.UnsupportedFileTypeError( + 'invalid file extension for root doc' + ) + ) + } + } + ) + }, + + unsetRootDoc(projectId, callback) { + logger.log({ projectId }, 'removing root doc') + Project.updateOne( + { _id: projectId }, + { $unset: { rootDoc_id: true } }, + {}, + callback + ) + }, + + _addDocAndSendToTpds(projectId, folderId, doc, callback) { + ProjectEntityMongoUpdateHandler.addDoc( + projectId, + folderId, + doc, + (err, result, project) => { + if (err != null) { + OError.tag(err, 'error adding file with project', { + projectId, + folderId, + doc_name: doc != null ? doc.name : undefined, + doc_id: doc != null ? doc._id : undefined, + }) + return callback(err) + } + TpdsUpdateSender.addDoc( + { + project_id: projectId, + doc_id: doc != null ? doc._id : undefined, + path: result && result.path && result.path.fileSystem, + project_name: project.name, + rev: 0, + }, + err => { + if (err != null) { + return callback(err) + } + callback(null, result, project) + } + ) + } + ) + }, + + addDoc(projectId, folderId, docName, docLines, userId, callback) { + ProjectEntityUpdateHandler.addDocWithRanges( + projectId, + folderId, + docName, + docLines, + {}, + userId, + callback + ) + }, + + addDocWithRanges: wrapWithLock({ + beforeLock(next) { + return function ( + projectId, + folderId, + docName, + docLines, + ranges, + userId, + callback + ) { + if (!SafePath.isCleanFilename(docName)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + // Put doc in docstore first, so that if it errors, we don't have a doc_id in the project + // which hasn't been created in docstore. + const doc = new Doc({ name: docName }) + DocstoreManager.updateDoc( + projectId.toString(), + doc._id.toString(), + docLines, + 0, + ranges, + (err, modified, rev) => { + if (err != null) { + return callback(err) + } + next( + projectId, + folderId, + doc, + docName, + docLines, + ranges, + userId, + callback + ) + } + ) + } + }, + withLock( + projectId, + folderId, + doc, + docName, + docLines, + ranges, + userId, + callback + ) { + ProjectEntityUpdateHandler._addDocAndSendToTpds( + projectId, + folderId, + doc, + (err, result, project) => { + if (err != null) { + return callback(err) + } + const docPath = result && result.path && result.path.fileSystem + const projectHistoryId = + project.overleaf && + project.overleaf.history && + project.overleaf.history.id + const newDocs = [ + { + doc, + path: docPath, + docLines: docLines.join('\n'), + }, + ] + DocumentUpdaterHandler.updateProjectStructure( + projectId, + projectHistoryId, + userId, + { newDocs, newProject: project }, + error => { + if (error != null) { + return callback(error) + } + callback(null, doc, folderId) + } + ) + } + ) + }, + }), + + _uploadFile(projectId, folderId, fileName, fsPath, linkedFileData, callback) { + if (!SafePath.isCleanFilename(fileName)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + const fileArgs = { + name: fileName, + linkedFileData, + } + FileStoreHandler.uploadFileFromDisk( + projectId, + fileArgs, + fsPath, + (err, fileStoreUrl, fileRef) => { + if (err != null) { + OError.tag(err, 'error uploading image to s3', { + projectId, + folderId, + file_name: fileName, + fileRef, + }) + return callback(err) + } + callback(null, fileStoreUrl, fileRef) + } + ) + }, + + _addFileAndSendToTpds(projectId, folderId, fileRef, callback) { + ProjectEntityMongoUpdateHandler.addFile( + projectId, + folderId, + fileRef, + (err, result, project) => { + if (err != null) { + OError.tag(err, 'error adding file with project', { + projectId, + folderId, + file_name: fileRef.name, + fileRef, + }) + return callback(err) + } + TpdsUpdateSender.addFile( + { + project_id: projectId, + file_id: fileRef._id, + path: result && result.path && result.path.fileSystem, + project_name: project.name, + rev: fileRef.rev, + }, + err => { + if (err != null) { + return callback(err) + } + callback(null, result, project) + } + ) + } + ) + }, + + addFile: wrapWithLock({ + beforeLock(next) { + return function ( + projectId, + folderId, + fileName, + fsPath, + linkedFileData, + userId, + callback + ) { + if (!SafePath.isCleanFilename(fileName)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + ProjectEntityUpdateHandler._uploadFile( + projectId, + folderId, + fileName, + fsPath, + linkedFileData, + (error, fileStoreUrl, fileRef) => { + if (error != null) { + return callback(error) + } + next( + projectId, + folderId, + fileName, + fsPath, + linkedFileData, + userId, + fileRef, + fileStoreUrl, + callback + ) + } + ) + } + }, + withLock( + projectId, + folderId, + fileName, + fsPath, + linkedFileData, + userId, + fileRef, + fileStoreUrl, + callback + ) { + ProjectEntityUpdateHandler._addFileAndSendToTpds( + projectId, + folderId, + fileRef, + (err, result, project) => { + if (err != null) { + return callback(err) + } + const projectHistoryId = + project.overleaf && + project.overleaf.history && + project.overleaf.history.id + const newFiles = [ + { + file: fileRef, + path: result && result.path && result.path.fileSystem, + url: fileStoreUrl, + }, + ] + DocumentUpdaterHandler.updateProjectStructure( + projectId, + projectHistoryId, + userId, + { newFiles, newProject: project }, + error => { + if (error != null) { + return callback(error) + } + ProjectUpdateHandler.markAsUpdated(projectId, new Date(), userId) + callback(null, fileRef, folderId) + } + ) + } + ) + }, + }), + + replaceFile: wrapWithLock({ + beforeLock(next) { + return function ( + projectId, + fileId, + fsPath, + linkedFileData, + userId, + callback + ) { + // create a new file + const fileArgs = { + name: 'dummy-upload-filename', + linkedFileData, + } + FileStoreHandler.uploadFileFromDisk( + projectId, + fileArgs, + fsPath, + (err, fileStoreUrl, fileRef) => { + if (err != null) { + return callback(err) + } + next( + projectId, + fileId, + fsPath, + linkedFileData, + userId, + fileRef, + fileStoreUrl, + callback + ) + } + ) + } + }, + withLock( + projectId, + fileId, + fsPath, + linkedFileData, + userId, + newFileRef, + fileStoreUrl, + callback + ) { + ProjectEntityMongoUpdateHandler.replaceFileWithNew( + projectId, + fileId, + newFileRef, + (err, oldFileRef, project, path, newProject) => { + if (err != null) { + return callback(err) + } + const oldFiles = [ + { + file: oldFileRef, + path: path.fileSystem, + }, + ] + const newFiles = [ + { + file: newFileRef, + path: path.fileSystem, + url: fileStoreUrl, + }, + ] + const projectHistoryId = + project.overleaf && + project.overleaf.history && + project.overleaf.history.id + // Increment the rev for an in-place update (with the same path) so the third-party-datastore + // knows this is a new file. + // Ideally we would get this from ProjectEntityMongoUpdateHandler.replaceFileWithNew + // but it returns the original oldFileRef (after incrementing the rev value in mongo), + // so we add 1 to the rev from that. This isn't atomic and relies on the lock + // but it is acceptable for now. + TpdsUpdateSender.addFile( + { + project_id: project._id, + file_id: newFileRef._id, + path: path.fileSystem, + rev: oldFileRef.rev + 1, + project_name: project.name, + }, + err => { + if (err != null) { + return callback(err) + } + ProjectUpdateHandler.markAsUpdated(projectId, new Date(), userId) + + DocumentUpdaterHandler.updateProjectStructure( + projectId, + projectHistoryId, + userId, + { oldFiles, newFiles, newProject }, + callback + ) + } + ) + } + ) + }, + }), + + upsertDoc: wrapWithLock(function ( + projectId, + folderId, + docName, + docLines, + source, + userId, + callback + ) { + if (!SafePath.isCleanFilename(docName)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + ProjectLocator.findElement( + { project_id: projectId, element_id: folderId, type: 'folder' }, + (error, folder, folderPath) => { + if (error != null) { + return callback(error) + } + if (folder == null) { + return callback(new Error("Couldn't find folder")) + } + const existingDoc = folder.docs.find(({ name }) => name === docName) + const existingFile = folder.fileRefs.find( + ({ name }) => name === docName + ) + if (existingFile) { + const doc = new Doc({ name: docName }) + const filePath = `${folderPath.fileSystem}/${existingFile.name}` + DocstoreManager.updateDoc( + projectId.toString(), + doc._id.toString(), + docLines, + 0, + {}, + (err, modified, rev) => { + if (err != null) { + return callback(err) + } + ProjectEntityMongoUpdateHandler.replaceFileWithDoc( + projectId, + existingFile._id, + doc, + (err, project) => { + if (err) { + return callback(err) + } + TpdsUpdateSender.addDoc( + { + project_id: projectId, + doc_id: doc._id, + path: filePath, + project_name: project.name, + rev: existingFile.rev + 1, + }, + err => { + if (err) { + return callback(err) + } + const projectHistoryId = + project.overleaf && + project.overleaf.history && + project.overleaf.history.id + const newDocs = [ + { + doc, + path: filePath, + docLines: docLines.join('\n'), + }, + ] + const oldFiles = [ + { + file: existingFile, + path: filePath, + }, + ] + DocumentUpdaterHandler.updateProjectStructure( + projectId, + projectHistoryId, + userId, + { oldFiles, newDocs, newProject: project }, + error => { + if (error != null) { + return callback(error) + } + EditorRealTimeController.emitToRoom( + projectId, + 'removeEntity', + existingFile._id, + 'convertFileToDoc' + ) + callback(null, doc, true) + } + ) + } + ) + } + ) + } + ) + } else if (existingDoc) { + DocumentUpdaterHandler.setDocument( + projectId, + existingDoc._id, + userId, + docLines, + source, + err => { + if (err != null) { + return callback(err) + } + logger.log( + { projectId, docId: existingDoc._id }, + 'notifying users that the document has been updated' + ) + DocumentUpdaterHandler.flushDocToMongo( + projectId, + existingDoc._id, + err => { + if (err != null) { + return callback(err) + } + callback(null, existingDoc, existingDoc == null) + } + ) + } + ) + } else { + ProjectEntityUpdateHandler.addDocWithRanges.withoutLock( + projectId, + folderId, + docName, + docLines, + {}, + userId, + (err, doc) => { + if (err != null) { + return callback(err) + } + callback(null, doc, existingDoc == null) + } + ) + } + } + ) + }), + + upsertFile: wrapWithLock({ + beforeLock(next) { + return function ( + projectId, + folderId, + fileName, + fsPath, + linkedFileData, + userId, + callback + ) { + if (!SafePath.isCleanFilename(fileName)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + // create a new file + const fileArgs = { + name: fileName, + linkedFileData, + } + FileStoreHandler.uploadFileFromDisk( + projectId, + fileArgs, + fsPath, + (err, fileStoreUrl, fileRef) => { + if (err != null) { + return callback(err) + } + next( + projectId, + folderId, + fileName, + fsPath, + linkedFileData, + userId, + fileRef, + fileStoreUrl, + callback + ) + } + ) + } + }, + withLock( + projectId, + folderId, + fileName, + fsPath, + linkedFileData, + userId, + newFileRef, + fileStoreUrl, + callback + ) { + ProjectLocator.findElement( + { project_id: projectId, element_id: folderId, type: 'folder' }, + (error, folder) => { + if (error != null) { + return callback(error) + } + if (folder == null) { + return callback(new Error("Couldn't find folder")) + } + const existingFile = folder.fileRefs.find( + ({ name }) => name === fileName + ) + const existingDoc = folder.docs.find(({ name }) => name === fileName) + + if (existingDoc) { + ProjectLocator.findElement( + { + project_id: projectId, + element_id: existingDoc._id, + type: 'doc', + }, + (err, doc, path) => { + if (err) { + return callback(new Error('coudnt find existing file')) + } + ProjectEntityMongoUpdateHandler.replaceDocWithFile( + projectId, + existingDoc._id, + newFileRef, + (err, project) => { + if (err) { + return callback(err) + } + const projectHistoryId = + project.overleaf && + project.overleaf.history && + project.overleaf.history.id + TpdsUpdateSender.addFile( + { + project_id: project._id, + file_id: newFileRef._id, + path: path.fileSystem, + rev: newFileRef.rev, + project_name: project.name, + }, + err => { + if (err) { + return callback(err) + } + DocumentUpdaterHandler.updateProjectStructure( + projectId, + projectHistoryId, + userId, + { + oldDocs: [ + { doc: existingDoc, path: path.fileSystem }, + ], + + newFiles: [ + { + file: newFileRef, + path: path.fileSystem, + url: fileStoreUrl, + }, + ], + newProject: project, + }, + err => { + if (err) { + return callback(err) + } + EditorRealTimeController.emitToRoom( + projectId, + 'removeEntity', + existingDoc._id, + 'convertDocToFile' + ) + callback(null, newFileRef, true, existingFile) + } + ) + } + ) + } + ) + } + ) + } else if (existingFile) { + // this calls directly into the replaceFile main task (without the beforeLock part) + return ProjectEntityUpdateHandler.replaceFile.mainTask( + projectId, + existingFile._id, + fsPath, + linkedFileData, + userId, + newFileRef, + fileStoreUrl, + err => { + if (err != null) { + return callback(err) + } + callback(null, newFileRef, existingFile == null, existingFile) + } + ) + } else { + // this calls directly into the addFile main task (without the beforeLock part) + ProjectEntityUpdateHandler.addFile.mainTask( + projectId, + folderId, + fileName, + fsPath, + linkedFileData, + userId, + newFileRef, + fileStoreUrl, + err => { + if (err != null) { + return callback(err) + } + callback(null, newFileRef, existingFile == null, existingFile) + } + ) + } + } + ) + }, + }), + + upsertDocWithPath: wrapWithLock(function ( + projectId, + elementPath, + docLines, + source, + userId, + callback + ) { + if (!SafePath.isCleanPath(elementPath)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + const docName = Path.basename(elementPath) + const folderPath = Path.dirname(elementPath) + ProjectEntityUpdateHandler.mkdirp.withoutLock( + projectId, + folderPath, + (err, newFolders, folder) => { + if (err != null) { + return callback(err) + } + ProjectEntityUpdateHandler.upsertDoc.withoutLock( + projectId, + folder._id, + docName, + docLines, + source, + userId, + (err, doc, isNewDoc) => { + if (err != null) { + return callback(err) + } + callback(null, doc, isNewDoc, newFolders, folder) + } + ) + } + ) + }), + + upsertFileWithPath: wrapWithLock({ + beforeLock(next) { + return function ( + projectId, + elementPath, + fsPath, + linkedFileData, + userId, + callback + ) { + if (!SafePath.isCleanPath(elementPath)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + const fileName = Path.basename(elementPath) + const folderPath = Path.dirname(elementPath) + // create a new file + const fileArgs = { + name: fileName, + linkedFileData, + } + FileStoreHandler.uploadFileFromDisk( + projectId, + fileArgs, + fsPath, + (err, fileStoreUrl, fileRef) => { + if (err != null) { + return callback(err) + } + next( + projectId, + folderPath, + fileName, + fsPath, + linkedFileData, + userId, + fileRef, + fileStoreUrl, + callback + ) + } + ) + } + }, + withLock( + projectId, + folderPath, + fileName, + fsPath, + linkedFileData, + userId, + fileRef, + fileStoreUrl, + callback + ) { + ProjectEntityUpdateHandler.mkdirp.withoutLock( + projectId, + folderPath, + (err, newFolders, folder) => { + if (err != null) { + return callback(err) + } + // this calls directly into the upsertFile main task (without the beforeLock part) + ProjectEntityUpdateHandler.upsertFile.mainTask( + projectId, + folder._id, + fileName, + fsPath, + linkedFileData, + userId, + fileRef, + fileStoreUrl, + (err, newFile, isNewFile, existingFile) => { + if (err != null) { + return callback(err) + } + callback( + null, + newFile, + isNewFile, + existingFile, + newFolders, + folder + ) + } + ) + } + ) + }, + }), + + deleteEntity: wrapWithLock(function ( + projectId, + entityId, + entityType, + userId, + callback + ) { + logger.log({ entityId, entityType, projectId }, 'deleting project entity') + if (entityType == null) { + logger.warn({ err: 'No entityType set', projectId, entityId }) + return callback(new Error('No entityType set')) + } + entityType = entityType.toLowerCase() + ProjectEntityMongoUpdateHandler.deleteEntity( + projectId, + entityId, + entityType, + (error, entity, path, projectBeforeDeletion, newProject) => { + if (error != null) { + return callback(error) + } + ProjectEntityUpdateHandler._cleanUpEntity( + projectBeforeDeletion, + newProject, + entity, + entityType, + path.fileSystem, + userId, + error => { + if (error != null) { + return callback(error) + } + TpdsUpdateSender.deleteEntity( + { + project_id: projectId, + path: path.fileSystem, + project_name: projectBeforeDeletion.name, + }, + error => { + if (error != null) { + return callback(error) + } + callback(null, entityId) + } + ) + } + ) + } + ) + }), + + deleteEntityWithPath: wrapWithLock((projectId, path, userId, callback) => + ProjectLocator.findElementByPath( + { project_id: projectId, path }, + (err, element, type) => { + if (err != null) { + return callback(err) + } + if (element == null) { + return callback(new Errors.NotFoundError('project not found')) + } + ProjectEntityUpdateHandler.deleteEntity.withoutLock( + projectId, + element._id, + type, + userId, + callback + ) + } + ) + ), + + mkdirp: wrapWithLock(function (projectId, path, callback) { + for (const folder of path.split('/')) { + if (folder.length > 0 && !SafePath.isCleanFilename(folder)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + } + ProjectEntityMongoUpdateHandler.mkdirp( + projectId, + path, + { exactCaseMatch: false }, + callback + ) + }), + + mkdirpWithExactCase: wrapWithLock(function (projectId, path, callback) { + for (const folder of path.split('/')) { + if (folder.length > 0 && !SafePath.isCleanFilename(folder)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + } + ProjectEntityMongoUpdateHandler.mkdirp( + projectId, + path, + { exactCaseMatch: true }, + callback + ) + }), + + addFolder: wrapWithLock(function ( + projectId, + parentFolderId, + folderName, + callback + ) { + if (!SafePath.isCleanFilename(folderName)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + ProjectEntityMongoUpdateHandler.addFolder( + projectId, + parentFolderId, + folderName, + callback + ) + }), + + moveEntity: wrapWithLock(function ( + projectId, + entityId, + destFolderId, + entityType, + userId, + callback + ) { + logger.log( + { entityType, entityId, projectId, destFolderId }, + 'moving entity' + ) + if (entityType == null) { + logger.warn({ err: 'No entityType set', projectId, entityId }) + return callback(new Error('No entityType set')) + } + entityType = entityType.toLowerCase() + ProjectEntityMongoUpdateHandler.moveEntity( + projectId, + entityId, + destFolderId, + entityType, + (err, project, startPath, endPath, rev, changes) => { + if (err != null) { + return callback(err) + } + const projectHistoryId = + project.overleaf && + project.overleaf.history && + project.overleaf.history.id + // do not wait + TpdsUpdateSender.promises + .moveEntity({ + project_id: projectId, + project_name: project.name, + startPath, + endPath, + rev, + }) + .catch(err => { + logger.error({ err }, 'error sending tpds update') + }) + DocumentUpdaterHandler.updateProjectStructure( + projectId, + projectHistoryId, + userId, + changes, + callback + ) + } + ) + }), + + renameEntity: wrapWithLock(function ( + projectId, + entityId, + entityType, + newName, + userId, + callback + ) { + if (!SafePath.isCleanFilename(newName)) { + return callback(new Errors.InvalidNameError('invalid element name')) + } + logger.log({ entityId, projectId }, `renaming ${entityType}`) + if (entityType == null) { + logger.warn({ err: 'No entityType set', projectId, entityId }) + return callback(new Error('No entityType set')) + } + entityType = entityType.toLowerCase() + + ProjectEntityMongoUpdateHandler.renameEntity( + projectId, + entityId, + entityType, + newName, + (err, project, startPath, endPath, rev, changes) => { + if (err != null) { + return callback(err) + } + const projectHistoryId = + project.overleaf && + project.overleaf.history && + project.overleaf.history.id + // do not wait + TpdsUpdateSender.promises + .moveEntity({ + project_id: projectId, + project_name: project.name, + startPath, + endPath, + rev, + }) + .catch(err => { + logger.error({ err }, 'error sending tpds update') + }) + DocumentUpdaterHandler.updateProjectStructure( + projectId, + projectHistoryId, + userId, + changes, + callback + ) + } + ) + }), + + // This doesn't directly update project structure but we need to take the lock + // to prevent anything else being queued before the resync update + resyncProjectHistory: wrapWithLock((projectId, callback) => + ProjectGetter.getProject( + projectId, + { rootFolder: true, overleaf: true }, + (error, project) => { + if (error != null) { + return callback(error) + } + + const projectHistoryId = + project && + project.overleaf && + project.overleaf.history && + project.overleaf.history.id + if (projectHistoryId == null) { + error = new Errors.ProjectHistoryDisabledError( + `project history not enabled for ${projectId}` + ) + return callback(error) + } + + ProjectEntityHandler.getAllEntitiesFromProject( + project, + (error, docs, files) => { + if (error != null) { + return callback(error) + } + + docs = _.map(docs, doc => ({ + doc: doc.doc._id, + path: doc.path, + })) + + files = _.map(files, file => ({ + file: file.file._id, + path: file.path, + url: FileStoreHandler._buildUrl(projectId, file.file._id), + })) + + DocumentUpdaterHandler.resyncProjectHistory( + projectId, + projectHistoryId, + docs, + files, + callback + ) + } + ) + } + ) + ), + + isPathValidForRootDoc(docPath) { + const docExtension = Path.extname(docPath) + return VALID_ROOT_DOC_REGEXP.test(docExtension) + }, + + _cleanUpEntity( + project, + newProject, + entity, + entityType, + path, + userId, + callback + ) { + ProjectEntityUpdateHandler._updateProjectStructureWithDeletedEntity( + project, + newProject, + entity, + entityType, + path, + userId, + error => { + if (error != null) { + return callback(error) + } + if (entityType.indexOf('file') !== -1) { + ProjectEntityUpdateHandler._cleanUpFile( + project, + entity, + path, + userId, + callback + ) + } else if (entityType.indexOf('doc') !== -1) { + ProjectEntityUpdateHandler._cleanUpDoc( + project, + entity, + path, + userId, + callback + ) + } else if (entityType.indexOf('folder') !== -1) { + ProjectEntityUpdateHandler._cleanUpFolder( + project, + entity, + path, + userId, + callback + ) + } else { + callback() + } + } + ) + }, + + // Note: the _cleanUpEntity code and _updateProjectStructureWithDeletedEntity + // methods both need to recursively iterate over the entities in folder. + // These are currently using separate implementations of the recursion. In + // future, these could be simplified using a common project entity iterator. + _updateProjectStructureWithDeletedEntity( + project, + newProject, + entity, + entityType, + entityPath, + userId, + callback + ) { + // compute the changes to the project structure + let changes + if (entityType.indexOf('file') !== -1) { + changes = { oldFiles: [{ file: entity, path: entityPath }] } + } else if (entityType.indexOf('doc') !== -1) { + changes = { oldDocs: [{ doc: entity, path: entityPath }] } + } else if (entityType.indexOf('folder') !== -1) { + changes = { oldDocs: [], oldFiles: [] } + const _recurseFolder = (folder, folderPath) => { + for (const doc of folder.docs) { + changes.oldDocs.push({ doc, path: Path.join(folderPath, doc.name) }) + } + for (const file of folder.fileRefs) { + changes.oldFiles.push({ + file, + path: Path.join(folderPath, file.name), + }) + } + for (const childFolder of folder.folders) { + _recurseFolder(childFolder, Path.join(folderPath, childFolder.name)) + } + } + _recurseFolder(entity, entityPath) + } + // now send the project structure changes to the docupdater + changes.newProject = newProject + const projectId = project._id.toString() + const projectHistoryId = + project.overleaf && + project.overleaf.history && + project.overleaf.history.id + DocumentUpdaterHandler.updateProjectStructure( + projectId, + projectHistoryId, + userId, + changes, + callback + ) + }, + + _cleanUpDoc(project, doc, path, userId, callback) { + const projectId = project._id.toString() + const docId = doc._id.toString() + const unsetRootDocIfRequired = callback => { + if ( + project.rootDoc_id != null && + project.rootDoc_id.toString() === docId + ) { + ProjectEntityUpdateHandler.unsetRootDoc(projectId, callback) + } else { + callback() + } + } + + unsetRootDocIfRequired(error => { + if (error != null) { + return callback(error) + } + const { name } = doc + const deletedAt = new Date() + DocstoreManager.deleteDoc(projectId, docId, name, deletedAt, error => { + if (error) { + return callback(error) + } + DocumentUpdaterHandler.deleteDoc(projectId, docId, callback) + }) + }) + }, + + _cleanUpFile(project, file, path, userId, callback) { + ProjectEntityMongoUpdateHandler._insertDeletedFileReference( + project._id, + file, + callback + ) + }, + + _cleanUpFolder(project, folder, folderPath, userId, callback) { + const jobs = [] + folder.docs.forEach(doc => { + const docPath = Path.join(folderPath, doc.name) + jobs.push(callback => + ProjectEntityUpdateHandler._cleanUpDoc( + project, + doc, + docPath, + userId, + callback + ) + ) + }) + + folder.fileRefs.forEach(file => { + const filePath = Path.join(folderPath, file.name) + jobs.push(callback => + ProjectEntityUpdateHandler._cleanUpFile( + project, + file, + filePath, + userId, + callback + ) + ) + }) + + folder.folders.forEach(childFolder => { + folderPath = Path.join(folderPath, childFolder.name) + jobs.push(callback => + ProjectEntityUpdateHandler._cleanUpFolder( + project, + childFolder, + folderPath, + userId, + callback + ) + ) + }) + + async.series(jobs, callback) + }, + + convertDocToFile: wrapWithLock({ + beforeLock(next) { + return function (projectId, docId, userId, callback) { + DocumentUpdaterHandler.flushDocToMongo(projectId, docId, err => { + if (err) { + return callback(err) + } + ProjectLocator.findElement( + { project_id: projectId, element_id: docId, type: 'doc' }, + (err, doc, path) => { + const docPath = path.fileSystem + if (err) { + return callback(err) + } + DocstoreManager.getDoc( + projectId, + docId, + (err, docLines, rev, version, ranges) => { + if (err) { + return callback(err) + } + if (!_.isEmpty(ranges)) { + return callback(new Errors.DocHasRangesError({})) + } + DocumentUpdaterHandler.deleteDoc(projectId, docId, err => { + if (err) { + return callback(err) + } + FileWriter.writeLinesToDisk( + projectId, + docLines, + (err, fsPath) => { + if (err) { + return callback(err) + } + FileStoreHandler.uploadFileFromDisk( + projectId, + { name: doc.name, rev: rev + 1 }, + fsPath, + (err, fileStoreUrl, fileRef) => { + if (err) { + return callback(err) + } + fs.unlink(fsPath, err => { + if (err) { + logger.warn( + { err, path: fsPath }, + 'failed to clean up temporary file' + ) + } + next( + projectId, + doc, + docPath, + fileRef, + fileStoreUrl, + userId, + callback + ) + }) + } + ) + } + ) + }) + } + ) + } + ) + }) + } + }, + withLock(projectId, doc, path, fileRef, fileStoreUrl, userId, callback) { + ProjectEntityMongoUpdateHandler.replaceDocWithFile( + projectId, + doc._id, + fileRef, + (err, project) => { + if (err) { + return callback(err) + } + const projectHistoryId = + project.overleaf && + project.overleaf.history && + project.overleaf.history.id + DocumentUpdaterHandler.updateProjectStructure( + projectId, + projectHistoryId, + userId, + { + oldDocs: [{ doc, path }], + newFiles: [{ file: fileRef, path, url: fileStoreUrl }], + newProject: project, + }, + err => { + if (err) { + return callback(err) + } + ProjectLocator.findElement( + { + project_id: projectId, + element_id: fileRef._id, + type: 'file', + }, + (err, element, path, folder) => { + if (err) { + return callback(err) + } + EditorRealTimeController.emitToRoom( + projectId, + 'removeEntity', + doc._id, + 'convertDocToFile' + ) + EditorRealTimeController.emitToRoom( + projectId, + 'reciveNewFile', + folder._id, + fileRef, + 'convertDocToFile', + null, + userId + ) + callback(null, fileRef) + } + ) + } + ) + } + ) + }, + }), +} + +module.exports = ProjectEntityUpdateHandler +module.exports.promises = promisifyAll(ProjectEntityUpdateHandler, { + without: ['isPathValidForRootDoc'], + multiResult: { + _addDocAndSendToTpds: ['result', 'project'], + addDoc: ['doc', 'folderId'], + addDocWithRanges: ['doc', 'folderId'], + _uploadFile: ['fileStoreUrl', 'fileRef'], + _addFileAndSendToTpds: ['result', 'project'], + addFile: ['fileRef', 'folderId'], + upsertDoc: ['doc', 'isNew'], + upsertFile: ['fileRef', 'isNew', 'oldFileRef'], + upsertDocWithPath: ['doc', 'isNew', 'newFolders', 'folder'], + upsertFileWithPath: ['fileRef', 'isNew', 'oldFile', 'newFolders', 'folder'], + mkdirp: ['newFolders', 'folder'], + mkdirpWithExactCase: ['newFolders', 'folder'], + addFolder: ['folder', 'parentFolderId'], + }, +}) diff --git a/services/web/app/src/Features/Project/ProjectGetter.js b/services/web/app/src/Features/Project/ProjectGetter.js new file mode 100644 index 0000000000..25997e3ac0 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectGetter.js @@ -0,0 +1,173 @@ +const { db } = require('../../infrastructure/mongodb') +const { normalizeQuery } = require('../Helpers/Mongo') +const OError = require('@overleaf/o-error') +const metrics = require('@overleaf/metrics') +const { promisifyAll } = require('../../util/promises') +const { Project } = require('../../models/Project') +const logger = require('logger-sharelatex') +const LockManager = require('../../infrastructure/LockManager') +const { DeletedProject } = require('../../models/DeletedProject') + +const ProjectGetter = { + EXCLUDE_DEPTH: 8, + + getProjectWithoutDocLines(projectId, callback) { + const excludes = {} + for (let i = 1; i <= ProjectGetter.EXCLUDE_DEPTH; i++) { + excludes[`rootFolder${Array(i).join('.folders')}.docs.lines`] = 0 + } + ProjectGetter.getProject(projectId, excludes, callback) + }, + + getProjectWithOnlyFolders(projectId, callback) { + const excludes = {} + for (let i = 1; i <= ProjectGetter.EXCLUDE_DEPTH; i++) { + excludes[`rootFolder${Array(i).join('.folders')}.docs`] = 0 + excludes[`rootFolder${Array(i).join('.folders')}.fileRefs`] = 0 + } + ProjectGetter.getProject(projectId, excludes, callback) + }, + + getProject(projectId, projection, callback) { + if (typeof projection === 'function' && callback == null) { + callback = projection + projection = {} + } + if (projectId == null) { + return callback(new Error('no project id provided')) + } + if (typeof projection !== 'object') { + return callback(new Error('projection is not an object')) + } + + if (projection.rootFolder || Object.keys(projection).length === 0) { + const ProjectEntityMongoUpdateHandler = require('./ProjectEntityMongoUpdateHandler') + LockManager.runWithLock( + ProjectEntityMongoUpdateHandler.LOCK_NAMESPACE, + projectId, + cb => ProjectGetter.getProjectWithoutLock(projectId, projection, cb), + callback + ) + } else { + ProjectGetter.getProjectWithoutLock(projectId, projection, callback) + } + }, + + getProjectWithoutLock(projectId, projection, callback) { + if (typeof projection === 'function' && callback == null) { + callback = projection + projection = {} + } + if (projectId == null) { + return callback(new Error('no project id provided')) + } + if (typeof projection !== 'object') { + return callback(new Error('projection is not an object')) + } + + let query + try { + query = normalizeQuery(projectId) + } catch (err) { + return callback(err) + } + + db.projects.findOne(query, { projection }, function (err, project) { + if (err) { + OError.tag(err, 'error getting project', { + query, + projection, + }) + return callback(err) + } + callback(null, project) + }) + }, + + getProjectIdByReadAndWriteToken(token, callback) { + Project.findOne( + { 'tokens.readAndWrite': token }, + { _id: 1 }, + function (err, project) { + if (err) { + return callback(err) + } + if (project == null) { + return callback() + } + callback(null, project._id) + } + ) + }, + + findAllUsersProjects(userId, fields, callback) { + const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') + Project.find( + { owner_ref: userId }, + fields, + function (error, ownedProjects) { + if (error) { + return callback(error) + } + CollaboratorsGetter.getProjectsUserIsMemberOf( + userId, + fields, + function (error, projects) { + if (error) { + return callback(error) + } + const result = { + owned: ownedProjects || [], + readAndWrite: projects.readAndWrite || [], + readOnly: projects.readOnly || [], + tokenReadAndWrite: projects.tokenReadAndWrite || [], + tokenReadOnly: projects.tokenReadOnly || [], + } + callback(null, result) + } + ) + } + ) + }, + + /** + * Return all projects with the given name that belong to the given user. + * + * Projects include the user's own projects as well as collaborations with + * read/write access. + */ + findUsersProjectsByName(userId, projectName, callback) { + ProjectGetter.findAllUsersProjects( + userId, + 'name archived trashed', + (err, allProjects) => { + if (err != null) { + return callback(err) + } + const { owned, readAndWrite } = allProjects + const projects = owned.concat(readAndWrite) + const lowerCasedProjectName = projectName.toLowerCase() + const matches = projects.filter( + project => project.name.toLowerCase() === lowerCasedProjectName + ) + callback(null, matches) + } + ) + }, + + getUsersDeletedProjects(userId, callback) { + DeletedProject.find( + { + 'deleterData.deletedProjectOwnerId': userId, + }, + callback + ) + }, +} + +;['getProject', 'getProjectWithoutDocLines'].map(method => + metrics.timeAsyncMethod(ProjectGetter, method, 'mongo.ProjectGetter', logger) +) + +ProjectGetter.promises = promisifyAll(ProjectGetter) +module.exports = ProjectGetter diff --git a/services/web/app/src/Features/Project/ProjectHelper.js b/services/web/app/src/Features/Project/ProjectHelper.js new file mode 100644 index 0000000000..eee36944da --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectHelper.js @@ -0,0 +1,172 @@ +const { ObjectId } = require('mongodb') +const _ = require('lodash') +const { promisify } = require('util') +const Settings = require('@overleaf/settings') + +const ENGINE_TO_COMPILER_MAP = { + latex_dvipdf: 'latex', + pdflatex: 'pdflatex', + xelatex: 'xelatex', + lualatex: 'lualatex', +} + +module.exports = { + compilerFromV1Engine, + isArchived, + isTrashed, + isArchivedOrTrashed, + calculateArchivedArray, + ensureNameIsUnique, + getAllowedImagesForUser, + promises: { + ensureNameIsUnique: promisify(ensureNameIsUnique), + }, +} + +function compilerFromV1Engine(engine) { + return ENGINE_TO_COMPILER_MAP[engine] +} + +function isArchived(project, userId) { + userId = ObjectId(userId) + + if (Array.isArray(project.archived)) { + return project.archived.some(id => id.equals(userId)) + } else { + return !!project.archived + } +} + +function isTrashed(project, userId) { + userId = ObjectId(userId) + + if (project.trashed) { + return project.trashed.some(id => id.equals(userId)) + } else { + return false + } +} + +function isArchivedOrTrashed(project, userId) { + return isArchived(project, userId) || isTrashed(project, userId) +} + +function _allCollaborators(project) { + return _.unionWith( + [project.owner_ref], + project.collaberator_refs, + project.readOnly_refs, + project.tokenAccessReadAndWrite_refs, + project.tokenAccessReadOnly_refs, + _objectIdEquals + ) +} + +function calculateArchivedArray(project, userId, action) { + let archived = project.archived + userId = ObjectId(userId) + + if (archived === true) { + archived = _allCollaborators(project) + } else if (!archived) { + archived = [] + } + + if (action === 'ARCHIVE') { + archived = _.unionWith(archived, [userId], _objectIdEquals) + } else if (action === 'UNARCHIVE') { + archived = archived.filter(id => !_objectIdEquals(id, userId)) + } else { + throw new Error('Unrecognised action') + } + + return archived +} + +function ensureNameIsUnique(nameList, name, suffixes, maxLength, callback) { + // create a set of all project names + if (suffixes == null) { + suffixes = [] + } + const allNames = new Set(nameList) + const isUnique = x => !allNames.has(x) + // check if the supplied name is already unique + if (isUnique(name)) { + return callback(null, name) + } + // the name already exists, try adding the user-supplied suffixes to generate a unique name + for (const suffix of suffixes) { + const candidateName = _addSuffixToProjectName(name, suffix, maxLength) + if (isUnique(candidateName)) { + return callback(null, candidateName) + } + } + // if there are no (more) suffixes, use a numeric one + const uniqueName = _addNumericSuffixToProjectName(name, allNames, maxLength) + if (uniqueName != null) { + callback(null, uniqueName) + } else { + callback(new Error(`Failed to generate a unique name for: ${name}`)) + } +} + +function _objectIdEquals(firstVal, secondVal) { + // For use as a comparator for unionWith + return firstVal.toString() === secondVal.toString() +} + +function _addSuffixToProjectName(name, suffix, maxLength) { + // append the suffix and truncate the project title if needed + if (suffix == null) { + suffix = '' + } + const truncatedLength = maxLength - suffix.length + return name.substr(0, truncatedLength) + suffix +} + +function _addNumericSuffixToProjectName(name, allProjectNames, maxLength) { + const NUMERIC_SUFFIX_MATCH = / \((\d+)\)$/ + const suffixedName = function (basename, number) { + const suffix = ` (${number})` + return basename.substr(0, maxLength - suffix.length) + suffix + } + + const match = name.match(NUMERIC_SUFFIX_MATCH) + let basename = name + let n = 1 + const last = allProjectNames.size + n + + if (match != null) { + basename = name.replace(NUMERIC_SUFFIX_MATCH, '') + n = parseInt(match[1]) + } + + const prefixMatcher = new RegExp(`^${basename} \\(\\d+\\)$`) + const projectNamesWithSamePrefix = Array.from(allProjectNames).filter(name => + prefixMatcher.test(name) + ) + const nIsLikelyAYear = n > 1000 && projectNamesWithSamePrefix.length < n / 2 + if (nIsLikelyAYear) { + basename = name + n = 1 + } + + while (n <= last) { + const candidate = suffixedName(basename, n) + if (!allProjectNames.has(candidate)) { + return candidate + } + n += 1 + } + + return null +} + +function getAllowedImagesForUser(sessionUser) { + const images = Settings.allowedImageNames || [] + if (sessionUser && sessionUser.isAdmin) { + return images + } else { + return images.filter(image => !image.adminOnly) + } +} diff --git a/services/web/app/src/Features/Project/ProjectHistoryHandler.js b/services/web/app/src/Features/Project/ProjectHistoryHandler.js new file mode 100644 index 0000000000..f3f7c39c5b --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectHistoryHandler.js @@ -0,0 +1,175 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { Project } = require('../../models/Project') +const ProjectDetailsHandler = require('./ProjectDetailsHandler') +const logger = require('logger-sharelatex') +const settings = require('@overleaf/settings') +const HistoryManager = require('../History/HistoryManager') +const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler') +const { promisifyAll } = require('../../util/promises') + +const ProjectHistoryHandler = { + setHistoryId(project_id, history_id, callback) { + // reject invalid history ids + if (callback == null) { + callback = function (err) {} + } + if (!history_id || typeof history_id !== 'number') { + return callback(new Error('invalid history id')) + } + // use $exists:false to prevent overwriting any existing history id, atomically + return Project.updateOne( + { _id: project_id, 'overleaf.history.id': { $exists: false } }, + { 'overleaf.history.id': history_id }, + function (err, result) { + if (err != null) { + return callback(err) + } + if ((result != null ? result.n : undefined) === 0) { + return callback(new Error('history exists')) + } + return callback() + } + ) + }, + + getHistoryId(project_id, callback) { + if (callback == null) { + callback = function (err, result) {} + } + return ProjectDetailsHandler.getDetails( + project_id, + function (err, project) { + if (err != null) { + return callback(err) + } // n.b. getDetails returns an error if the project doesn't exist + return callback( + null, + __guard__( + __guard__( + project != null ? project.overleaf : undefined, + x1 => x1.history + ), + x => x.id + ) + ) + } + ) + }, + + upgradeHistory(project_id, callback) { + // project must have an overleaf.history.id before allowing display of new history + if (callback == null) { + callback = function (err, result) {} + } + return Project.updateOne( + { _id: project_id, 'overleaf.history.id': { $exists: true } }, + { + 'overleaf.history.display': true, + 'overleaf.history.upgradedAt': new Date(), + }, + function (err, result) { + if (err != null) { + return callback(err) + } + // return an error if overleaf.history.id wasn't present + if ((result != null ? result.n : undefined) === 0) { + return callback(new Error('history not upgraded')) + } + return callback() + } + ) + }, + + downgradeHistory(project_id, callback) { + if (callback == null) { + callback = function (err, result) {} + } + return Project.updateOne( + { _id: project_id, 'overleaf.history.upgradedAt': { $exists: true } }, + { + 'overleaf.history.display': false, + $unset: { 'overleaf.history.upgradedAt': 1 }, + }, + function (err, result) { + if (err != null) { + return callback(err) + } + if ((result != null ? result.n : undefined) === 0) { + return callback(new Error('history not downgraded')) + } + return callback() + } + ) + }, + + ensureHistoryExistsForProject(project_id, callback) { + // We can only set a history id for a project that doesn't have one. The + // history id is cached in the project history service, and changing an + // existing value corrupts the history, leaving it in an irrecoverable + // state. Setting a history id when one wasn't present before is ok, + // because undefined history ids aren't cached. + if (callback == null) { + callback = function (err) {} + } + return ProjectHistoryHandler.getHistoryId( + project_id, + function (err, history_id) { + if (err != null) { + return callback(err) + } + if (history_id != null) { + return callback() + } // history already exists, success + return HistoryManager.initializeProject(function (err, history) { + if (err != null) { + return callback(err) + } + if (!(history != null ? history.overleaf_id : undefined)) { + return callback(new Error('failed to initialize history id')) + } + return ProjectHistoryHandler.setHistoryId( + project_id, + history.overleaf_id, + function (err) { + if (err != null) { + return callback(err) + } + return ProjectEntityUpdateHandler.resyncProjectHistory( + project_id, + function (err) { + if (err != null) { + return callback(err) + } + return HistoryManager.flushProject(project_id, callback) + } + ) + } + ) + }) + } + ) + }, +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} + +ProjectHistoryHandler.promises = promisifyAll(ProjectHistoryHandler) +module.exports = ProjectHistoryHandler diff --git a/services/web/app/src/Features/Project/ProjectLocator.js b/services/web/app/src/Features/Project/ProjectLocator.js new file mode 100644 index 0000000000..e62b4dccae --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectLocator.js @@ -0,0 +1,304 @@ +const _ = require('underscore') +const logger = require('logger-sharelatex') +const async = require('async') +const ProjectGetter = require('./ProjectGetter') +const Errors = require('../Errors/Errors') +const { promisifyMultiResult } = require('../../util/promises') + +function findElement(options, _callback) { + // The search algorithm below potentially invokes the callback multiple + // times. + const callback = _.once(_callback) + + const { + project, + project_id: projectId, + element_id: elementId, + type, + } = options + const elementType = sanitizeTypeOfElement(type) + + let count = 0 + const endOfBranch = function () { + if (--count === 0) { + logger.warn( + `element ${elementId} could not be found for project ${ + projectId || project._id + }` + ) + callback(new Errors.NotFoundError('entity not found')) + } + } + + function search(searchFolder, path) { + count++ + const element = _.find( + searchFolder[elementType], + el => (el != null ? el._id : undefined) + '' === elementId + '' + ) // need to ToString both id's for robustness + if ( + element == null && + searchFolder.folders != null && + searchFolder.folders.length !== 0 + ) { + _.each(searchFolder.folders, (folder, index) => { + if (folder == null) { + return + } + const newPath = {} + for (const key of Object.keys(path)) { + const value = path[key] + newPath[key] = value + } // make a value copy of the string + newPath.fileSystem += `/${folder.name}` + newPath.mongo += `.folders.${index}` + search(folder, newPath) + }) + endOfBranch() + } else if (element != null) { + const elementPlaceInArray = getIndexOf( + searchFolder[elementType], + elementId + ) + path.fileSystem += `/${element.name}` + path.mongo += `.${elementType}.${elementPlaceInArray}` + callback(null, element, path, searchFolder) + } else if (element == null) { + endOfBranch() + } + } + + const path = { fileSystem: '', mongo: 'rootFolder.0' } + + const startSearch = project => { + if (elementId + '' === project.rootFolder[0]._id + '') { + callback(null, project.rootFolder[0], path, null) + } else { + search(project.rootFolder[0], path) + } + } + + if (project != null) { + startSearch(project) + } else { + ProjectGetter.getProject( + projectId, + { rootFolder: true, rootDoc_id: true }, + (err, project) => { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(new Errors.NotFoundError('project not found')) + } + startSearch(project) + } + ) + } +} + +function findRootDoc(opts, callback) { + const getRootDoc = project => { + if (project.rootDoc_id != null) { + findElement( + { project, element_id: project.rootDoc_id, type: 'docs' }, + (error, ...args) => { + if (error != null) { + if (error instanceof Errors.NotFoundError) { + return callback(null, null) + } else { + return callback(error) + } + } + callback(null, ...args) + } + ) + } else { + callback(null, null) + } + } + const { project, project_id: projectId } = opts + if (project != null) { + getRootDoc(project) + } else { + ProjectGetter.getProject( + projectId, + { rootFolder: true, rootDoc_id: true }, + (err, project) => { + if (err != null) { + logger.warn({ err }, 'error getting project') + callback(err) + } else { + getRootDoc(project) + } + } + ) + } +} + +function findElementByPath(options, callback) { + const { project, project_id: projectId, path, exactCaseMatch } = options + if (path == null) { + return new Error('no path provided for findElementByPath') + } + + if (project != null) { + _findElementByPathWithProject(project, path, exactCaseMatch, callback) + } else { + ProjectGetter.getProject( + projectId, + { rootFolder: true, rootDoc_id: true }, + (err, project) => { + if (err != null) { + return callback(err) + } + _findElementByPathWithProject(project, path, exactCaseMatch, callback) + } + ) + } +} + +function _findElementByPathWithProject( + project, + needlePath, + exactCaseMatch, + callback +) { + let matchFn + if (exactCaseMatch) { + matchFn = (a, b) => a === b + } else { + matchFn = (a, b) => + (a != null ? a.toLowerCase() : undefined) === + (b != null ? b.toLowerCase() : undefined) + } + + function getParentFolder(haystackFolder, foldersList, level, cb) { + if (foldersList.length === 0) { + return cb(null, haystackFolder) + } + const needleFolderName = foldersList[level] + let found = false + for (const folder of haystackFolder.folders) { + if (matchFn(folder.name, needleFolderName)) { + found = true + if (level === foldersList.length - 1) { + return cb(null, folder) + } else { + return getParentFolder(folder, foldersList, level + 1, cb) + } + } + } + if (!found) { + cb( + new Error( + `not found project: ${project._id} search path: ${needlePath}, folder ${foldersList[level]} could not be found` + ) + ) + } + } + + function getEntity(folder, entityName, cb) { + let result, type + if (entityName == null) { + return cb(null, folder, 'folder') + } + for (const file of folder.fileRefs || []) { + if (matchFn(file != null ? file.name : undefined, entityName)) { + result = file + type = 'file' + } + } + for (const doc of folder.docs || []) { + if (matchFn(doc != null ? doc.name : undefined, entityName)) { + result = doc + type = 'doc' + } + } + for (const childFolder of folder.folders || []) { + if ( + matchFn(childFolder != null ? childFolder.name : undefined, entityName) + ) { + result = childFolder + type = 'folder' + } + } + + if (result != null) { + cb(null, result, type) + } else { + cb( + new Error( + `not found project: ${project._id} search path: ${needlePath}, entity ${entityName} could not be found` + ) + ) + } + } + + if (project == null) { + return callback(new Error('Tried to find an element for a null project')) + } + if (needlePath === '' || needlePath === '/') { + return callback(null, project.rootFolder[0], 'folder') + } + + if (needlePath.indexOf('/') === 0) { + needlePath = needlePath.substring(1) + } + const foldersList = needlePath.split('/') + const needleName = foldersList.pop() + const rootFolder = project.rootFolder[0] + + const jobs = [] + jobs.push(cb => getParentFolder(rootFolder, foldersList, 0, cb)) + jobs.push((folder, cb) => getEntity(folder, needleName, cb)) + async.waterfall(jobs, callback) +} + +function sanitizeTypeOfElement(elementType) { + const lastChar = elementType.slice(-1) + if (lastChar !== 's') { + elementType += 's' + } + if (elementType === 'files') { + elementType = 'fileRefs' + } + return elementType +} + +function getIndexOf(searchEntity, id) { + const { length } = searchEntity + let count = 0 + while (count < length) { + if ( + (searchEntity[count] != null ? searchEntity[count]._id : undefined) + + '' === + id + '' + ) { + return count + } + count++ + } +} + +module.exports = { + findElement, + findElementByPath, + findRootDoc, + promises: { + findElement: promisifyMultiResult(findElement, [ + 'element', + 'path', + 'folder', + ]), + findElementByPath: promisifyMultiResult(findElementByPath, [ + 'element', + 'type', + ]), + findRootDoc: promisifyMultiResult(findRootDoc, [ + 'element', + 'path', + 'folder', + ]), + }, +} diff --git a/services/web/app/src/Features/Project/ProjectOptionsHandler.js b/services/web/app/src/Features/Project/ProjectOptionsHandler.js new file mode 100644 index 0000000000..aac0dfa383 --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectOptionsHandler.js @@ -0,0 +1,69 @@ +const { Project } = require('../../models/Project') +const settings = require('@overleaf/settings') +const { promisifyAll } = require('../../util/promises') + +const safeCompilers = ['xelatex', 'pdflatex', 'latex', 'lualatex'] + +const ProjectOptionsHandler = { + setCompiler(projectId, compiler, callback) { + if (!compiler) { + return callback() + } + compiler = compiler.toLowerCase() + if (!safeCompilers.includes(compiler)) { + return callback(new Error(`invalid compiler: ${compiler}`)) + } + const conditions = { _id: projectId } + const update = { compiler } + Project.updateOne(conditions, update, {}, callback) + }, + + setImageName(projectId, imageName, callback) { + if (!imageName || !Array.isArray(settings.allowedImageNames)) { + return callback() + } + imageName = imageName.toLowerCase() + const isAllowed = settings.allowedImageNames.find( + allowed => imageName === allowed.imageName + ) + if (!isAllowed) { + return callback(new Error(`invalid imageName: ${imageName}`)) + } + const conditions = { _id: projectId } + const update = { imageName: settings.imageRoot + '/' + imageName } + Project.updateOne(conditions, update, {}, callback) + }, + + setSpellCheckLanguage(projectId, languageCode, callback) { + if (!Array.isArray(settings.languages)) { + return callback() + } + const language = settings.languages.find( + language => language.code === languageCode + ) + if (languageCode && !language) { + return callback(new Error(`invalid languageCode: ${languageCode}`)) + } + const conditions = { _id: projectId } + const update = { spellCheckLanguage: languageCode } + Project.updateOne(conditions, update, {}, callback) + }, + + setBrandVariationId(projectId, brandVariationId, callback) { + if (!brandVariationId) { + return callback() + } + const conditions = { _id: projectId } + const update = { brandVariationId } + Project.updateOne(conditions, update, {}, callback) + }, + + unsetBrandVariationId(projectId, callback) { + const conditions = { _id: projectId } + const update = { $unset: { brandVariationId: 1 } } + Project.updateOne(conditions, update, {}, callback) + }, +} + +ProjectOptionsHandler.promises = promisifyAll(ProjectOptionsHandler) +module.exports = ProjectOptionsHandler diff --git a/services/web/app/src/Features/Project/ProjectRootDocManager.js b/services/web/app/src/Features/Project/ProjectRootDocManager.js new file mode 100644 index 0000000000..aa4af9a0fb --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectRootDocManager.js @@ -0,0 +1,326 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, + no-useless-escape, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectRootDocManager +const ProjectEntityHandler = require('./ProjectEntityHandler') +const ProjectEntityUpdateHandler = require('./ProjectEntityUpdateHandler') +const ProjectGetter = require('./ProjectGetter') +const DocumentHelper = require('../Documents/DocumentHelper') +const Path = require('path') +const fs = require('fs') +const { promisify } = require('util') +const async = require('async') +const globby = require('globby') +const _ = require('underscore') + +module.exports = ProjectRootDocManager = { + setRootDocAutomatically(project_id, callback) { + if (callback == null) { + callback = function (error) {} + } + return ProjectEntityHandler.getAllDocs(project_id, function (error, docs) { + if (error != null) { + return callback(error) + } + + const jobs = _.map( + docs, + (doc, path) => + function (cb) { + if ( + ProjectEntityUpdateHandler.isPathValidForRootDoc(path) && + DocumentHelper.contentHasDocumentclass(doc.lines) + ) { + async.setImmediate(function () { + cb(doc._id) + }) + } else { + async.setImmediate(function () { + cb(null) + }) + } + } + ) + + return async.series(jobs, function (root_doc_id) { + if (root_doc_id != null) { + return ProjectEntityUpdateHandler.setRootDoc( + project_id, + root_doc_id, + callback + ) + } else { + return callback() + } + }) + }) + }, + + findRootDocFileFromDirectory(directoryPath, callback) { + if (callback == null) { + callback = function (error, path, content) {} + } + const filePathsPromise = globby(['**/*.{tex,Rtex}'], { + cwd: directoryPath, + followSymlinkedDirectories: false, + onlyFiles: true, + case: false, + }) + + // the search order is such that we prefer files closer to the project root, then + // we go by file size in ascending order, because people often have a main + // file that just includes a bunch of other files; then we go by name, in + // order to be deterministic + filePathsPromise.then( + unsortedFiles => + ProjectRootDocManager._sortFileList( + unsortedFiles, + directoryPath, + function (err, files) { + if (err != null) { + return callback(err) + } + let doc = null + + return async.until( + () => doc != null || files.length === 0, + function (cb) { + const file = files.shift() + return fs.readFile( + Path.join(directoryPath, file), + 'utf8', + function (error, content) { + if (error != null) { + return cb(error) + } + content = (content || '').replace(/\r/g, '') + if (DocumentHelper.contentHasDocumentclass(content)) { + doc = { path: file, content } + } + return cb(null) + } + ) + }, + err => + callback( + err, + doc != null ? doc.path : undefined, + doc != null ? doc.content : undefined + ) + ) + } + ), + err => callback(err) + ) + + // coffeescript's implicit-return mechanism returns filePathsPromise from this method, which confuses mocha + return null + }, + + setRootDocFromName(project_id, rootDocName, callback) { + if (callback == null) { + callback = function (error) {} + } + return ProjectEntityHandler.getAllDocPathsFromProjectById( + project_id, + function (error, docPaths) { + let doc_id, path + if (error != null) { + return callback(error) + } + // strip off leading and trailing quotes from rootDocName + rootDocName = rootDocName.replace(/^\'|\'$/g, '') + // prepend a slash for the root folder if not present + if (rootDocName[0] !== '/') { + rootDocName = `/${rootDocName}` + } + // find the root doc from the filename + let root_doc_id = null + for (doc_id in docPaths) { + // docpaths have a leading / so allow matching "folder/filename" and "/folder/filename" + path = docPaths[doc_id] + if (path === rootDocName) { + root_doc_id = doc_id + } + } + // try a basename match if there was no match + if (!root_doc_id) { + for (doc_id in docPaths) { + path = docPaths[doc_id] + if (Path.basename(path) === Path.basename(rootDocName)) { + root_doc_id = doc_id + } + } + } + // set the root doc id if we found a match + if (root_doc_id != null) { + return ProjectEntityUpdateHandler.setRootDoc( + project_id, + root_doc_id, + callback + ) + } else { + return callback() + } + } + ) + }, + + ensureRootDocumentIsSet(project_id, callback) { + if (callback == null) { + callback = function (error) {} + } + return ProjectGetter.getProject( + project_id, + { rootDoc_id: 1 }, + function (error, project) { + if (error != null) { + return callback(error) + } + if (project == null) { + return callback(new Error('project not found')) + } + + if (project.rootDoc_id != null) { + return callback() + } else { + return ProjectRootDocManager.setRootDocAutomatically( + project_id, + callback + ) + } + } + ) + }, + + /** + * @param {ObjectId | string} project_id + * @param {Function} callback + */ + ensureRootDocumentIsValid(project_id, callback) { + ProjectGetter.getProjectWithoutDocLines( + project_id, + function (error, project) { + if (error != null) { + return callback(error) + } + if (project == null) { + return callback(new Error('project not found')) + } + + if (project.rootDoc_id != null) { + ProjectEntityHandler.getDocPathFromProjectByDocId( + project, + project.rootDoc_id, + (err, docPath) => { + if (docPath) return callback() + ProjectEntityUpdateHandler.unsetRootDoc(project_id, () => + ProjectRootDocManager.setRootDocAutomatically( + project_id, + callback + ) + ) + } + ) + } else { + return ProjectRootDocManager.setRootDocAutomatically( + project_id, + callback + ) + } + } + ) + }, + + _sortFileList(listToSort, rootDirectory, callback) { + if (callback == null) { + callback = function (error, result) {} + } + return async.mapLimit( + listToSort, + 5, + (filePath, cb) => + fs.stat(Path.join(rootDirectory, filePath), function (err, stat) { + if (err != null) { + return cb(err) + } + return cb(null, { + size: stat.size, + path: filePath, + elements: filePath.split(Path.sep).length, + name: Path.basename(filePath), + }) + }), + function (err, files) { + if (err != null) { + return callback(err) + } + + return callback( + null, + _.map( + files.sort(ProjectRootDocManager._rootDocSort), + file => file.path + ) + ) + } + ) + }, + + _rootDocSort(a, b) { + // sort first by folder depth + if (a.elements !== b.elements) { + return a.elements - b.elements + } + // ensure main.tex is at the start of each folder + if (a.name === 'main.tex' && b.name !== 'main.tex') { + return -1 + } + if (a.name !== 'main.tex' && b.name === 'main.tex') { + return 1 + } + // prefer smaller files + if (a.size !== b.size) { + return a.size - b.size + } + // otherwise, use the full path name + return a.path.localeCompare(b.path) + }, +} + +const promises = { + setRootDocAutomatically: promisify( + ProjectRootDocManager.setRootDocAutomatically + ), + + findRootDocFileFromDirectory: directoryPath => + new Promise((resolve, reject) => { + ProjectRootDocManager.findRootDocFileFromDirectory( + directoryPath, + (error, path, content) => { + if (error) { + reject(error) + } else { + resolve({ path, content }) + } + } + ) + }), + setRootDocFromName: promisify(ProjectRootDocManager.setRootDocFromName), +} + +ProjectRootDocManager.promises = promises + +module.exports = ProjectRootDocManager diff --git a/services/web/app/src/Features/Project/ProjectUpdateHandler.js b/services/web/app/src/Features/Project/ProjectUpdateHandler.js new file mode 100644 index 0000000000..4716ac40dd --- /dev/null +++ b/services/web/app/src/Features/Project/ProjectUpdateHandler.js @@ -0,0 +1,67 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { Project } = require('../../models/Project') +const logger = require('logger-sharelatex') + +module.exports = { + markAsUpdated(projectId, lastUpdatedAt, lastUpdatedBy, callback) { + if (callback == null) { + callback = function () {} + } + if (lastUpdatedAt == null) { + lastUpdatedAt = new Date() + } + + const conditions = { + _id: projectId, + lastUpdated: { $lt: lastUpdatedAt }, + } + + const update = { + lastUpdated: lastUpdatedAt || new Date().getTime(), + lastUpdatedBy, + } + return Project.updateOne(conditions, update, {}, callback) + }, + + markAsOpened(project_id, callback) { + const conditions = { _id: project_id } + const update = { lastOpened: Date.now() } + return Project.updateOne(conditions, update, {}, function (err) { + if (callback != null) { + return callback() + } + }) + }, + + markAsInactive(project_id, callback) { + const conditions = { _id: project_id } + const update = { active: false } + return Project.updateOne(conditions, update, {}, function (err) { + if (callback != null) { + return callback() + } + }) + }, + + markAsActive(project_id, callback) { + const conditions = { _id: project_id } + const update = { active: true } + return Project.updateOne(conditions, update, {}, function (err) { + if (callback != null) { + return callback() + } + }) + }, +} diff --git a/services/web/app/src/Features/Project/SafePath.js b/services/web/app/src/Features/Project/SafePath.js new file mode 100644 index 0000000000..10267f7857 --- /dev/null +++ b/services/web/app/src/Features/Project/SafePath.js @@ -0,0 +1,135 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +// This file is shared between the frontend and server code of web, so that +// filename validation is the same in both implementations. +// The logic in all copies must be kept in sync: +// app/src/Features/Project/SafePath.js +// frontend/js/ide/directives/SafePath.js +// frontend/js/features/file-tree/util/safe-path.js + +const load = function () { + let SafePath + // eslint-disable-next-line prefer-regex-literals + const BADCHAR_RX = new RegExp( + `\ +[\ +\\/\ +\\\\\ +\\*\ +\\u0000-\\u001F\ +\\u007F\ +\\u0080-\\u009F\ +\\uD800-\\uDFFF\ +]\ +`, + 'g' + ) + + // eslint-disable-next-line prefer-regex-literals + const BADFILE_RX = new RegExp( + `\ +(^\\.$)\ +|(^\\.\\.$)\ +|(^\\s+)\ +|(\\s+$)\ +`, + 'g' + ) + + // Put a block on filenames which match javascript property names, as they + // can cause exceptions where the code puts filenames into a hash. This is a + // temporary workaround until the code in other places is made safe against + // property names. + // + // The list of property names is taken from + // ['prototype'].concat(Object.getOwnPropertyNames(Object.prototype)) + // eslint-disable-next-line prefer-regex-literals + const BLOCKEDFILE_RX = new RegExp(`\ +^(\ +prototype\ +|constructor\ +|toString\ +|toLocaleString\ +|valueOf\ +|hasOwnProperty\ +|isPrototypeOf\ +|propertyIsEnumerable\ +|__defineGetter__\ +|__lookupGetter__\ +|__defineSetter__\ +|__lookupSetter__\ +|__proto__\ +)$\ +`) + + const MAX_PATH = 1024 // Maximum path length, in characters. This is fairly arbitrary. + + return (SafePath = { + // convert any invalid characters to underscores in the given filename + clean(filename) { + filename = filename.replace(BADCHAR_RX, '_') + // for BADFILE_RX replace any matches with an equal number of underscores + filename = filename.replace(BADFILE_RX, match => + new Array(match.length + 1).join('_') + ) + // replace blocked filenames 'prototype' with '@prototype' + filename = filename.replace(BLOCKEDFILE_RX, '@$1') + return filename + }, + + // returns whether the filename is 'clean' (does not contain any invalid + // characters or reserved words) + isCleanFilename(filename) { + return ( + SafePath.isAllowedLength(filename) && + !filename.match(BADCHAR_RX) && + !filename.match(BADFILE_RX) + ) + }, + + isBlockedFilename(filename) { + return BLOCKEDFILE_RX.test(filename) + }, + + // returns whether a full path is 'clean' - e.g. is a full or relative path + // that points to a file, and each element passes the rules in 'isCleanFilename' + isCleanPath(path) { + const elements = path.split('/') + + const lastElementIsEmpty = elements[elements.length - 1].length === 0 + if (lastElementIsEmpty) { + return false + } + + for (const element of Array.from(elements)) { + if (element.length > 0 && !SafePath.isCleanFilename(element)) { + return false + } + } + + // check for a top-level reserved name + if (BLOCKEDFILE_RX.test(path.replace(/^\/?/, ''))) { + return false + } // remove leading slash if present + + return true + }, + + isAllowedLength(pathname) { + return pathname.length > 0 && pathname.length <= MAX_PATH + }, + }) +} + +module.exports = load() diff --git a/services/web/app/src/Features/Publishers/PublishersGetter.js b/services/web/app/src/Features/Publishers/PublishersGetter.js new file mode 100644 index 0000000000..4397abb421 --- /dev/null +++ b/services/web/app/src/Features/Publishers/PublishersGetter.js @@ -0,0 +1,32 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let PublishersGetter +const UserMembershipsHandler = require('../UserMembership/UserMembershipsHandler') +const UserMembershipEntityConfigs = require('../UserMembership/UserMembershipEntityConfigs') +const logger = require('logger-sharelatex') +const _ = require('underscore') + +module.exports = PublishersGetter = { + getManagedPublishers(user_id, callback) { + if (callback == null) { + callback = function (error, managedPublishers) {} + } + return UserMembershipsHandler.getEntitiesByUser( + UserMembershipEntityConfigs.publisher, + user_id, + (error, managedPublishers) => callback(error, managedPublishers) + ) + }, +} diff --git a/services/web/app/src/Features/Referal/ReferalAllocator.js b/services/web/app/src/Features/Referal/ReferalAllocator.js new file mode 100644 index 0000000000..d67e147a93 --- /dev/null +++ b/services/web/app/src/Features/Referal/ReferalAllocator.js @@ -0,0 +1,51 @@ +const OError = require('@overleaf/o-error') +const { User } = require('../../models/User') +const FeaturesUpdater = require('../Subscription/FeaturesUpdater') + +module.exports = { + allocate(referalId, newUserId, referalSource, referalMedium, callback) { + if (callback == null) { + callback = function () {} + } + if (referalId == null) { + return callback(null) + } + + const query = { referal_id: referalId } + return User.findOne(query, { _id: 1 }, function (error, user) { + if (error != null) { + return callback(error) + } + if (user == null || user._id == null) { + return callback(null) + } + + if (referalSource === 'bonus') { + User.updateOne( + query, + { + $push: { + refered_users: newUserId, + }, + $inc: { + refered_user_count: 1, + }, + }, + {}, + function (err) { + if (err != null) { + OError.tag(err, 'something went wrong allocating referal', { + referalId, + newUserId, + }) + return callback(err) + } + FeaturesUpdater.refreshFeatures(user._id, 'referral', callback) + } + ) + } else { + callback() + } + }) + }, +} diff --git a/services/web/app/src/Features/Referal/ReferalConnect.js b/services/web/app/src/Features/Referal/ReferalConnect.js new file mode 100644 index 0000000000..890cbf7d7d --- /dev/null +++ b/services/web/app/src/Features/Referal/ReferalConnect.js @@ -0,0 +1,52 @@ +module.exports = { + use(req, res, next) { + if (req.query != null) { + if (req.query.referal != null) { + req.session.referal_id = req.query.referal + } else if (req.query.r != null) { + // Short hand for referal + req.session.referal_id = req.query.r + } else if (req.query.fb_ref != null) { + req.session.referal_id = req.query.fb_ref + } + + if (req.query.rm != null) { + // referal medium e.g. twitter, facebook, email + switch (req.query.rm) { + case 'fb': + req.session.referal_medium = 'facebook' + break + case 't': + req.session.referal_medium = 'twitter' + break + case 'gp': + req.session.referal_medium = 'google_plus' + break + case 'e': + req.session.referal_medium = 'email' + break + case 'd': + req.session.referal_medium = 'direct' + break + } + } + + if (req.query.rs != null) { + // referal source e.g. project share, bonus + switch (req.query.rs) { + case 'b': + req.session.referal_source = 'bonus' + break + case 'ps': + req.session.referal_source = 'public_share' + break + case 'ci': + req.session.referal_source = 'collaborator_invite' + break + } + } + } + + next() + }, +} diff --git a/services/web/app/src/Features/Referal/ReferalController.js b/services/web/app/src/Features/Referal/ReferalController.js new file mode 100644 index 0000000000..8ca733305d --- /dev/null +++ b/services/web/app/src/Features/Referal/ReferalController.js @@ -0,0 +1,22 @@ +const ReferalHandler = require('./ReferalHandler') +const SessionManager = require('../Authentication/SessionManager') + +module.exports = { + bonus(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + ReferalHandler.getReferedUsers( + userId, + (err, referedUsers, referedUserCount) => { + if (err) { + next(err) + } else { + res.render('referal/bonus', { + title: 'bonus_please_recommend_us', + refered_users: referedUsers, + refered_user_count: referedUserCount, + }) + } + } + ) + }, +} diff --git a/services/web/app/src/Features/Referal/ReferalFeatures.js b/services/web/app/src/Features/Referal/ReferalFeatures.js new file mode 100644 index 0000000000..14b921bd71 --- /dev/null +++ b/services/web/app/src/Features/Referal/ReferalFeatures.js @@ -0,0 +1,49 @@ +const _ = require('underscore') +const { User } = require('../../models/User') +const Settings = require('@overleaf/settings') + +let ReferalFeatures + +module.exports = ReferalFeatures = { + getBonusFeatures(userId, callback) { + if (callback == null) { + callback = function () {} + } + const query = { _id: userId } + User.findOne(query, { refered_user_count: 1 }, function (error, user) { + if (error) { + return callback(error) + } + if (user == null) { + return callback(new Error(`user not found ${userId} for assignBonus`)) + } + if (user.refered_user_count != null && user.refered_user_count > 0) { + const newFeatures = ReferalFeatures._calculateFeatures(user) + callback(null, newFeatures) + } else { + callback(null, {}) + } + }) + }, + + _calculateFeatures(user) { + const bonusLevel = ReferalFeatures._getBonusLevel(user) + return ( + (Settings.bonus_features != null + ? Settings.bonus_features[`${bonusLevel}`] + : undefined) || {} + ) + }, + + _getBonusLevel(user) { + let highestBonusLevel = 0 + _.each(_.keys(Settings.bonus_features), function (level) { + const levelIsLessThanUser = level <= user.refered_user_count + const levelIsMoreThanCurrentHighest = level >= highestBonusLevel + if (levelIsLessThanUser && levelIsMoreThanCurrentHighest) { + return (highestBonusLevel = level) + } + }) + return highestBonusLevel + }, +} diff --git a/services/web/app/src/Features/Referal/ReferalHandler.js b/services/web/app/src/Features/Referal/ReferalHandler.js new file mode 100644 index 0000000000..bcc60815dd --- /dev/null +++ b/services/web/app/src/Features/Referal/ReferalHandler.js @@ -0,0 +1,15 @@ +const { User } = require('../../models/User') + +module.exports = { + getReferedUsers(userId, callback) { + const projection = { refered_users: 1, refered_user_count: 1 } + User.findById(userId, projection, function (err, user) { + if (err) { + return callback(err) + } + const referedUsers = user.refered_users || [] + const referedUserCount = user.refered_user_count || referedUsers.length + callback(null, referedUsers, referedUserCount) + }) + }, +} diff --git a/services/web/app/src/Features/References/ReferencesController.js b/services/web/app/src/Features/References/ReferencesController.js new file mode 100644 index 0000000000..160da5f065 --- /dev/null +++ b/services/web/app/src/Features/References/ReferencesController.js @@ -0,0 +1,77 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ReferencesController +const logger = require('logger-sharelatex') +const ReferencesHandler = require('./ReferencesHandler') +const settings = require('@overleaf/settings') +const EditorRealTimeController = require('../Editor/EditorRealTimeController') + +module.exports = ReferencesController = { + index(req, res) { + const projectId = req.params.Project_id + const { shouldBroadcast } = req.body + const { docIds } = req.body + if (!docIds || !(docIds instanceof Array)) { + logger.err( + { projectId, docIds }, + "docIds is not valid, should be either Array or String 'ALL'" + ) + return res.sendStatus(400) + } + return ReferencesHandler.index(projectId, docIds, function (err, data) { + if (err != null) { + logger.err({ err, projectId }, 'error indexing all references') + return res.sendStatus(500) + } + return ReferencesController._handleIndexResponse( + req, + res, + projectId, + shouldBroadcast, + data + ) + }) + }, + + indexAll(req, res) { + const projectId = req.params.Project_id + const { shouldBroadcast } = req.body + return ReferencesHandler.indexAll(projectId, function (err, data) { + if (err != null) { + logger.err({ err, projectId }, 'error indexing all references') + return res.sendStatus(500) + } + return ReferencesController._handleIndexResponse( + req, + res, + projectId, + shouldBroadcast, + data + ) + }) + }, + + _handleIndexResponse(req, res, projectId, shouldBroadcast, data) { + if (data == null || data.keys == null) { + return res.json({ projectId, keys: [] }) + } + if (shouldBroadcast) { + EditorRealTimeController.emitToRoom( + projectId, + 'references:keys:updated', + data.keys + ) + } + return res.json(data) + }, +} diff --git a/services/web/app/src/Features/References/ReferencesHandler.js b/services/web/app/src/Features/References/ReferencesHandler.js new file mode 100644 index 0000000000..a81d144a63 --- /dev/null +++ b/services/web/app/src/Features/References/ReferencesHandler.js @@ -0,0 +1,224 @@ +/* eslint-disable + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ReferencesHandler +const OError = require('@overleaf/o-error') +const logger = require('logger-sharelatex') +const request = require('request') +const settings = require('@overleaf/settings') +const Features = require('../../infrastructure/Features') +const ProjectGetter = require('../Project/ProjectGetter') +const UserGetter = require('../User/UserGetter') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const _ = require('underscore') +const Async = require('async') + +const oneMinInMs = 60 * 1000 +const fiveMinsInMs = oneMinInMs * 5 + +if (!Features.hasFeature('references')) { + logger.log('references search not enabled') +} + +module.exports = ReferencesHandler = { + _buildDocUrl(projectId, docId) { + return `${settings.apis.docstore.url}/project/${projectId}/doc/${docId}/raw` + }, + + _buildFileUrl(projectId, fileId) { + return `${settings.apis.filestore.url}/project/${projectId}/file/${fileId}` + }, + + _findBibFileIds(project) { + const ids = [] + var _process = function (folder) { + _.each(folder.fileRefs || [], function (file) { + if ( + __guard__(file != null ? file.name : undefined, x1 => + x1.match(/^.*\.bib$/) + ) + ) { + return ids.push(file._id) + } + }) + return _.each(folder.folders || [], folder => _process(folder)) + } + _.each(project.rootFolder || [], rootFolder => _process(rootFolder)) + return ids + }, + + _findBibDocIds(project) { + const ids = [] + var _process = function (folder) { + _.each(folder.docs || [], function (doc) { + if ( + __guard__(doc != null ? doc.name : undefined, x1 => + x1.match(/^.*\.bib$/) + ) + ) { + return ids.push(doc._id) + } + }) + return _.each(folder.folders || [], folder => _process(folder)) + } + _.each(project.rootFolder || [], rootFolder => _process(rootFolder)) + return ids + }, + + _isFullIndex(project, callback) { + if (callback == null) { + callback = function (err, result) {} + } + return UserGetter.getUser( + project.owner_ref, + { features: true }, + function (err, owner) { + if (err != null) { + return callback(err) + } + const features = owner != null ? owner.features : undefined + return callback( + null, + (features != null ? features.references : undefined) === true || + (features != null ? features.referencesSearch : undefined) === true + ) + } + ) + }, + + indexAll(projectId, callback) { + if (callback == null) { + callback = function (err, data) {} + } + return ProjectGetter.getProject( + projectId, + { rootFolder: true, owner_ref: 1 }, + function (err, project) { + if (err) { + OError.tag(err, 'error finding project', { + projectId, + }) + return callback(err) + } + logger.log({ projectId }, 'indexing all bib files in project') + const docIds = ReferencesHandler._findBibDocIds(project) + const fileIds = ReferencesHandler._findBibFileIds(project) + return ReferencesHandler._doIndexOperation( + projectId, + project, + docIds, + fileIds, + callback + ) + } + ) + }, + + index(projectId, docIds, callback) { + if (callback == null) { + callback = function (err, data) {} + } + return ProjectGetter.getProject( + projectId, + { rootFolder: true, owner_ref: 1 }, + function (err, project) { + if (err) { + OError.tag(err, 'error finding project', { + projectId, + }) + return callback(err) + } + return ReferencesHandler._doIndexOperation( + projectId, + project, + docIds, + [], + callback + ) + } + ) + }, + + _doIndexOperation(projectId, project, docIds, fileIds, callback) { + if (!Features.hasFeature('references')) { + return callback() + } + return ReferencesHandler._isFullIndex(project, function (err, isFullIndex) { + if (err) { + OError.tag(err, 'error checking whether to do full index', { + projectId, + }) + return callback(err) + } + logger.log( + { projectId, docIds }, + 'flushing docs to mongo before calling references service' + ) + return Async.series( + docIds.map(docId => cb => + DocumentUpdaterHandler.flushDocToMongo(projectId, docId, cb) + ), + function (err) { + // continue + if (err) { + OError.tag(err, 'error flushing docs to mongo', { + projectId, + docIds, + }) + return callback(err) + } + const bibDocUrls = docIds.map(docId => + ReferencesHandler._buildDocUrl(projectId, docId) + ) + const bibFileUrls = fileIds.map(fileId => + ReferencesHandler._buildFileUrl(projectId, fileId) + ) + const allUrls = bibDocUrls.concat(bibFileUrls) + return request.post( + { + url: `${settings.apis.references.url}/project/${projectId}/index`, + json: { + docUrls: allUrls, + fullIndex: isFullIndex, + }, + }, + function (err, res, data) { + if (err) { + OError.tag(err, 'error communicating with references api', { + projectId, + }) + return callback(err) + } + if (res.statusCode >= 200 && res.statusCode < 300) { + logger.log({ projectId }, 'got keys from references api') + return callback(null, data) + } else { + err = new Error( + `references api responded with non-success code: ${res.statusCode}` + ) + return callback(err) + } + } + ) + } + ) + }) + }, +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/SamlLog/SamlLogHandler.js b/services/web/app/src/Features/SamlLog/SamlLogHandler.js new file mode 100644 index 0000000000..354eaa6ac7 --- /dev/null +++ b/services/web/app/src/Features/SamlLog/SamlLogHandler.js @@ -0,0 +1,32 @@ +const { SamlLog } = require('../../models/SamlLog') +const logger = require('logger-sharelatex') + +function log(providerId, sessionId, data) { + try { + const samlLog = new SamlLog() + samlLog.providerId = providerId = (providerId || '').toString() + samlLog.sessionId = sessionId = (sessionId || '').toString().substr(0, 8) + try { + samlLog.jsonData = JSON.stringify(data) + } catch (err) { + // log but continue on data errors + logger.error( + { err, sessionId, providerId }, + 'SamlLog JSON.stringify Error' + ) + } + samlLog.save(err => { + if (err) { + logger.error({ err, sessionId, providerId }, 'SamlLog Error') + } + }) + } catch (err) { + logger.error({ err, sessionId, providerId }, 'SamlLog Error') + } +} + +const SamlLogHandler = { + log, +} + +module.exports = SamlLogHandler diff --git a/services/web/app/src/Features/Security/LoginRateLimiter.js b/services/web/app/src/Features/Security/LoginRateLimiter.js new file mode 100644 index 0000000000..21b2efd5e9 --- /dev/null +++ b/services/web/app/src/Features/Security/LoginRateLimiter.js @@ -0,0 +1,30 @@ +const RateLimiter = require('../../infrastructure/RateLimiter') +const { promisifyAll } = require('../../util/promises') + +const ONE_MIN = 60 +const ATTEMPT_LIMIT = 10 + +function processLoginRequest(email, callback) { + const opts = { + endpointName: 'login', + throttle: ATTEMPT_LIMIT, + timeInterval: ONE_MIN * 2, + subjectName: email, + } + RateLimiter.addCount(opts, (err, shouldAllow) => callback(err, shouldAllow)) +} + +function recordSuccessfulLogin(email, callback) { + if (callback == null) { + callback = function () {} + } + RateLimiter.clearRateLimit('login', email, callback) +} + +const LoginRateLimiter = { + processLoginRequest, + recordSuccessfulLogin, +} +LoginRateLimiter.promises = promisifyAll(LoginRateLimiter) + +module.exports = LoginRateLimiter diff --git a/services/web/app/src/Features/Security/OneTimeTokenHandler.js b/services/web/app/src/Features/Security/OneTimeTokenHandler.js new file mode 100644 index 0000000000..44684edf89 --- /dev/null +++ b/services/web/app/src/Features/Security/OneTimeTokenHandler.js @@ -0,0 +1,75 @@ +const crypto = require('crypto') +const { db } = require('../../infrastructure/mongodb') +const Errors = require('../Errors/Errors') +const { promisifyAll } = require('../../util/promises') + +const ONE_HOUR_IN_S = 60 * 60 + +const OneTimeTokenHandler = { + getNewToken(use, data, options, callback) { + // options is optional + if (!options) { + options = {} + } + if (!callback) { + callback = function (error, data) {} + } + if (typeof options === 'function') { + callback = options + options = {} + } + const expiresIn = options.expiresIn || ONE_HOUR_IN_S + const createdAt = new Date() + const expiresAt = new Date(createdAt.getTime() + expiresIn * 1000) + const token = crypto.randomBytes(32).toString('hex') + db.tokens.insertOne( + { + use, + token, + data, + createdAt, + expiresAt, + }, + function (error) { + if (error) { + return callback(error) + } + callback(null, token) + } + ) + }, + + getValueFromTokenAndExpire(use, token, callback) { + if (!callback) { + callback = function (error, data) {} + } + const now = new Date() + db.tokens.findOneAndUpdate( + { + use, + token, + expiresAt: { $gt: now }, + usedAt: { $exists: false }, + }, + { + $set: { + usedAt: now, + }, + }, + function (error, result) { + if (error) { + return callback(error) + } + const token = result.value + if (!token) { + return callback(new Errors.NotFoundError('no token found')) + } + callback(null, token.data) + } + ) + }, +} + +OneTimeTokenHandler.promises = promisifyAll(OneTimeTokenHandler) + +module.exports = OneTimeTokenHandler diff --git a/services/web/app/src/Features/Security/RateLimiterMiddleware.js b/services/web/app/src/Features/Security/RateLimiterMiddleware.js new file mode 100644 index 0000000000..bf6f175c45 --- /dev/null +++ b/services/web/app/src/Features/Security/RateLimiterMiddleware.js @@ -0,0 +1,85 @@ +const RateLimiter = require('../../infrastructure/RateLimiter') +const logger = require('logger-sharelatex') +const SessionManager = require('../Authentication/SessionManager') +const LoginRateLimiter = require('./LoginRateLimiter') +const settings = require('@overleaf/settings') + +/* + Do not allow more than opts.maxRequests from a single client in + opts.timeInterval. Pass an array of opts.params to segment this based on + parameters in the request URL, e.g.: + + app.get "/project/:project_id", RateLimiterMiddleware.rateLimit(endpointName: "open-editor", params: ["project_id"]) + + will rate limit each project_id separately. + + Unique clients are identified by user_id if logged in, and IP address if not. +*/ +function rateLimit(opts) { + return function (req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) || req.ip + if ( + settings.smokeTest && + settings.smokeTest.userId && + settings.smokeTest.userId.toString() === userId.toString() + ) { + // ignore smoke test user + return next() + } + const params = (opts.params || []).map(p => req.params[p]) + params.push(userId) + let subjectName = params.join(':') + if (opts.ipOnly) { + subjectName = req.ip + } + if (opts.endpointName == null) { + throw new Error('no endpointName provided') + } + const options = { + endpointName: opts.endpointName, + timeInterval: opts.timeInterval || 60, + subjectName, + throttle: opts.maxRequests || 6, + } + return RateLimiter.addCount(options, function (error, canContinue) { + if (error != null) { + return next(error) + } + if (canContinue) { + return next() + } else { + logger.warn(options, 'rate limit exceeded') + res.status(429) // Too many requests + res.write('Rate limit reached, please try again later') + return res.end() + } + }) + } +} + +function loginRateLimit(req, res, next) { + const { email } = req.body + if (!email) { + return next() + } + LoginRateLimiter.processLoginRequest(email, function (err, isAllowed) { + if (err) { + return next(err) + } + if (isAllowed) { + return next() + } else { + logger.warn({ email }, 'rate limit exceeded') + res.status(429) // Too many requests + res.write('Rate limit reached, please try again later') + return res.end() + } + }) +} + +const RateLimiterMiddleware = { + rateLimit, + loginRateLimit, +} + +module.exports = RateLimiterMiddleware diff --git a/services/web/app/src/Features/ServerAdmin/AdminController.js b/services/web/app/src/Features/ServerAdmin/AdminController.js new file mode 100644 index 0000000000..5c77a5b963 --- /dev/null +++ b/services/web/app/src/Features/ServerAdmin/AdminController.js @@ -0,0 +1,180 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ + +const metrics = require('@overleaf/metrics') +const logger = require('logger-sharelatex') +const _ = require('underscore') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const Settings = require('@overleaf/settings') +const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') +const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher') +const EditorRealTimeController = require('../Editor/EditorRealTimeController') +const SystemMessageManager = require('../SystemMessages/SystemMessageManager') + +const oneMinInMs = 60 * 1000 + +var updateOpenConnetionsMetrics = function () { + metrics.gauge( + 'open_connections.socketio', + __guard__( + __guard__( + __guard__(require('../../infrastructure/Server').io, x2 => x2.sockets), + x1 => x1.clients() + ), + x => x.length + ) + ) + metrics.gauge( + 'open_connections.http', + _.size(__guard__(require('http').globalAgent, x3 => x3.sockets)) + ) + metrics.gauge( + 'open_connections.https', + _.size(__guard__(require('https').globalAgent, x4 => x4.sockets)) + ) + return setTimeout(updateOpenConnetionsMetrics, oneMinInMs) +} + +setTimeout(updateOpenConnetionsMetrics, oneMinInMs) + +const AdminController = { + index: (req, res, next) => { + let agents, url + let agent + const openSockets = {} + const object = require('http').globalAgent.sockets + for (url in object) { + agents = object[url] + openSockets[`http://${url}`] = (() => { + const result = [] + for (agent of Array.from(agents)) { + result.push(agent._httpMessage.path) + } + return result + })() + } + const object1 = require('https').globalAgent.sockets + for (url in object1) { + agents = object1[url] + openSockets[`https://${url}`] = (() => { + const result1 = [] + for (agent of Array.from(agents)) { + result1.push(agent._httpMessage.path) + } + return result1 + })() + } + + return SystemMessageManager.getMessagesFromDB(function ( + error, + systemMessages + ) { + if (error != null) { + return next(error) + } + return res.render('admin/index', { + title: 'System Admin', + openSockets, + systemMessages, + }) + }) + }, + + registerNewUser(req, res, next) { + return res.render('admin/register') + }, + + disconnectAllUsers: (req, res) => { + logger.warn('disconecting everyone') + const delay = (req.query && req.query.delay) > 0 ? req.query.delay : 10 + EditorRealTimeController.emitToAll( + 'forceDisconnect', + 'Sorry, we are performing a quick update to the editor and need to close it down. Please refresh the page to continue.', + delay + ) + return res.sendStatus(200) + }, + + unregisterServiceWorker: (req, res) => { + logger.warn('unregistering service worker for all users') + EditorRealTimeController.emitToAll('unregisterServiceWorker') + return res.sendStatus(200) + }, + + openEditor(req, res) { + logger.warn('opening editor') + Settings.editorIsOpen = true + return res.sendStatus(200) + }, + + closeEditor(req, res) { + logger.warn('closing editor') + Settings.editorIsOpen = req.body.isOpen + return res.sendStatus(200) + }, + + writeAllToMongo(req, res) { + logger.log('writing all docs to mongo') + Settings.mongo.writeAll = true + return DocumentUpdaterHandler.flushAllDocsToMongo(function () { + logger.log('all docs have been saved to mongo') + return res.sendStatus(200) + }) + }, + + flushProjectToTpds(req, res) { + return TpdsProjectFlusher.flushProjectToTpds(req.body.project_id, err => + res.sendStatus(200) + ) + }, + + pollDropboxForUser(req, res) { + const { user_id } = req.body + return TpdsUpdateSender.pollDropboxForUser(user_id, () => + res.sendStatus(200) + ) + }, + + createMessage(req, res, next) { + return SystemMessageManager.createMessage( + req.body.content, + function (error) { + if (error != null) { + return next(error) + } + return res.sendStatus(200) + } + ) + }, + + clearMessages(req, res, next) { + return SystemMessageManager.clearMessages(function (error) { + if (error != null) { + return next(error) + } + return res.sendStatus(200) + }) + }, +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} + +module.exports = AdminController diff --git a/services/web/app/src/Features/Spelling/SpellingController.js b/services/web/app/src/Features/Spelling/SpellingController.js new file mode 100644 index 0000000000..baf0a29d83 --- /dev/null +++ b/services/web/app/src/Features/Spelling/SpellingController.js @@ -0,0 +1,47 @@ +const request = require('request') +const Settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const SessionManager = require('../Authentication/SessionManager') + +const TEN_SECONDS = 1000 * 10 + +const languageCodeIsSupported = code => + Settings.languages.some(lang => lang.code === code) + +module.exports = { + proxyRequestToSpellingApi(req, res) { + const { language } = req.body + + let url = req.url.slice('/spelling'.length) + + if (url === '/check') { + if (!language) { + logger.error('"language" field should be included for spell checking') + return res.status(422).send(JSON.stringify({ misspellings: [] })) + } + + if (!languageCodeIsSupported(language)) { + // this log statement can be changed to 'error' once projects with + // unsupported languages are removed from the DB + logger.info({ language }, 'language not supported') + return res.status(422).send(JSON.stringify({ misspellings: [] })) + } + } + + const userId = SessionManager.getLoggedInUserId(req.session) + url = `/user/${userId}${url}` + req.headers.Host = Settings.apis.spelling.host + return request({ + url: Settings.apis.spelling.url + url, + method: req.method, + headers: req.headers, + json: req.body, + timeout: TEN_SECONDS, + }) + .on('error', function (error) { + logger.error({ err: error }, 'Spelling API error') + return res.status(500).end() + }) + .pipe(res) + }, +} diff --git a/services/web/app/src/Features/Spelling/SpellingHandler.js b/services/web/app/src/Features/Spelling/SpellingHandler.js new file mode 100644 index 0000000000..a1664fe44a --- /dev/null +++ b/services/web/app/src/Features/Spelling/SpellingHandler.js @@ -0,0 +1,85 @@ +const request = require('request') +const Settings = require('@overleaf/settings') +const OError = require('@overleaf/o-error') + +const TIMEOUT = 10 * 1000 + +module.exports = { + getUserDictionary(userId, callback) { + const url = `${Settings.apis.spelling.url}/user/${userId}` + request.get({ url: url, timeout: TIMEOUT }, (error, response) => { + if (error) { + return callback( + OError.tag(error, 'error getting user dictionary', { error, userId }) + ) + } + + if (response.statusCode < 200 || response.statusCode >= 300) { + return callback( + new OError( + 'Non-success code from spelling API when getting user dictionary', + { userId, statusCode: response.statusCode } + ) + ) + } + + callback(null, JSON.parse(response.body)) + }) + }, + + deleteWordFromUserDictionary(userId, word, callback) { + const url = `${Settings.apis.spelling.url}/user/${userId}/unlearn` + request.post( + { + url: url, + json: { + word, + }, + timeout: TIMEOUT, + }, + (error, response) => { + if (error) { + return callback( + OError.tag(error, 'error deleting word from user dictionary', { + userId, + word, + }) + ) + } + + if (response.statusCode < 200 || response.statusCode >= 300) { + return callback( + new OError( + 'Non-success code from spelling API when removing word from user dictionary', + { userId, word, statusCode: response.statusCode } + ) + ) + } + + callback() + } + ) + }, + + deleteUserDictionary(userId, callback) { + const url = `${Settings.apis.spelling.url}/user/${userId}` + request.delete({ url: url, timeout: TIMEOUT }, (error, response) => { + if (error) { + return callback( + OError.tag(error, 'error deleting user dictionary', { userId }) + ) + } + + if (response.statusCode < 200 || response.statusCode >= 300) { + return callback( + new OError( + 'Non-success code from spelling API when removing user dictionary', + { userId, statusCode: response.statusCode } + ) + ) + } + + callback() + }) + }, +} diff --git a/services/web/app/src/Features/SplitTests/SplitTestCache.js b/services/web/app/src/Features/SplitTests/SplitTestCache.js new file mode 100644 index 0000000000..204baf715e --- /dev/null +++ b/services/web/app/src/Features/SplitTests/SplitTestCache.js @@ -0,0 +1,25 @@ +const SplitTestManager = require('./SplitTestManager') +const { SplitTest } = require('../../models/SplitTest') +const { CacheLoader } = require('cache-flow') + +class SplitTestCache extends CacheLoader { + constructor() { + super('split-test', { + expirationTime: 60, // 1min in seconds + }) + } + + async load(name) { + return await SplitTestManager.getSplitTestByName(name) + } + + serialize(value) { + return value ? value.toObject() : undefined + } + + deserialize(value) { + return new SplitTest(value) + } +} + +module.exports = new SplitTestCache() diff --git a/services/web/app/src/Features/SplitTests/SplitTestHandler.js b/services/web/app/src/Features/SplitTests/SplitTestHandler.js new file mode 100644 index 0000000000..0a08aad2de --- /dev/null +++ b/services/web/app/src/Features/SplitTests/SplitTestHandler.js @@ -0,0 +1,127 @@ +const UserGetter = require('../User/UserGetter') +const UserUpdater = require('../User/UserUpdater') +const AnalyticsManager = require('../Analytics/AnalyticsManager') +const Settings = require('@overleaf/settings') +const _ = require('lodash') +const crypto = require('crypto') +const OError = require('@overleaf/o-error') +const { callbackify } = require('util') + +const duplicateSplitTest = _.findKey( + _.groupBy(Settings.splitTests, 'id'), + group => { + return group.length > 1 + } +) +if (duplicateSplitTest) { + throw new OError( + `Split test IDs must be unique: ${duplicateSplitTest} is defined at least twice` + ) +} + +const ACTIVE_SPLIT_TESTS = [] +for (const splitTest of Settings.splitTests) { + for (const variant of splitTest.variants) { + if (variant.id === 'default') { + throw new OError( + `Split test variant ID cannot be 'default' (reserved value), defined in split test ${JSON.stringify( + splitTest + )}` + ) + } + } + const totalVariantsRolloutPercent = _.sumBy( + splitTest.variants, + 'rolloutPercent' + ) + if (splitTest.active) { + if (totalVariantsRolloutPercent > 100) { + for (const variant of splitTest.variants) { + variant.rolloutPercent = + (variant.rolloutPercent * 100) / totalVariantsRolloutPercent + } + } + if (totalVariantsRolloutPercent > 0) { + ACTIVE_SPLIT_TESTS.push(splitTest) + } + } +} + +async function getTestSegmentation(userId, splitTestId) { + const splitTest = _.find(ACTIVE_SPLIT_TESTS, ['id', splitTestId]) + if (splitTest) { + const alreadyAssignedVariant = await getAlreadyAssignedVariant( + userId, + splitTestId + ) + if (alreadyAssignedVariant) { + return { + enabled: true, + variant: alreadyAssignedVariant, + } + } else { + const variant = await assignUserToVariant(userId, splitTest) + return { + enabled: true, + variant, + } + } + } + return { + enabled: false, + } +} + +async function getAlreadyAssignedVariant(userId, splitTestId) { + const user = await UserGetter.promises.getUser(userId, { splitTests: 1 }) + if (user && user.splitTests) { + return user.splitTests[splitTestId] + } + return undefined +} + +async function assignUserToVariant(userId, splitTest) { + let userIdAsPercentile = await _getPercentile(userId, splitTest.id) + let selectedVariant = 'default' + for (const variant of splitTest.variants) { + if (userIdAsPercentile < variant.rolloutPercent) { + selectedVariant = variant.id + break + } else { + userIdAsPercentile -= variant.rolloutPercent + } + } + await UserUpdater.promises.updateUser(userId, { + $set: { + [`splitTests.${splitTest.id}`]: selectedVariant, + }, + }) + AnalyticsManager.setUserProperty( + userId, + `split-test-${splitTest.id}`, + selectedVariant + ) + return selectedVariant +} + +function _getPercentile(userId, splitTestId) { + const hash = crypto + .createHash('md5') + .update(userId + splitTestId) + .digest('hex') + const hashPrefix = hash.substr(0, 8) + return Math.floor((parseInt(hashPrefix, 16) / 0xffffffff) * 100) +} + +module.exports = { + /** + * @deprecated: use SplitTestV2Handler.getAssignment instead + */ + getTestSegmentation: callbackify(getTestSegmentation), + promises: { + /** + * @deprecated: use SplitTestV2Handler.promises.getAssignment instead + */ + getTestSegmentation, + }, +} diff --git a/services/web/app/src/Features/SplitTests/SplitTestManager.js b/services/web/app/src/Features/SplitTests/SplitTestManager.js new file mode 100644 index 0000000000..6a321a377b --- /dev/null +++ b/services/web/app/src/Features/SplitTests/SplitTestManager.js @@ -0,0 +1,230 @@ +const { SplitTest } = require('../../models/SplitTest') +const OError = require('@overleaf/o-error') +const _ = require('lodash') + +const ALPHA_PHASE = 'alpha' +const BETA_PHASE = 'beta' +const RELEASE_PHASE = 'release' + +async function getSplitTests() { + try { + return await SplitTest.find().exec() + } catch (error) { + throw OError.tag(error, 'Failed to get split tests list') + } +} + +async function getSplitTestByName(name) { + try { + return await SplitTest.findOne({ name }).exec() + } catch (error) { + throw OError.tag(error, 'Failed to get split test', { name }) + } +} + +async function createSplitTest(name, configuration) { + const stripedVariants = [] + let stripeStart = 0 + _checkNewVariantsConfiguration([], configuration.variants) + for (const variant of configuration.variants) { + stripedVariants.push({ + name: variant.name, + active: variant.active, + rolloutPercent: variant.rolloutPercent, + rolloutStripes: [ + { + start: stripeStart, + end: stripeStart + variant.rolloutPercent, + }, + ], + }) + stripeStart += variant.rolloutPercent + } + const splitTest = new SplitTest({ + name, + versions: [ + { + versionNumber: 1, + phase: configuration.phase, + active: configuration.active, + variants: stripedVariants, + }, + ], + }) + return _saveSplitTest(splitTest) +} + +async function updateSplitTest(name, configuration) { + const splitTest = await getSplitTestByName(name) + if (splitTest) { + const lastVersion = splitTest.getCurrentVersion().toObject() + if (configuration.phase !== lastVersion.phase) { + throw new OError( + `Cannot update with different phase - use switchToNextPhase endpoint instead` + ) + } + _checkNewVariantsConfiguration(lastVersion.variants, configuration.variants) + const updatedVariants = _updateVariantsWithNewConfiguration( + lastVersion.variants, + configuration.variants + ) + splitTest.versions.push({ + versionNumber: lastVersion.versionNumber + 1, + phase: configuration.phase, + active: configuration.active, + variants: updatedVariants, + }) + return _saveSplitTest(splitTest) + } else { + throw new OError(`Cannot update split test '${name}': not found`) + } +} + +async function switchToNextPhase(name) { + const splitTest = await getSplitTestByName(name) + if (splitTest) { + const lastVersionCopy = splitTest.getCurrentVersion().toObject() + lastVersionCopy.versionNumber++ + if (lastVersionCopy.phase === ALPHA_PHASE) { + lastVersionCopy.phase = BETA_PHASE + } else if (lastVersionCopy.phase === BETA_PHASE) { + if (splitTest.forbidReleasePhase) { + throw new OError('Switch to release phase is disabled for this test') + } + lastVersionCopy.phase = RELEASE_PHASE + } else if (splitTest.phase === RELEASE_PHASE) { + throw new OError( + `Split test with ID '${name}' is already in the release phase` + ) + } + for (const variant of lastVersionCopy.variants) { + variant.rolloutPercent = 0 + variant.rolloutStripes = [] + } + splitTest.versions.push(lastVersionCopy) + return _saveSplitTest(splitTest) + } else { + throw new OError( + `Cannot switch split test with ID '${name}' to next phase: not found` + ) + } +} + +async function revertToPreviousVersion(name, versionNumber) { + const splitTest = await getSplitTestByName(name) + if (splitTest) { + if (splitTest.versions.length <= 1) { + throw new OError( + `Cannot revert split test with ID '${name}' to previous version: split test must have at least 2 versions` + ) + } + const previousVersion = splitTest.getVersion(versionNumber) + if (!previousVersion) { + throw new OError( + `Cannot revert split test with ID '${name}' to version number ${versionNumber}: version not found` + ) + } + const lastVersion = splitTest.getCurrentVersion() + if ( + lastVersion.phase === RELEASE_PHASE && + previousVersion.phase !== RELEASE_PHASE + ) { + splitTest.forbidReleasePhase = true + } + const previousVersionCopy = previousVersion.toObject() + previousVersionCopy.versionNumber = lastVersion.versionNumber + 1 + splitTest.versions.push(previousVersionCopy) + return _saveSplitTest(splitTest) + } else { + throw new OError( + `Cannot revert split test with ID '${name}' to previous version: not found` + ) + } +} + +function _checkNewVariantsConfiguration(variants, newVariantsConfiguration) { + const totalRolloutPercentage = _getTotalRolloutPercentage( + newVariantsConfiguration + ) + if (totalRolloutPercentage > 100) { + throw new OError(`Total variants rollout percentage cannot exceed 100`) + } + for (const variant of variants) { + const newVariantConfiguration = _.find(newVariantsConfiguration, { + name: variant.name, + }) + if (!newVariantConfiguration) { + throw new OError( + `Variant defined in previous version as ${JSON.stringify( + variant + )} cannot be removed in new configuration: either set it inactive or create a new split test` + ) + } + if (newVariantConfiguration.rolloutPercent < variant.rolloutPercent) { + throw new OError( + `Rollout percentage for variant defined in previous version as ${JSON.stringify( + variant + )} cannot be decreased: revert to a previous configuration instead` + ) + } + } +} + +function _updateVariantsWithNewConfiguration( + variants, + newVariantsConfiguration +) { + let totalRolloutPercentage = _getTotalRolloutPercentage(variants) + const variantsCopy = _.clone(variants) + for (const newVariantConfig of newVariantsConfiguration) { + const variant = _.find(variantsCopy, { name: newVariantConfig.name }) + if (!variant) { + variantsCopy.push({ + name: newVariantConfig.name, + active: newVariantConfig.active, + rolloutPercent: newVariantConfig.rolloutPercent, + rolloutStripes: [ + { + start: totalRolloutPercentage, + end: totalRolloutPercentage + newVariantConfig.rolloutPercent, + }, + ], + }) + totalRolloutPercentage += newVariantConfig.rolloutPercent + } else if (variant.rolloutPercent < newVariantConfig.rolloutPercent) { + const newStripeSize = + newVariantConfig.rolloutPercent - variant.rolloutPercent + variant.active = newVariantConfig.active + variant.rolloutPercent = newVariantConfig.rolloutPercent + variant.rolloutStripes.push({ + start: totalRolloutPercentage, + end: totalRolloutPercentage + newStripeSize, + }) + totalRolloutPercentage += newStripeSize + } + } + return variantsCopy +} + +function _getTotalRolloutPercentage(variants) { + return _.sumBy(variants, 'rolloutPercent') +} + +async function _saveSplitTest(splitTest) { + try { + return (await splitTest.save()).toObject() + } catch (error) { + throw OError.tag(error, 'Failed to save split test', { + splitTest: JSON.stringify(splitTest), + }) + } +} + +module.exports = { + getSplitTestByName, + getSplitTests, + createSplitTest, + updateSplitTest, + switchToNextPhase, + revertToPreviousVersion, +} diff --git a/services/web/app/src/Features/SplitTests/SplitTestV2Handler.js b/services/web/app/src/Features/SplitTests/SplitTestV2Handler.js new file mode 100644 index 0000000000..8526746e5a --- /dev/null +++ b/services/web/app/src/Features/SplitTests/SplitTestV2Handler.js @@ -0,0 +1,177 @@ +const UserGetter = require('../User/UserGetter') +const UserUpdater = require('../User/UserUpdater') +const AnalyticsManager = require('../Analytics/AnalyticsManager') +const crypto = require('crypto') +const _ = require('lodash') +const { callbackify } = require('util') +const splitTestCache = require('./SplitTestCache') + +const DEFAULT_VARIANT = 'default' +const ALPHA_PHASE = 'alpha' +const BETA_PHASE = 'beta' + +/** + * Get the assignment of a user to a split test. + * + * @example + * // Assign user and record an event + * + * const assignment = await SplitTestV2Handler.getAssignment(userId, 'example-project') + * if (assignment.variant === 'awesome-new-version') { + * // execute my awesome change + * } + * else { + * // execute the default behaviour (control group) + * } + * // then record an event + * AnalyticsManager.recordEvent(userId, 'example-project-created', { + * projectId: project._id, + * ...assignment.analytics.segmentation + * }) + * + * @param userId the user's ID + * @param splitTestName the unique name of the split test + * @param options {Object} - for test purposes only, to force the synchronous update of the user's profile + * @returns {Promise<{variant: string, analytics: {segmentation: {splitTest: string, variant: string, phase: string, versionNumber: number}|{}}}>} + */ +async function getAssignment(userId, splitTestName, options) { + const splitTest = await splitTestCache.get(splitTestName) + + if (splitTest) { + const currentVersion = splitTest.getCurrentVersion() + if (currentVersion.active) { + const { + activeForUser, + selectedVariantName, + phase, + versionNumber, + } = await _getAssignmentMetadata(userId, splitTest) + if (activeForUser) { + const assignmentConfig = { + userId, + splitTestName, + variantName: selectedVariantName, + phase, + versionNumber, + } + if (options && options.sync === true) { + await _updateVariantAssignment(assignmentConfig) + } else { + _updateVariantAssignment(assignmentConfig) + } + return { + variant: selectedVariantName, + analytics: { + segmentation: { + splitTest: splitTestName, + variant: selectedVariantName, + phase, + versionNumber, + }, + }, + } + } + } + } + return { + variant: DEFAULT_VARIANT, + analytics: { + segmentation: {}, + }, + } +} + +async function _getAssignmentMetadata(userId, splitTest) { + const currentVersion = splitTest.getCurrentVersion() + const phase = currentVersion.phase + if ([ALPHA_PHASE, BETA_PHASE].includes(phase)) { + const user = await _getUser(userId) + if ( + (phase === ALPHA_PHASE && !(user && user.alphaProgram)) || + (phase === BETA_PHASE && !(user && user.betaProgram)) + ) { + return { + activeForUser: false, + } + } + } + const percentile = _getPercentile(userId, splitTest.name, phase) + const selectedVariantName = _getVariantFromPercentile( + currentVersion.variants, + percentile + ) + return { + activeForUser: true, + selectedVariantName: selectedVariantName || DEFAULT_VARIANT, + phase, + versionNumber: currentVersion.versionNumber, + } +} + +function _getPercentile(userId, splitTestName, splitTestPhase) { + const hash = crypto + .createHash('md5') + .update(userId + splitTestName + splitTestPhase) + .digest('hex') + const hashPrefix = hash.substr(0, 8) + return Math.floor( + ((parseInt(hashPrefix, 16) % 0xffffffff) / 0xffffffff) * 100 + ) +} + +function _getVariantFromPercentile(variants, percentile) { + for (const variant of variants) { + for (const stripe of variant.rolloutStripes) { + if (percentile >= stripe.start && percentile < stripe.end) { + return variant.name + } + } + } +} + +async function _updateVariantAssignment({ + userId, + splitTestName, + phase, + versionNumber, + variantName, +}) { + const user = await _getUser(userId) + if (user) { + const assignedSplitTests = user.splitTests || [] + const assignmentLog = assignedSplitTests[splitTestName] || [] + const existingAssignment = _.find(assignmentLog, { versionNumber }) + if (!existingAssignment) { + await UserUpdater.promises.updateUser(userId, { + $addToSet: { + [`splitTests.${splitTestName}`]: { + variantName, + versionNumber, + phase, + assignedAt: new Date(), + }, + }, + }) + AnalyticsManager.setUserProperty( + userId, + `split-test-${splitTestName}-${versionNumber}`, + variantName + ) + } + } +} + +async function _getUser(id) { + return UserGetter.promises.getUser(id, { + splitTests: 1, + alphaProgram: 1, + betaProgram: 1, + }) +} + +module.exports = { + getAssignment: callbackify(getAssignment), + promises: { + getAssignment, + }, +} diff --git a/services/web/app/src/Features/StaticPages/HomeController.js b/services/web/app/src/Features/StaticPages/HomeController.js new file mode 100644 index 0000000000..afc7d9638d --- /dev/null +++ b/services/web/app/src/Features/StaticPages/HomeController.js @@ -0,0 +1,68 @@ +/* eslint-disable + node/handle-callback-err, + max-len, + no-path-concat, + no-unused-vars, + node/no-deprecated-api, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let HomeController +const Features = require('../../infrastructure/Features') + +const Path = require('path') +const fs = require('fs') + +const ErrorController = require('../Errors/ErrorController') +const SessionManager = require('../Authentication/SessionManager') + +const homepageExists = fs.existsSync( + Path.resolve(__dirname + '/../../../views/external/home/v2.pug') +) + +module.exports = HomeController = { + index(req, res) { + if (SessionManager.isUserLoggedIn(req.session)) { + if (req.query.scribtex_path != null) { + return res.redirect(`/project?scribtex_path=${req.query.scribtex_path}`) + } else { + return res.redirect('/project') + } + } else { + return HomeController.home(req, res) + } + }, + + home(req, res, next) { + if (Features.hasFeature('homepage') && homepageExists) { + return res.render('external/home/v2') + } else { + return res.redirect('/login') + } + }, + + externalPage(page, title) { + return function (req, res, next) { + if (next == null) { + next = function (error) {} + } + const path = Path.resolve( + __dirname + `/../../../views/external/${page}.pug` + ) + return fs.exists(path, function (exists) { + // No error in this callback - old method in Node.js! + if (exists) { + return res.render(`external/${page}.pug`, { title }) + } else { + return ErrorController.notFound(req, res, next) + } + }) + } + }, +} diff --git a/services/web/app/src/Features/StaticPages/StaticPageHelpers.js b/services/web/app/src/Features/StaticPages/StaticPageHelpers.js new file mode 100644 index 0000000000..6f822e8259 --- /dev/null +++ b/services/web/app/src/Features/StaticPages/StaticPageHelpers.js @@ -0,0 +1,31 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const extensionsToProxy = [ + '.png', + '.xml', + '.jpeg', + '.json', + '.zip', + '.eps', + '.gif', + '.jpg', +] +const _ = require('underscore') + +module.exports = { + shouldProxy(url) { + const shouldProxy = _.find( + extensionsToProxy, + extension => url.indexOf(extension) !== -1 + ) + return shouldProxy + }, +} diff --git a/services/web/app/src/Features/StaticPages/StaticPagesRouter.js b/services/web/app/src/Features/StaticPages/StaticPagesRouter.js new file mode 100644 index 0000000000..72f8fe61f3 --- /dev/null +++ b/services/web/app/src/Features/StaticPages/StaticPagesRouter.js @@ -0,0 +1,37 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const HomeController = require('./HomeController') +const UniversityController = require('./UniversityController') + +module.exports = { + apply(webRouter, apiRouter) { + webRouter.get('/', HomeController.index) + webRouter.get('/home', HomeController.home) + + webRouter.get( + '/planned_maintenance', + HomeController.externalPage('planned_maintenance', 'Planned Maintenance') + ) + + webRouter.get( + '/track-changes-and-comments-in-latex', + HomeController.externalPage('review-features-page', 'Review features') + ) + + webRouter.get( + '/dropbox', + HomeController.externalPage('dropbox', 'Dropbox and ShareLaTeX') + ) + + webRouter.get('/university', UniversityController.getIndexPage) + return webRouter.get('/university/*', UniversityController.getPage) + }, +} diff --git a/services/web/app/src/Features/StaticPages/UniversityController.js b/services/web/app/src/Features/StaticPages/UniversityController.js new file mode 100644 index 0000000000..3304589d99 --- /dev/null +++ b/services/web/app/src/Features/StaticPages/UniversityController.js @@ -0,0 +1,27 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UniversityController +const settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const Settings = require('@overleaf/settings') + +module.exports = UniversityController = { + getPage(req, res, next) { + const url = + req.url != null ? req.url.toLowerCase().replace('.html', '') : undefined + return res.redirect(`/i${url}`) + }, + + getIndexPage(req, res) { + return res.redirect('/i/university') + }, +} diff --git a/services/web/app/src/Features/Subscription/Errors.js b/services/web/app/src/Features/Subscription/Errors.js new file mode 100644 index 0000000000..3d97f6ebbe --- /dev/null +++ b/services/web/app/src/Features/Subscription/Errors.js @@ -0,0 +1,14 @@ +const Errors = require('../Errors/Errors') + +class RecurlyTransactionError extends Errors.BackwardCompatibleError { + constructor(options) { + super({ + message: 'Unknown transaction error', + ...options, + }) + } +} + +module.exports = { + RecurlyTransactionError, +} diff --git a/services/web/app/src/Features/Subscription/FeaturesUpdater.js b/services/web/app/src/Features/Subscription/FeaturesUpdater.js new file mode 100644 index 0000000000..563ec45488 --- /dev/null +++ b/services/web/app/src/Features/Subscription/FeaturesUpdater.js @@ -0,0 +1,350 @@ +const async = require('async') +const OError = require('@overleaf/o-error') +const PlansLocator = require('./PlansLocator') +const _ = require('lodash') +const SubscriptionLocator = require('./SubscriptionLocator') +const UserFeaturesUpdater = require('./UserFeaturesUpdater') +const Settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const ReferalFeatures = require('../Referal/ReferalFeatures') +const V1SubscriptionManager = require('./V1SubscriptionManager') +const InstitutionsFeatures = require('../Institutions/InstitutionsFeatures') +const UserGetter = require('../User/UserGetter') +const AnalyticsManager = require('../Analytics/AnalyticsManager') + +const FeaturesUpdater = { + refreshFeatures(userId, reason, callback = () => {}) { + UserGetter.getUser(userId, { _id: 1, features: 1 }, (err, user) => { + if (err) { + return callback(err) + } + const oldFeatures = _.clone(user.features) + FeaturesUpdater._computeFeatures(userId, (error, features) => { + if (error) { + return callback(error) + } + logger.log({ userId, features }, 'updating user features') + + const matchedFeatureSet = FeaturesUpdater._getMatchedFeatureSet( + features + ) + AnalyticsManager.setUserProperty( + userId, + 'feature-set', + matchedFeatureSet + ) + + UserFeaturesUpdater.updateFeatures( + userId, + features, + (err, newFeatures, featuresChanged) => { + if (err) { + return callback(err) + } + if (oldFeatures.dropbox === true && features.dropbox === false) { + logger.log({ userId }, '[FeaturesUpdater] must unlink dropbox') + const Modules = require('../../infrastructure/Modules') + Modules.hooks.fire('removeDropbox', userId, reason, err => { + if (err) { + logger.error(err) + } + + return callback(null, newFeatures, featuresChanged) + }) + } else { + return callback(null, newFeatures, featuresChanged) + } + } + ) + }) + }) + }, + + _computeFeatures(userId, callback) { + const jobs = { + individualFeatures(cb) { + FeaturesUpdater._getIndividualFeatures(userId, cb) + }, + groupFeatureSets(cb) { + FeaturesUpdater._getGroupFeatureSets(userId, cb) + }, + institutionFeatures(cb) { + InstitutionsFeatures.getInstitutionsFeatures(userId, cb) + }, + v1Features(cb) { + FeaturesUpdater._getV1Features(userId, cb) + }, + bonusFeatures(cb) { + ReferalFeatures.getBonusFeatures(userId, cb) + }, + featuresOverrides(cb) { + FeaturesUpdater._getFeaturesOverrides(userId, cb) + }, + } + async.series(jobs, function (err, results) { + if (err) { + OError.tag( + err, + 'error getting subscription or group for refreshFeatures', + { + userId, + } + ) + return callback(err) + } + + const { + individualFeatures, + groupFeatureSets, + institutionFeatures, + v1Features, + bonusFeatures, + featuresOverrides, + } = results + logger.log( + { + userId, + individualFeatures, + groupFeatureSets, + institutionFeatures, + v1Features, + bonusFeatures, + featuresOverrides, + }, + 'merging user features' + ) + const featureSets = groupFeatureSets.concat([ + individualFeatures, + institutionFeatures, + v1Features, + bonusFeatures, + featuresOverrides, + ]) + const features = _.reduce( + featureSets, + FeaturesUpdater._mergeFeatures, + Settings.defaultFeatures + ) + callback(null, features) + }) + }, + + _getIndividualFeatures(userId, callback) { + SubscriptionLocator.getUserIndividualSubscription(userId, (err, sub) => + callback(err, FeaturesUpdater._subscriptionToFeatures(sub)) + ) + }, + + _getGroupFeatureSets(userId, callback) { + SubscriptionLocator.getGroupSubscriptionsMemberOf(userId, (err, subs) => + callback(err, (subs || []).map(FeaturesUpdater._subscriptionToFeatures)) + ) + }, + + _getFeaturesOverrides(userId, callback) { + UserGetter.getUser(userId, { featuresOverrides: 1 }, (error, user) => { + if (error) { + return callback(error) + } + if ( + !user || + !user.featuresOverrides || + user.featuresOverrides.length === 0 + ) { + return callback(null, {}) + } + const activeFeaturesOverrides = [] + for (const featuresOverride of user.featuresOverrides) { + if ( + !featuresOverride.expiresAt || + featuresOverride.expiresAt > new Date() + ) { + activeFeaturesOverrides.push(featuresOverride.features) + } + } + const features = _.reduce( + activeFeaturesOverrides, + FeaturesUpdater._mergeFeatures, + {} + ) + callback(null, features) + }) + }, + + _getV1Features(userId, callback) { + V1SubscriptionManager.getPlanCodeFromV1( + userId, + function (err, planCode, v1Id) { + if (err) { + if ((err ? err.name : undefined) === 'NotFoundError') { + return callback(null, []) + } + return callback(err) + } + + callback( + err, + FeaturesUpdater._mergeFeatures( + V1SubscriptionManager.getGrandfatheredFeaturesForV1User(v1Id) || {}, + FeaturesUpdater.planCodeToFeatures(planCode) + ) + ) + } + ) + }, + + _mergeFeatures(featuresA, featuresB) { + const features = Object.assign({}, featuresA) + for (const key in featuresB) { + // Special merging logic for non-boolean features + if (key === 'compileGroup') { + if ( + features.compileGroup === 'priority' || + featuresB.compileGroup === 'priority' + ) { + features.compileGroup = 'priority' + } else { + features.compileGroup = 'standard' + } + } else if (key === 'collaborators') { + if (features.collaborators === -1 || featuresB.collaborators === -1) { + features.collaborators = -1 + } else { + features.collaborators = Math.max( + features.collaborators || 0, + featuresB.collaborators || 0 + ) + } + } else if (key === 'compileTimeout') { + features.compileTimeout = Math.max( + features.compileTimeout || 0, + featuresB.compileTimeout || 0 + ) + } else { + // Boolean keys, true is better + features[key] = features[key] || featuresB[key] + } + } + return features + }, + + isFeatureSetBetter(featuresA, featuresB) { + const mergedFeatures = FeaturesUpdater._mergeFeatures(featuresA, featuresB) + return _.isEqual(featuresA, mergedFeatures) + }, + + _subscriptionToFeatures(subscription) { + return FeaturesUpdater.planCodeToFeatures( + subscription ? subscription.planCode : undefined + ) + }, + + planCodeToFeatures(planCode) { + if (!planCode) { + return {} + } + const plan = PlansLocator.findLocalPlanInSettings(planCode) + if (!plan) { + return {} + } else { + return plan.features + } + }, + + compareFeatures(currentFeatures, expectedFeatures) { + currentFeatures = _.clone(currentFeatures) + expectedFeatures = _.clone(expectedFeatures) + if (_.isEqual(currentFeatures, expectedFeatures)) { + return {} + } + + const mismatchReasons = {} + const featureKeys = [ + ...new Set([ + ...Object.keys(currentFeatures), + ...Object.keys(expectedFeatures), + ]), + ] + featureKeys.sort().forEach(key => { + if (expectedFeatures[key] !== currentFeatures[key]) { + mismatchReasons[key] = expectedFeatures[key] + } + }) + + if (mismatchReasons.compileTimeout) { + // store the compile timeout difference instead of the new compile timeout + mismatchReasons.compileTimeout = + expectedFeatures.compileTimeout - currentFeatures.compileTimeout + } + + if (mismatchReasons.collaborators) { + // store the collaborators difference instead of the new number only + // replace -1 by 100 to make it clearer + if (expectedFeatures.collaborators === -1) { + expectedFeatures.collaborators = 100 + } + if (currentFeatures.collaborators === -1) { + currentFeatures.collaborators = 100 + } + mismatchReasons.collaborators = + expectedFeatures.collaborators - currentFeatures.collaborators + } + + return mismatchReasons + }, + + doSyncFromV1(v1UserId, callback) { + logger.log({ v1UserId }, '[AccountSync] starting account sync') + return UserGetter.getUser( + { 'overleaf.id': v1UserId }, + { _id: 1 }, + function (err, user) { + if (err != null) { + OError.tag(err, '[AccountSync] error getting user', { + v1UserId, + }) + return callback(err) + } + if ((user != null ? user._id : undefined) == null) { + logger.warn({ v1UserId }, '[AccountSync] no user found for v1 id') + return callback(null) + } + logger.log( + { v1UserId, userId: user._id }, + '[AccountSync] updating user subscription and features' + ) + return FeaturesUpdater.refreshFeatures(user._id, 'sync-v1', callback) + } + ) + }, + + _getMatchedFeatureSet(features) { + for (const [name, featureSet] of Object.entries(Settings.features)) { + if (_.isEqual(features, featureSet)) { + return name + } + } + return 'mixed' + }, +} + +const refreshFeaturesPromise = (userId, reason) => + new Promise(function (resolve, reject) { + FeaturesUpdater.refreshFeatures( + userId, + reason, + (error, features, featuresChanged) => { + if (error) { + reject(error) + } else { + resolve({ features, featuresChanged }) + } + } + ) + }) + +FeaturesUpdater.promises = { + refreshFeatures: refreshFeaturesPromise, +} + +module.exports = FeaturesUpdater diff --git a/services/web/app/src/Features/Subscription/GroupPlansData.js b/services/web/app/src/Features/Subscription/GroupPlansData.js new file mode 100644 index 0000000000..a6fd24301e --- /dev/null +++ b/services/web/app/src/Features/Subscription/GroupPlansData.js @@ -0,0 +1,54 @@ +const Settings = require('@overleaf/settings') +const fs = require('fs') +const Path = require('path') + +// The groups.json file encodes the various group plan options we provide, and +// is used in the app the render the appropriate dialog in the plans page, and +// to generate the appropriate entries in the Settings.plans array. +// It is also used by scripts/recurly/sync_recurly.rb, which will make sure +// Recurly has a plan configured for all the groups, and that the prices are +// up to date with the data in groups.json. +const data = fs.readFileSync( + Path.join(__dirname, '/../../../templates/plans/groups.json') +) +const groups = JSON.parse(data.toString()) + +const capitalize = string => string.charAt(0).toUpperCase() + string.slice(1) + +// With group accounts in Recurly, we end up with a lot of plans to manage. +// Rather than hand coding them in the settings file, and then needing to keep +// that data in sync with the data in groups.json, we can auto generate the +// group plan entries and append them to Settings.plans at boot time. This is not +// a particularly clean pattern, since it's a little surprising that settings +// are modified at boot-time, but I think it's a better option than trying to +// keep two sources of data in sync. +for (const [usage, planData] of Object.entries(groups)) { + for (const [planCode, currencyData] of Object.entries(planData)) { + // Gather all possible sizes that are set up in at least one currency + const sizes = new Set() + for (const priceData of Object.values(currencyData)) { + for (const size in priceData) { + sizes.add(size) + } + } + + // Generate plans in settings + for (const size of sizes) { + Settings.plans.push({ + planCode: `group_${planCode}_${size}_${usage}`, + name: `${Settings.appName} ${capitalize( + planCode + )} - Group Account (${size} licenses) - ${capitalize(usage)}`, + hideFromUsers: true, + price: groups[usage][planCode].USD[size], + annual: true, + features: Settings.features[planCode], + groupPlan: true, + membersLimit: parseInt(size), + membersLimitAddOn: 'additional-license', + }) + } + } +} + +module.exports = groups diff --git a/services/web/app/src/Features/Subscription/LimitationsManager.js b/services/web/app/src/Features/Subscription/LimitationsManager.js new file mode 100644 index 0000000000..6d91798bc3 --- /dev/null +++ b/services/web/app/src/Features/Subscription/LimitationsManager.js @@ -0,0 +1,237 @@ +const OError = require('@overleaf/o-error') +const logger = require('logger-sharelatex') +const ProjectGetter = require('../Project/ProjectGetter') +const UserGetter = require('../User/UserGetter') +const SubscriptionLocator = require('./SubscriptionLocator') +const Settings = require('@overleaf/settings') +const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') +const CollaboratorsInvitesHandler = require('../Collaborators/CollaboratorsInviteHandler') +const V1SubscriptionManager = require('./V1SubscriptionManager') +const { V1ConnectionError } = require('../Errors/Errors') +const { promisifyAll } = require('../../util/promises') + +const LimitationsManager = { + allowedNumberOfCollaboratorsInProject(projectId, callback) { + ProjectGetter.getProject( + projectId, + { owner_ref: true }, + (error, project) => { + if (error) { + return callback(error) + } + this.allowedNumberOfCollaboratorsForUser(project.owner_ref, callback) + } + ) + }, + + allowedNumberOfCollaboratorsForUser(userId, callback) { + UserGetter.getUser(userId, { features: 1 }, function (error, user) { + if (error) { + return callback(error) + } + if (user.features && user.features.collaborators) { + callback(null, user.features.collaborators) + } else { + callback(null, Settings.defaultFeatures.collaborators) + } + }) + }, + + canAddXCollaborators(projectId, numberOfNewCollaborators, callback) { + if (!callback) { + callback = function (error, allowed) {} + } + this.allowedNumberOfCollaboratorsInProject( + projectId, + (error, allowedNumber) => { + if (error) { + return callback(error) + } + CollaboratorsGetter.getInvitedCollaboratorCount( + projectId, + (error, currentNumber) => { + if (error) { + return callback(error) + } + CollaboratorsInvitesHandler.getInviteCount( + projectId, + (error, inviteCount) => { + if (error) { + return callback(error) + } + if ( + currentNumber + inviteCount + numberOfNewCollaborators <= + allowedNumber || + allowedNumber < 0 + ) { + callback(null, true) + } else { + callback(null, false) + } + } + ) + } + ) + } + ) + }, + + hasPaidSubscription(user, callback) { + if (!callback) { + callback = function (err, hasSubscriptionOrIsMember) {} + } + this.userHasV2Subscription(user, (err, hasSubscription, subscription) => { + if (err) { + return callback(err) + } + this.userIsMemberOfGroupSubscription(user, (err, isMember) => { + if (err) { + return callback(err) + } + this.userHasV1Subscription(user, (err, hasV1Subscription) => { + if (err) { + return callback( + new V1ConnectionError( + 'error getting subscription from v1' + ).withCause(err) + ) + } + callback( + err, + isMember || hasSubscription || hasV1Subscription, + subscription + ) + }) + }) + }) + }, + + // alias for backward-compatibility with modules. Use `haspaidsubscription` instead + userHasSubscriptionOrIsGroupMember(user, callback) { + this.hasPaidSubscription(user, callback) + }, + + userHasV2Subscription(user, callback) { + if (!callback) { + callback = function (err, hasSubscription, subscription) {} + } + SubscriptionLocator.getUsersSubscription( + user._id, + function (err, subscription) { + if (err) { + return callback(err) + } + let hasValidSubscription = false + if (subscription) { + if ( + subscription.recurlySubscription_id || + subscription.customAccount + ) { + hasValidSubscription = true + } + } + callback(err, hasValidSubscription, subscription) + } + ) + }, + + userHasV1OrV2Subscription(user, callback) { + if (!callback) { + callback = function (err, hasSubscription) {} + } + this.userHasV2Subscription(user, (err, hasV2Subscription) => { + if (err) { + return callback(err) + } + if (hasV2Subscription) { + return callback(null, true) + } + this.userHasV1Subscription(user, (err, hasV1Subscription) => { + if (err) { + return callback(err) + } + if (hasV1Subscription) { + return callback(null, true) + } + callback(null, false) + }) + }) + }, + + userIsMemberOfGroupSubscription(user, callback) { + if (!callback) { + callback = function (error, isMember, subscriptions) {} + } + SubscriptionLocator.getMemberSubscriptions( + user._id, + function (err, subscriptions) { + if (!subscriptions) { + subscriptions = [] + } + if (err) { + return callback(err) + } + callback(err, subscriptions.length > 0, subscriptions) + } + ) + }, + + userHasV1Subscription(user, callback) { + if (!callback) { + callback = function (error, hasV1Subscription) {} + } + V1SubscriptionManager.getSubscriptionsFromV1( + user._id, + function (err, v1Subscription) { + callback( + err, + !!(v1Subscription ? v1Subscription.has_subscription : undefined) + ) + } + ) + }, + + teamHasReachedMemberLimit(subscription) { + const currentTotal = + (subscription.member_ids || []).length + + (subscription.teamInvites || []).length + + (subscription.invited_emails || []).length + + return currentTotal >= subscription.membersLimit + }, + + hasGroupMembersLimitReached(subscriptionId, callback) { + if (!callback) { + callback = function (err, limitReached, subscription) {} + } + SubscriptionLocator.getSubscription( + subscriptionId, + function (err, subscription) { + if (err) { + OError.tag(err, 'error getting subscription', { + subscriptionId, + }) + return callback(err) + } + if (!subscription) { + logger.warn({ subscriptionId }, 'no subscription found') + return callback(new Error('no subscription found')) + } + + const limitReached = LimitationsManager.teamHasReachedMemberLimit( + subscription + ) + callback(err, limitReached, subscription) + } + ) + }, +} + +LimitationsManager.promises = promisifyAll(LimitationsManager, { + multiResult: { + userHasV2Subscription: ['hasSubscription', 'subscription'], + userIsMemberOfGroupSubscription: ['isMember', 'subscriptions'], + hasGroupMembersLimitReached: ['limitReached', 'subscription'], + }, +}) +module.exports = LimitationsManager diff --git a/services/web/app/src/Features/Subscription/PlansLocator.js b/services/web/app/src/Features/Subscription/PlansLocator.js new file mode 100644 index 0000000000..27972b0eef --- /dev/null +++ b/services/web/app/src/Features/Subscription/PlansLocator.js @@ -0,0 +1,25 @@ +const Settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') + +function ensurePlansAreSetupCorrectly() { + Settings.plans.forEach(plan => { + if (typeof plan.price !== 'number') { + logger.fatal({ plan }, 'missing price on plan') + process.exit(1) + } + }) +} + +function findLocalPlanInSettings(planCode) { + for (const plan of Settings.plans) { + if (plan.planCode === planCode) { + return plan + } + } + return null +} + +module.exports = { + ensurePlansAreSetupCorrectly, + findLocalPlanInSettings, +} diff --git a/services/web/app/src/Features/Subscription/RecurlyClient.js b/services/web/app/src/Features/Subscription/RecurlyClient.js new file mode 100644 index 0000000000..6387536025 --- /dev/null +++ b/services/web/app/src/Features/Subscription/RecurlyClient.js @@ -0,0 +1,117 @@ +const recurly = require('recurly') +const Settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const { callbackify } = require('util') +const UserGetter = require('../User/UserGetter') + +const recurlySettings = Settings.apis.recurly +const recurlyApiKey = recurlySettings ? recurlySettings.apiKey : undefined + +const client = new recurly.Client(recurlyApiKey) + +async function getAccountForUserId(userId) { + try { + return await client.getAccount(`code-${userId}`) + } catch (err) { + if (err instanceof recurly.errors.NotFoundError) { + // An expected error, we don't need to handle it, just return nothing + logger.debug({ userId }, 'no recurly account found for user') + } else { + throw err + } + } +} + +async function createAccountForUserId(userId) { + const user = await UserGetter.promises.getUser(userId, { + _id: 1, + first_name: 1, + last_name: 1, + email: 1, + }) + const accountCreate = { + code: user._id.toString(), + email: user.email, + firstName: user.first_name, + lastName: user.last_name, + } + const account = await client.createAccount(accountCreate) + logger.log({ userId, account }, 'created recurly account') + return account +} + +async function getSubscription(subscriptionId) { + return await client.getSubscription(subscriptionId) +} + +async function changeSubscription(subscriptionId, body) { + const change = await client.createSubscriptionChange(subscriptionId, body) + logger.log( + { subscriptionId, changeId: change.id }, + 'created subscription change' + ) + return change +} + +async function changeSubscriptionByUuid(subscriptionUuid, ...args) { + return await changeSubscription('uuid-' + subscriptionUuid, ...args) +} + +async function removeSubscriptionChange(subscriptionId) { + const removed = await client.removeSubscriptionChange(subscriptionId) + logger.log({ subscriptionId }, 'removed pending subscription change') + return removed +} + +async function removeSubscriptionChangeByUuid(subscriptionUuid) { + return await removeSubscriptionChange('uuid-' + subscriptionUuid) +} + +async function reactivateSubscriptionByUuid(subscriptionUuid) { + return await client.reactivateSubscription('uuid-' + subscriptionUuid) +} + +async function cancelSubscriptionByUuid(subscriptionUuid) { + try { + return await client.cancelSubscription('uuid-' + subscriptionUuid) + } catch (err) { + if (err instanceof recurly.errors.ValidationError) { + if ( + err.message === 'Only active and future subscriptions can be canceled.' + ) { + logger.log( + { subscriptionUuid }, + 'subscription cancellation failed, subscription not active' + ) + } + } else { + throw err + } + } +} + +module.exports = { + errors: recurly.errors, + + getAccountForUserId: callbackify(getAccountForUserId), + createAccountForUserId: callbackify(createAccountForUserId), + getSubscription: callbackify(getSubscription), + changeSubscription: callbackify(changeSubscription), + changeSubscriptionByUuid: callbackify(changeSubscriptionByUuid), + removeSubscriptionChange: callbackify(removeSubscriptionChange), + removeSubscriptionChangeByUuid: callbackify(removeSubscriptionChangeByUuid), + reactivateSubscriptionByUuid: callbackify(reactivateSubscriptionByUuid), + cancelSubscriptionByUuid: callbackify(cancelSubscriptionByUuid), + + promises: { + getSubscription, + getAccountForUserId, + createAccountForUserId, + changeSubscription, + changeSubscriptionByUuid, + removeSubscriptionChange, + removeSubscriptionChangeByUuid, + reactivateSubscriptionByUuid, + cancelSubscriptionByUuid, + }, +} diff --git a/services/web/app/src/Features/Subscription/RecurlyEventHandler.js b/services/web/app/src/Features/Subscription/RecurlyEventHandler.js new file mode 100644 index 0000000000..f21815b051 --- /dev/null +++ b/services/web/app/src/Features/Subscription/RecurlyEventHandler.js @@ -0,0 +1,151 @@ +const AnalyticsManager = require('../Analytics/AnalyticsManager') + +function sendRecurlyAnalyticsEvent(event, eventData) { + switch (event) { + case 'new_subscription_notification': + _sendSubscriptionStartedEvent(eventData) + break + case 'updated_subscription_notification': + _sendSubscriptionUpdatedEvent(eventData) + break + case 'canceled_subscription_notification': + _sendSubscriptionCancelledEvent(eventData) + break + case 'expired_subscription_notification': + _sendSubscriptionExpiredEvent(eventData) + break + case 'renewed_subscription_notification': + _sendSubscriptionRenewedEvent(eventData) + break + case 'reactivated_account_notification': + _sendSubscriptionReactivatedEvent(eventData) + break + case 'paid_charge_invoice_notification': + if ( + eventData.invoice.state === 'paid' && + eventData.invoice.total_in_cents > 0 + ) { + _sendInvoicePaidEvent(eventData) + } + break + case 'closed_invoice_notification': + if ( + eventData.invoice.state === 'collected' && + eventData.invoice.total_in_cents > 0 + ) { + _sendInvoicePaidEvent(eventData) + } + break + } +} + +function _sendSubscriptionStartedEvent(eventData) { + const userId = _getUserId(eventData) + const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData) + AnalyticsManager.recordEvent(userId, 'subscription-started', { + plan_code: planCode, + quantity, + is_trial: isTrial, + }) + AnalyticsManager.setUserProperty(userId, 'subscription-plan-code', planCode) + AnalyticsManager.setUserProperty(userId, 'subscription-state', state) + AnalyticsManager.setUserProperty(userId, 'subscription-is-trial', isTrial) +} + +function _sendSubscriptionUpdatedEvent(eventData) { + const userId = _getUserId(eventData) + const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData) + AnalyticsManager.recordEvent(userId, 'subscription-updated', { + plan_code: planCode, + quantity, + }) + AnalyticsManager.setUserProperty(userId, 'subscription-plan-code', planCode) + AnalyticsManager.setUserProperty(userId, 'subscription-state', state) + AnalyticsManager.setUserProperty(userId, 'subscription-is-trial', isTrial) +} + +function _sendSubscriptionCancelledEvent(eventData) { + const userId = _getUserId(eventData) + const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData) + AnalyticsManager.recordEvent(userId, 'subscription-cancelled', { + plan_code: planCode, + quantity, + is_trial: isTrial, + }) + AnalyticsManager.setUserProperty(userId, 'subscription-state', state) + AnalyticsManager.setUserProperty(userId, 'subscription-is-trial', isTrial) +} + +function _sendSubscriptionExpiredEvent(eventData) { + const userId = _getUserId(eventData) + const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData) + AnalyticsManager.recordEvent(userId, 'subscription-expired', { + plan_code: planCode, + quantity, + is_trial: isTrial, + }) + AnalyticsManager.setUserProperty(userId, 'subscription-plan-code', planCode) + AnalyticsManager.setUserProperty(userId, 'subscription-state', state) + AnalyticsManager.setUserProperty(userId, 'subscription-is-trial', isTrial) +} + +function _sendSubscriptionRenewedEvent(eventData) { + const userId = _getUserId(eventData) + const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData) + AnalyticsManager.recordEvent(userId, 'subscription-renewed', { + plan_code: planCode, + quantity, + is_trial: isTrial, + }) + AnalyticsManager.setUserProperty(userId, 'subscription-plan-code', planCode) + AnalyticsManager.setUserProperty(userId, 'subscription-state', state) + AnalyticsManager.setUserProperty(userId, 'subscription-is-trial', isTrial) +} + +function _sendSubscriptionReactivatedEvent(eventData) { + const userId = _getUserId(eventData) + const { planCode, quantity, state, isTrial } = _getSubscriptionData(eventData) + AnalyticsManager.recordEvent(userId, 'subscription-reactivated', { + plan_code: planCode, + quantity, + }) + AnalyticsManager.setUserProperty(userId, 'subscription-plan-code', planCode) + AnalyticsManager.setUserProperty(userId, 'subscription-state', state) + AnalyticsManager.setUserProperty(userId, 'subscription-is-trial', isTrial) +} + +function _sendInvoicePaidEvent(eventData) { + const userId = _getUserId(eventData) + AnalyticsManager.recordEvent(userId, 'subscription-invoice-collected') + AnalyticsManager.setUserProperty(userId, 'subscription-is-trial', false) +} + +function _getUserId(eventData) { + let userId + if (eventData && eventData.account && eventData.account.account_code) { + userId = eventData.account.account_code + } else { + throw new Error( + 'account.account_code missing in event data to identity user ID' + ) + } + return userId +} + +function _getSubscriptionData(eventData) { + const isTrial = + eventData.subscription.trial_started_at && + eventData.subscription.current_period_started_at && + eventData.subscription.trial_started_at.getTime() === + eventData.subscription.current_period_started_at.getTime() + return { + planCode: eventData.subscription.plan.plan_code, + quantity: eventData.subscription.quantity, + state: eventData.subscription.state, + isTrial, + } +} + +module.exports = { + sendRecurlyAnalyticsEvent, +} diff --git a/services/web/app/src/Features/Subscription/RecurlyWrapper.js b/services/web/app/src/Features/Subscription/RecurlyWrapper.js new file mode 100644 index 0000000000..3a7fd2a936 --- /dev/null +++ b/services/web/app/src/Features/Subscription/RecurlyWrapper.js @@ -0,0 +1,1091 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, + node/no-deprecated-api, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const OError = require('@overleaf/o-error') +const querystring = require('querystring') +const crypto = require('crypto') +const request = require('request') +const Settings = require('@overleaf/settings') +const xml2js = require('xml2js') +const logger = require('logger-sharelatex') +const Async = require('async') +const Errors = require('../Errors/Errors') +const SubscriptionErrors = require('./Errors') +const { promisify } = require('util') + +function updateAccountEmailAddress(accountId, newEmail, callback) { + const data = { + email: newEmail, + } + const requestBody = RecurlyWrapper._buildXml('account', data) + + RecurlyWrapper.apiRequest( + { + url: `accounts/${accountId}`, + method: 'PUT', + body: requestBody, + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + RecurlyWrapper._parseAccountXml(body, callback) + } + ) +} + +const RecurlyWrapper = { + apiUrl: Settings.apis.recurly.url || 'https://api.recurly.com/v2', + + _paypal: { + checkAccountExists(cache, next) { + const { user } = cache + const { subscriptionDetails } = cache + logger.log( + { user_id: user._id }, + 'checking if recurly account exists for user' + ) + return RecurlyWrapper.apiRequest( + { + url: `accounts/${user._id}`, + method: 'GET', + expect404: true, + }, + function (error, response, responseBody) { + if (error) { + OError.tag( + error, + 'error response from recurly while checking account', + { + user_id: user._id, + } + ) + return next(error) + } + if (response.statusCode === 404) { + // actually not an error in this case, just no existing account + logger.log( + { user_id: user._id }, + 'user does not currently exist in recurly, proceed' + ) + cache.userExists = false + return next(null, cache) + } + logger.log({ user_id: user._id }, 'user appears to exist in recurly') + return RecurlyWrapper._parseAccountXml( + responseBody, + function (err, account) { + if (err) { + OError.tag(err, 'error parsing account', { + user_id: user._id, + }) + return next(err) + } + cache.userExists = true + cache.account = account + return next(null, cache) + } + ) + } + ) + }, + createAccount(cache, next) { + const { user } = cache + const { subscriptionDetails } = cache + if (cache.userExists) { + return next(null, cache) + } + + let address + try { + address = getAddressFromSubscriptionDetails(subscriptionDetails, false) + } catch (error) { + return next(error) + } + const data = { + account_code: user._id, + email: user.email, + first_name: user.first_name, + last_name: user.last_name, + address, + } + const requestBody = RecurlyWrapper._buildXml('account', data) + + return RecurlyWrapper.apiRequest( + { + url: 'accounts', + method: 'POST', + body: requestBody, + }, + (error, response, responseBody) => { + if (error) { + OError.tag( + error, + 'error response from recurly while creating account', + { + user_id: user._id, + } + ) + return next(error) + } + return RecurlyWrapper._parseAccountXml( + responseBody, + function (err, account) { + if (err) { + OError.tag(err, 'error creating account', { + user_id: user._id, + }) + return next(err) + } + cache.account = account + return next(null, cache) + } + ) + } + ) + }, + createBillingInfo(cache, next) { + const { user } = cache + const { recurlyTokenIds } = cache + const { subscriptionDetails } = cache + logger.log({ user_id: user._id }, 'creating billing info in recurly') + const accountCode = __guard__( + cache != null ? cache.account : undefined, + x1 => x1.account_code + ) + if (!accountCode) { + return next(new Error('no account code at createBillingInfo stage')) + } + const data = { token_id: recurlyTokenIds.billing } + const requestBody = RecurlyWrapper._buildXml('billing_info', data) + return RecurlyWrapper.apiRequest( + { + url: `accounts/${accountCode}/billing_info`, + method: 'POST', + body: requestBody, + }, + (error, response, responseBody) => { + if (error) { + OError.tag( + error, + 'error response from recurly while creating billing info', + { + user_id: user._id, + } + ) + return next(error) + } + return RecurlyWrapper._parseBillingInfoXml( + responseBody, + function (err, billingInfo) { + if (err) { + OError.tag(err, 'error creating billing info', { + user_id: user._id, + accountCode, + }) + return next(err) + } + cache.billingInfo = billingInfo + return next(null, cache) + } + ) + } + ) + }, + + setAddressAndCompanyBillingInfo(cache, next) { + const { user } = cache + const { subscriptionDetails } = cache + logger.log( + { user_id: user._id }, + 'setting billing address and company info in recurly' + ) + const accountCode = __guard__( + cache != null ? cache.account : undefined, + x1 => x1.account_code + ) + if (!accountCode) { + return next( + new Error('no account code at setAddressAndCompanyBillingInfo stage') + ) + } + + let addressAndCompanyBillingInfo + try { + addressAndCompanyBillingInfo = getAddressFromSubscriptionDetails( + subscriptionDetails, + true + ) + } catch (error) { + return next(error) + } + const requestBody = RecurlyWrapper._buildXml( + 'billing_info', + addressAndCompanyBillingInfo + ) + + return RecurlyWrapper.apiRequest( + { + url: `accounts/${accountCode}/billing_info`, + method: 'PUT', + body: requestBody, + }, + (error, response, responseBody) => { + if (error) { + OError.tag( + error, + 'error response from recurly while setting address', + { + user_id: user._id, + } + ) + return next(error) + } + return RecurlyWrapper._parseBillingInfoXml( + responseBody, + function (err, billingInfo) { + if (err) { + OError.tag(err, 'error updating billing info', { + user_id: user._id, + }) + return next(err) + } + cache.billingInfo = billingInfo + return next(null, cache) + } + ) + } + ) + }, + createSubscription(cache, next) { + const { user } = cache + const { subscriptionDetails } = cache + logger.log({ user_id: user._id }, 'creating subscription in recurly') + const data = { + plan_code: subscriptionDetails.plan_code, + currency: subscriptionDetails.currencyCode, + coupon_code: subscriptionDetails.coupon_code, + account: { + account_code: user._id, + }, + } + const customFields = getCustomFieldsFromSubscriptionDetails( + subscriptionDetails + ) + if (customFields) { + data.custom_fields = customFields + } + const requestBody = RecurlyWrapper._buildXml('subscription', data) + + return RecurlyWrapper.apiRequest( + { + url: 'subscriptions', + method: 'POST', + body: requestBody, + }, + (error, response, responseBody) => { + if (error) { + OError.tag( + error, + 'error response from recurly while creating subscription', + { + user_id: user._id, + } + ) + return next(error) + } + return RecurlyWrapper._parseSubscriptionXml( + responseBody, + function (err, subscription) { + if (err) { + OError.tag(err, 'error creating subscription', { + user_id: user._id, + }) + return next(err) + } + cache.subscription = subscription + return next(null, cache) + } + ) + } + ) + }, + }, + + _createPaypalSubscription( + user, + subscriptionDetails, + recurlyTokenIds, + callback + ) { + logger.log( + { user_id: user._id }, + 'starting process of creating paypal subscription' + ) + // We use `async.waterfall` to run each of these actions in sequence + // passing a `cache` object along the way. The cache is initialized + // with required data, and `async.apply` to pass the cache to the first function + const cache = { user, recurlyTokenIds, subscriptionDetails } + return Async.waterfall( + [ + Async.apply(RecurlyWrapper._paypal.checkAccountExists, cache), + RecurlyWrapper._paypal.createAccount, + RecurlyWrapper._paypal.createBillingInfo, + RecurlyWrapper._paypal.setAddressAndCompanyBillingInfo, + RecurlyWrapper._paypal.createSubscription, + ], + function (err, result) { + if (err) { + OError.tag(err, 'error in paypal subscription creation process', { + user_id: user._id, + }) + return callback(err) + } + if (!result.subscription) { + err = new Error('no subscription object in result') + OError.tag(err, 'error in paypal subscription creation process', { + user_id: user._id, + }) + return callback(err) + } + logger.log( + { user_id: user._id }, + 'done creating paypal subscription for user' + ) + return callback(null, result.subscription) + } + ) + }, + + _createCreditCardSubscription( + user, + subscriptionDetails, + recurlyTokenIds, + callback + ) { + const data = { + plan_code: subscriptionDetails.plan_code, + currency: subscriptionDetails.currencyCode, + coupon_code: subscriptionDetails.coupon_code, + account: { + account_code: user._id, + email: user.email, + first_name: subscriptionDetails.first_name || user.first_name, + last_name: subscriptionDetails.last_name || user.last_name, + billing_info: { + token_id: recurlyTokenIds.billing, + }, + }, + } + if (recurlyTokenIds.threeDSecureActionResult) { + data.account.billing_info.three_d_secure_action_result_token_id = + recurlyTokenIds.threeDSecureActionResult + } + const customFields = getCustomFieldsFromSubscriptionDetails( + subscriptionDetails + ) + if (customFields) { + data.custom_fields = customFields + } + const requestBody = RecurlyWrapper._buildXml('subscription', data) + + return RecurlyWrapper.apiRequest( + { + url: 'subscriptions', + method: 'POST', + body: requestBody, + expect422: true, + }, + (error, response, responseBody) => { + if (error != null) { + return callback(error) + } + + if (response.statusCode === 422) { + RecurlyWrapper._handle422Response(responseBody, callback) + } else { + RecurlyWrapper._parseSubscriptionXml(responseBody, callback) + } + } + ) + }, + + createSubscription(user, subscriptionDetails, recurlyTokenIds, callback) { + const { isPaypal } = subscriptionDetails + logger.log( + { user_id: user._id, isPaypal }, + 'setting up subscription in recurly' + ) + const fn = isPaypal + ? RecurlyWrapper._createPaypalSubscription + : RecurlyWrapper._createCreditCardSubscription + return fn(user, subscriptionDetails, recurlyTokenIds, callback) + }, + + apiRequest(options, callback) { + options.url = RecurlyWrapper.apiUrl + '/' + options.url + options.headers = { + Authorization: `Basic ${new Buffer(Settings.apis.recurly.apiKey).toString( + 'base64' + )}`, + Accept: 'application/xml', + 'Content-Type': 'application/xml; charset=utf-8', + 'X-Api-Version': Settings.apis.recurly.apiVersion, + } + const { expect404, expect422 } = options + delete options.expect404 + delete options.expect422 + return request(options, function (error, response, body) { + if ( + error == null && + response.statusCode !== 200 && + response.statusCode !== 201 && + response.statusCode !== 204 && + (response.statusCode !== 404 || !expect404) && + (response.statusCode !== 422 || !expect422) + ) { + logger.warn( + { + err: error, + body, + options, + statusCode: response != null ? response.statusCode : undefined, + }, + 'error returned from recurly' + ) + // TODO: this should be an Error object not a string + error = `Recurly API returned with status code: ${response.statusCode}` + } + return callback(error, response, body) + }) + }, + + getSubscriptions(accountId, callback) { + return RecurlyWrapper.apiRequest( + { + url: `accounts/${accountId}/subscriptions`, + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return RecurlyWrapper._parseXml(body, callback) + } + ) + }, + + getSubscription(subscriptionId, options, callback) { + let url + if (callback == null) { + callback = options + } + if (!options) { + options = {} + } + + if (options.recurlyJsResult) { + url = `recurly_js/result/${subscriptionId}` + } else { + url = `subscriptions/${subscriptionId}` + } + + return RecurlyWrapper.apiRequest( + { + url, + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return RecurlyWrapper._parseSubscriptionXml( + body, + (error, recurlySubscription) => { + if (error != null) { + return callback(error) + } + if (options.includeAccount) { + let accountId + if ( + recurlySubscription.account != null && + recurlySubscription.account.url != null + ) { + accountId = recurlySubscription.account.url.match( + /accounts\/(.*)/ + )[1] + } else { + return callback( + new Error("I don't understand the response from Recurly") + ) + } + + return RecurlyWrapper.getAccount( + accountId, + function (error, account) { + if (error != null) { + return callback(error) + } + recurlySubscription.account = account + return callback(null, recurlySubscription) + } + ) + } else { + return callback(null, recurlySubscription) + } + } + ) + } + ) + }, + + getPaginatedEndpoint(resource, queryParams, callback) { + queryParams.per_page = queryParams.per_page || 200 + let allItems = [] + var getPage = (cursor = null) => { + const opts = { + url: resource, + qs: queryParams, + } + if (cursor != null) { + opts.qs.cursor = cursor + } + return RecurlyWrapper.apiRequest(opts, (error, response, body) => { + if (error != null) { + return callback(error) + } + return RecurlyWrapper._parseXml(body, function (err, data) { + if (err != null) { + logger.warn({ err }, 'could not get accoutns') + callback(err) + } + const items = data[resource] + allItems = allItems.concat(items) + logger.log( + `got another ${items.length}, total now ${allItems.length}` + ) + cursor = __guard__( + response.headers.link != null + ? response.headers.link.match(/cursor=([0-9.]+%3A[0-9.]+)&/) + : undefined, + x1 => x1[1] + ) + if (cursor != null) { + cursor = decodeURIComponent(cursor) + return getPage(cursor) + } else { + return callback(err, allItems) + } + }) + }) + } + + return getPage() + }, + + getAccount(accountId, callback) { + return RecurlyWrapper.apiRequest( + { + url: `accounts/${accountId}`, + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return RecurlyWrapper._parseAccountXml(body, callback) + } + ) + }, + + updateAccountEmailAddress, + + getAccountActiveCoupons(accountId, callback) { + return RecurlyWrapper.apiRequest( + { + url: `accounts/${accountId}/redemptions`, + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return RecurlyWrapper._parseRedemptionsXml( + body, + function (error, redemptions) { + if (error != null) { + return callback(error) + } + const activeRedemptions = redemptions.filter( + redemption => redemption.state === 'active' + ) + const couponCodes = activeRedemptions.map( + redemption => redemption.coupon_code + ) + return Async.map( + couponCodes, + RecurlyWrapper.getCoupon, + function (error, coupons) { + if (error != null) { + return callback(error) + } + return callback(null, coupons) + } + ) + } + ) + } + ) + }, + + getCoupon(couponCode, callback) { + const opts = { url: `coupons/${couponCode}` } + return RecurlyWrapper.apiRequest(opts, (error, response, body) => + RecurlyWrapper._parseCouponXml(body, callback) + ) + }, + + getBillingInfo(accountId, callback) { + return RecurlyWrapper.apiRequest( + { + url: `accounts/${accountId}/billing_info`, + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return RecurlyWrapper._parseXml(body, callback) + } + ) + }, + + getAccountPastDueInvoices(accountId, callback) { + RecurlyWrapper.apiRequest( + { + url: `accounts/${accountId}/invoices?state=past_due`, + }, + (error, response, body) => { + if (error) { + return callback(error) + } + RecurlyWrapper._parseInvoicesXml(body, callback) + } + ) + }, + + attemptInvoiceCollection(invoiceId, callback) { + RecurlyWrapper.apiRequest( + { + url: `invoices/${invoiceId}/collect`, + method: 'put', + }, + callback + ) + }, + + updateSubscription(subscriptionId, options, callback) { + logger.log( + { subscriptionId, options }, + 'telling recurly to update subscription' + ) + const data = { + plan_code: options.plan_code, + timeframe: options.timeframe, + } + const requestBody = RecurlyWrapper._buildXml('subscription', data) + + return RecurlyWrapper.apiRequest( + { + url: `subscriptions/${subscriptionId}`, + method: 'put', + body: requestBody, + }, + (error, response, responseBody) => { + if (error != null) { + return callback(error) + } + return RecurlyWrapper._parseSubscriptionXml(responseBody, callback) + } + ) + }, + + createFixedAmmountCoupon( + coupon_code, + name, + currencyCode, + discount_in_cents, + plan_code, + callback + ) { + const data = { + coupon_code, + name, + discount_type: 'dollars', + discount_in_cents: {}, + plan_codes: { + plan_code, + }, + applies_to_all_plans: false, + } + data.discount_in_cents[currencyCode] = discount_in_cents + const requestBody = RecurlyWrapper._buildXml('coupon', data) + + logger.log({ coupon_code, requestBody }, 'creating coupon') + return RecurlyWrapper.apiRequest( + { + url: 'coupons', + method: 'post', + body: requestBody, + }, + (error, response, responseBody) => { + if (error != null) { + logger.warn({ err: error, coupon_code }, 'error creating coupon') + } + return callback(error) + } + ) + }, + + lookupCoupon(coupon_code, callback) { + return RecurlyWrapper.apiRequest( + { + url: `coupons/${coupon_code}`, + }, + (error, response, body) => { + if (error != null) { + return callback(error) + } + return RecurlyWrapper._parseXml(body, callback) + } + ) + }, + + redeemCoupon(account_code, coupon_code, callback) { + const data = { + account_code, + currency: 'USD', + } + const requestBody = RecurlyWrapper._buildXml('redemption', data) + + logger.log( + { account_code, coupon_code, requestBody }, + 'redeeming coupon for user' + ) + return RecurlyWrapper.apiRequest( + { + url: `coupons/${coupon_code}/redeem`, + method: 'post', + body: requestBody, + }, + (error, response, responseBody) => { + if (error != null) { + logger.warn( + { err: error, account_code, coupon_code }, + 'error redeeming coupon' + ) + } + return callback(error) + } + ) + }, + + extendTrial(subscriptionId, daysUntilExpire, callback) { + if (daysUntilExpire == null) { + daysUntilExpire = 7 + } + const next_renewal_date = new Date() + next_renewal_date.setDate(next_renewal_date.getDate() + daysUntilExpire) + logger.log( + { subscriptionId, daysUntilExpire }, + 'Exending Free trial for user' + ) + return RecurlyWrapper.apiRequest( + { + url: `/subscriptions/${subscriptionId}/postpone?next_renewal_date=${next_renewal_date}&bulk=false`, + method: 'put', + }, + (error, response, responseBody) => { + if (error != null) { + logger.warn( + { err: error, subscriptionId, daysUntilExpire }, + 'error exending trial' + ) + } + return callback(error) + } + ) + }, + + listAccountActiveSubscriptions(account_id, callback) { + if (callback == null) { + callback = function (error, subscriptions) {} + } + return RecurlyWrapper.apiRequest( + { + url: `accounts/${account_id}/subscriptions`, + qs: { + state: 'active', + }, + expect404: true, + }, + function (error, response, body) { + if (error != null) { + return callback(error) + } + if (response.statusCode === 404) { + return callback(null, []) + } else { + return RecurlyWrapper._parseSubscriptionsXml(body, callback) + } + } + ) + }, + + _handle422Response(body, callback) { + RecurlyWrapper._parseErrorsXml(body, (error, data) => { + if (error) { + return callback(error) + } + + let errorData = {} + if (data.transaction_error) { + errorData = { + message: data.transaction_error.merchant_message, + info: { + category: data.transaction_error.error_category, + gatewayCode: data.transaction_error.gateway_error_code, + public: { + code: data.transaction_error.error_code, + message: data.transaction_error.customer_message, + }, + }, + } + if (data.transaction_error.three_d_secure_action_token_id) { + errorData.info.public.threeDSecureActionTokenId = + data.transaction_error.three_d_secure_action_token_id + } + } else if (data.error && data.error._) { + // fallback for errors that don't have a `transaction_error` field, but + // instead a `error` field with a message (e.g. VATMOSS errors) + errorData = { + info: { + public: { + message: data.error._, + }, + }, + } + } + callback(new SubscriptionErrors.RecurlyTransactionError(errorData)) + }) + }, + _parseSubscriptionsXml(xml, callback) { + return RecurlyWrapper._parseXmlAndGetAttribute( + xml, + 'subscriptions', + callback + ) + }, + + _parseSubscriptionXml(xml, callback) { + return RecurlyWrapper._parseXmlAndGetAttribute( + xml, + 'subscription', + callback + ) + }, + + _parseAccountXml(xml, callback) { + return RecurlyWrapper._parseXmlAndGetAttribute(xml, 'account', callback) + }, + + _parseBillingInfoXml(xml, callback) { + return RecurlyWrapper._parseXmlAndGetAttribute( + xml, + 'billing_info', + callback + ) + }, + + _parseRedemptionsXml(xml, callback) { + return RecurlyWrapper._parseXmlAndGetAttribute(xml, 'redemptions', callback) + }, + + _parseCouponXml(xml, callback) { + return RecurlyWrapper._parseXmlAndGetAttribute(xml, 'coupon', callback) + }, + + _parseErrorsXml(xml, callback) { + return RecurlyWrapper._parseXmlAndGetAttribute(xml, 'errors', callback) + }, + + _parseInvoicesXml(xml, callback) { + return RecurlyWrapper._parseXmlAndGetAttribute(xml, 'invoices', callback) + }, + + _parseXmlAndGetAttribute(xml, attribute, callback) { + return RecurlyWrapper._parseXml(xml, function (error, data) { + if (error != null) { + return callback(error) + } + if (data != null && data[attribute] != null) { + return callback(null, data[attribute]) + } else { + return callback( + new Error("I don't understand the response from Recurly") + ) + } + }) + }, + + _parseXml(xml, callback) { + var convertDataTypes = function (data) { + let key, value + if (data != null && data.$ != null) { + if (data.$.nil === 'nil') { + data = null + } else if (data.$.href != null) { + data.url = data.$.href + delete data.$ + } else if (data.$.type === 'integer') { + data = parseInt(data._, 10) + } else if (data.$.type === 'datetime') { + data = new Date(data._) + } else if (data.$.type === 'array') { + delete data.$ + let array = [] + for (key in data) { + value = data[key] + if (value instanceof Array) { + array = array.concat(convertDataTypes(value)) + } else { + array.push(convertDataTypes(value)) + } + } + data = array + } + } + + if (data instanceof Array) { + data = Array.from(data).map(entry => convertDataTypes(entry)) + } else if (typeof data === 'object') { + for (key in data) { + value = data[key] + data[key] = convertDataTypes(value) + } + } + return data + } + + const parser = new xml2js.Parser({ + explicitRoot: true, + explicitArray: false, + emptyTag: '', + }) + return parser.parseString(xml, function (error, data) { + if (error != null) { + return callback(error) + } + const result = convertDataTypes(data) + return callback(null, result) + }) + }, + + _buildXml(rootName, data) { + const options = { + headless: true, + renderOpts: { + pretty: true, + indent: '\t', + }, + rootName, + } + const builder = new xml2js.Builder(options) + return builder.buildObject(data) + }, +} + +RecurlyWrapper.promises = { + updateAccountEmailAddress: promisify(updateAccountEmailAddress), +} + +module.exports = RecurlyWrapper + +function getCustomFieldsFromSubscriptionDetails(subscriptionDetails) { + if (!subscriptionDetails.ITMCampaign) { + return null + } + + const customFields = [ + { + name: 'itm_campaign', + value: subscriptionDetails.ITMCampaign, + }, + ] + if (subscriptionDetails.ITMContent) { + customFields.push({ + name: 'itm_content', + value: subscriptionDetails.ITMContent, + }) + } + return { custom_field: customFields } +} + +function getAddressFromSubscriptionDetails( + subscriptionDetails, + includeCompanyInfo +) { + const { address } = subscriptionDetails + + if (!address || !address.country) { + throw new Errors.InvalidError({ + message: 'Invalid country', + info: { + public: { + message: 'Invalid country', + }, + }, + }) + } + + const addressObject = { + address1: address.address1, + address2: address.address2 || '', + city: address.city || '', + state: address.state || '', + zip: address.zip || '', + country: address.country, + } + + if ( + includeCompanyInfo && + subscriptionDetails.billing_info && + subscriptionDetails.billing_info.company && + subscriptionDetails.billing_info.company !== '' + ) { + addressObject.company = subscriptionDetails.billing_info.company + if ( + subscriptionDetails.billing_info.vat_number && + subscriptionDetails.billing_info.vat_number !== '' + ) { + addressObject.vat_number = subscriptionDetails.billing_info.vat_number + } + } + + return addressObject +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js new file mode 100644 index 0000000000..d13059cfb1 --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionController.js @@ -0,0 +1,504 @@ +const SessionManager = require('../Authentication/SessionManager') +const SubscriptionHandler = require('./SubscriptionHandler') +const PlansLocator = require('./PlansLocator') +const SubscriptionViewModelBuilder = require('./SubscriptionViewModelBuilder') +const LimitationsManager = require('./LimitationsManager') +const RecurlyWrapper = require('./RecurlyWrapper') +const Settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const GeoIpLookup = require('../../infrastructure/GeoIpLookup') +const FeaturesUpdater = require('./FeaturesUpdater') +const planFeatures = require('./planFeatures') +const GroupPlansData = require('./GroupPlansData') +const V1SubscriptionManager = require('./V1SubscriptionManager') +const Errors = require('../Errors/Errors') +const HttpErrorHandler = require('../Errors/HttpErrorHandler') +const SubscriptionErrors = require('./Errors') +const SplitTestHandler = require('../SplitTests/SplitTestHandler') +const AnalyticsManager = require('../Analytics/AnalyticsManager') +const RecurlyEventHandler = require('./RecurlyEventHandler') +const { expressify } = require('../../util/promises') +const OError = require('@overleaf/o-error') +const _ = require('lodash') + +const SUBSCRIPTION_PAGE_SPLIT_TEST = 'subscription-page' + +async function plansPage(req, res) { + const plans = SubscriptionViewModelBuilder.buildPlansList() + + const { + currencyCode: recommendedCurrency, + } = await GeoIpLookup.promises.getCurrencyCode( + (req.query ? req.query.ip : undefined) || req.ip + ) + + res.render('subscriptions/plans', { + title: 'plans_and_pricing', + plans, + gaExperiments: Settings.gaExperiments.plansPage, + gaOptimize: true, + recomendedCurrency: recommendedCurrency, + planFeatures, + groupPlans: GroupPlansData, + }) +} + +// get to show the recurly.js page +async function paymentPage(req, res) { + const user = SessionManager.getSessionUser(req.session) + const plan = PlansLocator.findLocalPlanInSettings(req.query.planCode) + if (!plan) { + return HttpErrorHandler.unprocessableEntity(req, res, 'Plan not found') + } + const hasSubscription = await LimitationsManager.promises.userHasV1OrV2Subscription( + user + ) + if (hasSubscription) { + res.redirect('/user/subscription?hasSubscription=true') + } else { + // LimitationsManager.userHasV2Subscription only checks Mongo. Double check with + // Recurly as well at this point (we don't do this most places for speed). + const valid = await SubscriptionHandler.promises.validateNoSubscriptionInRecurly( + user._id + ) + if (!valid) { + res.redirect('/user/subscription?hasSubscription=true') + } else { + let currency = req.query.currency + ? req.query.currency.toUpperCase() + : undefined + const { + currencyCode: recommendedCurrency, + countryCode, + } = await GeoIpLookup.promises.getCurrencyCode( + (req.query ? req.query.ip : undefined) || req.ip + ) + if (recommendedCurrency && currency == null) { + currency = recommendedCurrency + } + res.render('subscriptions/new', { + title: 'subscribe', + currency, + countryCode, + plan, + showStudentPlan: req.query.ssp === 'true', + recurlyConfig: JSON.stringify({ + currency, + subdomain: Settings.apis.recurly.subdomain, + }), + showCouponField: !!req.query.scf, + showVatField: !!req.query.svf, + gaOptimize: true, + }) + } + } +} + +async function userSubscriptionPage(req, res) { + const user = SessionManager.getSessionUser(req.session) + const results = await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel( + user + ) + const { + personalSubscription, + memberGroupSubscriptions, + managedGroupSubscriptions, + confirmedMemberAffiliations, + managedInstitutions, + managedPublishers, + v1SubscriptionStatus, + } = results + const hasSubscription = await LimitationsManager.promises.userHasV1OrV2Subscription( + user + ) + const fromPlansPage = req.query.hasSubscription + const plans = SubscriptionViewModelBuilder.buildPlansList( + personalSubscription ? personalSubscription.plan : undefined + ) + + let subscriptionCopy = 'default' + if ( + personalSubscription || + hasSubscription || + (memberGroupSubscriptions && memberGroupSubscriptions.length > 0) || + (confirmedMemberAffiliations && + confirmedMemberAffiliations.length > 0 && + _.find(confirmedMemberAffiliations, affiliation => { + return affiliation.licence && affiliation.licence !== 'free' + })) + ) { + AnalyticsManager.recordEvent(user._id, 'subscription-page-view') + } else { + try { + const testSegmentation = await SplitTestHandler.promises.getTestSegmentation( + user._id, + SUBSCRIPTION_PAGE_SPLIT_TEST + ) + if (testSegmentation.enabled) { + subscriptionCopy = testSegmentation.variant + + AnalyticsManager.recordEvent(user._id, 'subscription-page-view', { + splitTestId: SUBSCRIPTION_PAGE_SPLIT_TEST, + splitTestVariantId: testSegmentation.variant, + }) + } else { + AnalyticsManager.recordEvent(user._id, 'subscription-page-view') + } + } catch (error) { + logger.error( + { err: error }, + `Failed to get segmentation for user '${user._id}' and split test '${SUBSCRIPTION_PAGE_SPLIT_TEST}'` + ) + AnalyticsManager.recordEvent(user._id, 'subscription-page-view') + } + } + + const data = { + title: 'your_subscription', + plans, + user, + hasSubscription, + subscriptionCopy, + fromPlansPage, + personalSubscription, + memberGroupSubscriptions, + managedGroupSubscriptions, + confirmedMemberAffiliations, + managedInstitutions, + managedPublishers, + v1SubscriptionStatus, + } + res.render('subscriptions/dashboard', data) +} + +function createSubscription(req, res, next) { + const user = SessionManager.getSessionUser(req.session) + const recurlyTokenIds = { + billing: req.body.recurly_token_id, + threeDSecureActionResult: + req.body.recurly_three_d_secure_action_result_token_id, + } + const { subscriptionDetails } = req.body + + LimitationsManager.userHasV1OrV2Subscription( + user, + function (err, hasSubscription) { + if (err) { + return next(err) + } + if (hasSubscription) { + logger.warn({ user_id: user._id }, 'user already has subscription') + return res.sendStatus(409) // conflict + } + return SubscriptionHandler.createSubscription( + user, + subscriptionDetails, + recurlyTokenIds, + function (err) { + if (!err) { + return res.sendStatus(201) + } + + if ( + err instanceof SubscriptionErrors.RecurlyTransactionError || + err instanceof Errors.InvalidError + ) { + logger.error({ err }, 'recurly transaction error, potential 422') + HttpErrorHandler.unprocessableEntity( + req, + res, + err.message, + OError.getFullInfo(err).public + ) + } else { + logger.warn( + { err, user_id: user._id }, + 'something went wrong creating subscription' + ) + next(err) + } + } + ) + } + ) +} + +function successfulSubscription(req, res, next) { + const user = SessionManager.getSessionUser(req.session) + return SubscriptionViewModelBuilder.buildUsersSubscriptionViewModel( + user, + function (error, { personalSubscription }) { + if (error) { + return next(error) + } + if (personalSubscription == null) { + res.redirect('/user/subscription/plans') + } else { + res.render('subscriptions/successful_subscription', { + title: 'thank_you', + personalSubscription, + }) + } + } + ) +} + +function cancelSubscription(req, res, next) { + const user = SessionManager.getSessionUser(req.session) + logger.log({ user_id: user._id }, 'canceling subscription') + SubscriptionHandler.cancelSubscription(user, function (err) { + if (err) { + OError.tag(err, 'something went wrong canceling subscription', { + user_id: user._id, + }) + return next(err) + } + // Note: this redirect isn't used in the main flow as the redirection is + // handled by Angular + res.redirect('/user/subscription/canceled') + }) +} + +function canceledSubscription(req, res, next) { + return res.render('subscriptions/canceled_subscription', { + title: 'subscription_canceled', + }) +} + +function cancelV1Subscription(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + logger.log({ userId }, 'canceling v1 subscription') + V1SubscriptionManager.cancelV1Subscription(userId, function (err) { + if (err) { + OError.tag(err, 'something went wrong canceling v1 subscription', { + userId, + }) + return next(err) + } + res.redirect('/user/subscription') + }) +} + +function updateSubscription(req, res, next) { + const origin = req && req.query ? req.query.origin : null + const user = SessionManager.getSessionUser(req.session) + const planCode = req.body.plan_code + if (planCode == null) { + const err = new Error('plan_code is not defined') + logger.warn( + { user_id: user._id, err, planCode, origin, body: req.body }, + '[Subscription] error in updateSubscription form' + ) + return next(err) + } + logger.log({ planCode, user_id: user._id }, 'updating subscription') + SubscriptionHandler.updateSubscription(user, planCode, null, function (err) { + if (err) { + OError.tag(err, 'something went wrong updating subscription', { + user_id: user._id, + }) + return next(err) + } + res.redirect('/user/subscription') + }) +} + +function cancelPendingSubscriptionChange(req, res, next) { + const user = SessionManager.getSessionUser(req.session) + logger.log({ user_id: user._id }, 'canceling pending subscription change') + SubscriptionHandler.cancelPendingSubscriptionChange(user, function (err) { + if (err) { + OError.tag( + err, + 'something went wrong canceling pending subscription change', + { + user_id: user._id, + } + ) + return next(err) + } + res.redirect('/user/subscription') + }) +} + +function updateAccountEmailAddress(req, res, next) { + const user = SessionManager.getSessionUser(req.session) + RecurlyWrapper.updateAccountEmailAddress( + user._id, + user.email, + function (error) { + if (error) { + return next(error) + } + res.sendStatus(200) + } + ) +} + +function reactivateSubscription(req, res, next) { + const user = SessionManager.getSessionUser(req.session) + logger.log({ user_id: user._id }, 'reactivating subscription') + SubscriptionHandler.reactivateSubscription(user, function (err) { + if (err) { + OError.tag(err, 'something went wrong reactivating subscription', { + user_id: user._id, + }) + return next(err) + } + res.redirect('/user/subscription') + }) +} + +function recurlyCallback(req, res, next) { + logger.log({ data: req.body }, 'received recurly callback') + const event = Object.keys(req.body)[0] + const eventData = req.body[event] + + RecurlyEventHandler.sendRecurlyAnalyticsEvent(event, eventData) + + if ( + [ + 'new_subscription_notification', + 'updated_subscription_notification', + 'expired_subscription_notification', + ].includes(event) + ) { + const recurlySubscription = eventData.subscription + SubscriptionHandler.syncSubscription( + recurlySubscription, + { ip: req.ip }, + function (err) { + if (err) { + return next(err) + } + res.sendStatus(200) + } + ) + } else if (event === 'billing_info_updated_notification') { + const recurlyAccountCode = eventData.account.account_code + SubscriptionHandler.attemptPaypalInvoiceCollection( + recurlyAccountCode, + function (err) { + if (err) { + return next(err) + } + res.sendStatus(200) + } + ) + } else { + res.sendStatus(200) + } +} + +function renderUpgradeToAnnualPlanPage(req, res, next) { + const user = SessionManager.getSessionUser(req.session) + LimitationsManager.userHasV2Subscription( + user, + function (err, hasSubscription, subscription) { + let planName + if (err) { + return next(err) + } + const planCode = subscription + ? subscription.planCode.toLowerCase() + : undefined + if ((planCode ? planCode.indexOf('annual') : undefined) !== -1) { + planName = 'annual' + } else if ((planCode ? planCode.indexOf('student') : undefined) !== -1) { + planName = 'student' + } else if ( + (planCode ? planCode.indexOf('collaborator') : undefined) !== -1 + ) { + planName = 'collaborator' + } + if (hasSubscription) { + res.render('subscriptions/upgradeToAnnual', { + title: 'Upgrade to annual', + planName, + }) + } else { + res.redirect('/user/subscription/plans') + } + } + ) +} + +function processUpgradeToAnnualPlan(req, res, next) { + const user = SessionManager.getSessionUser(req.session) + const { planName } = req.body + const couponCode = Settings.coupon_codes.upgradeToAnnualPromo[planName] + const annualPlanName = `${planName}-annual` + logger.log( + { user_id: user._id, planName: annualPlanName }, + 'user is upgrading to annual billing with discount' + ) + return SubscriptionHandler.updateSubscription( + user, + annualPlanName, + couponCode, + function (err) { + if (err) { + OError.tag(err, 'error updating subscription', { + user_id: user._id, + }) + return next(err) + } + res.sendStatus(200) + } + ) +} + +async function extendTrial(req, res) { + const user = SessionManager.getSessionUser(req.session) + const { + subscription, + } = await LimitationsManager.promises.userHasV2Subscription(user) + + try { + await SubscriptionHandler.promises.extendTrial(subscription, 14) + } catch (error) { + return res.sendStatus(500) + } + res.sendStatus(200) +} + +function recurlyNotificationParser(req, res, next) { + let xml = '' + req.on('data', chunk => (xml += chunk)) + req.on('end', () => + RecurlyWrapper._parseXml(xml, function (error, body) { + if (error) { + return next(error) + } + req.body = body + next() + }) + ) +} + +async function refreshUserFeatures(req, res) { + const { user_id: userId } = req.params + await FeaturesUpdater.promises.refreshFeatures(userId) + res.sendStatus(200) +} + +module.exports = { + plansPage: expressify(plansPage), + paymentPage: expressify(paymentPage), + userSubscriptionPage: expressify(userSubscriptionPage), + createSubscription, + successfulSubscription, + cancelSubscription, + canceledSubscription, + cancelV1Subscription, + updateSubscription, + cancelPendingSubscriptionChange, + updateAccountEmailAddress, + reactivateSubscription, + recurlyCallback, + renderUpgradeToAnnualPlanPage, + processUpgradeToAnnualPlan, + extendTrial: expressify(extendTrial), + recurlyNotificationParser, + refreshUserFeatures: expressify(refreshUserFeatures), +} diff --git a/services/web/app/src/Features/Subscription/SubscriptionFormatters.js b/services/web/app/src/Features/Subscription/SubscriptionFormatters.js new file mode 100644 index 0000000000..9e78d7ad49 --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionFormatters.js @@ -0,0 +1,56 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const dateformat = require('dateformat') +const settings = require('@overleaf/settings') + +const currenySymbols = { + EUR: '€', + USD: '$', + GBP: '£', + SEK: 'kr', + CAD: '$', + NOK: 'kr', + DKK: 'kr', + AUD: '$', + NZD: '$', + CHF: 'Fr', + SGD: '$', +} + +module.exports = { + formatPrice(priceInCents, currency) { + if (currency == null) { + currency = 'USD' + } + let string = priceInCents + '' + if (string.length === 2) { + string = `0${string}` + } + if (string.length === 1) { + string = `00${string}` + } + if (string.length === 0) { + string = '000' + } + const cents = string.slice(-2) + const dollars = string.slice(0, -2) + const symbol = currenySymbols[currency] + return `${symbol}${dollars}.${cents}` + }, + + formatDate(date) { + if (date == null) { + return null + } + return dateformat(date, 'dS mmmm yyyy') + }, +} diff --git a/services/web/app/src/Features/Subscription/SubscriptionGroupController.js b/services/web/app/src/Features/Subscription/SubscriptionGroupController.js new file mode 100644 index 0000000000..2985acbfbd --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionGroupController.js @@ -0,0 +1,73 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SubscriptionGroupHandler = require('./SubscriptionGroupHandler') +const OError = require('@overleaf/o-error') +const logger = require('logger-sharelatex') +const SubscriptionLocator = require('./SubscriptionLocator') +const SessionManager = require('../Authentication/SessionManager') +const _ = require('underscore') +const async = require('async') + +module.exports = { + removeUserFromGroup(req, res, next) { + const subscription = req.entity + const userToRemove_id = req.params.user_id + logger.log( + { subscriptionId: subscription._id, userToRemove_id }, + 'removing user from group subscription' + ) + return SubscriptionGroupHandler.removeUserFromGroup( + subscription._id, + userToRemove_id, + function (err) { + if (err != null) { + OError.tag(err, 'error removing user from group', { + subscriptionId: subscription._id, + userToRemove_id, + }) + return next(err) + } + return res.sendStatus(200) + } + ) + }, + + removeSelfFromGroup(req, res, next) { + const subscriptionId = req.query.subscriptionId + const userToRemove_id = SessionManager.getLoggedInUserId(req.session) + return SubscriptionLocator.getSubscription( + subscriptionId, + function (error, subscription) { + if (error != null) { + return next(error) + } + + return SubscriptionGroupHandler.removeUserFromGroup( + subscription._id, + userToRemove_id, + function (err) { + if (err != null) { + logger.err( + { err, userToRemove_id, subscriptionId }, + 'error removing self from group' + ) + return res.sendStatus(500) + } + return res.sendStatus(200) + } + ) + } + ) + }, +} diff --git a/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js b/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js new file mode 100644 index 0000000000..69743c2aea --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js @@ -0,0 +1,142 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const async = require('async') +const _ = require('underscore') +const { promisify } = require('util') +const SubscriptionUpdater = require('./SubscriptionUpdater') +const SubscriptionLocator = require('./SubscriptionLocator') +const UserGetter = require('../User/UserGetter') +const { Subscription } = require('../../models/Subscription') +const LimitationsManager = require('./LimitationsManager') +const logger = require('logger-sharelatex') +const OneTimeTokenHandler = require('../Security/OneTimeTokenHandler') +const EmailHandler = require('../Email/EmailHandler') +const settings = require('@overleaf/settings') +const NotificationsBuilder = require('../Notifications/NotificationsBuilder') +const UserMembershipViewModel = require('../UserMembership/UserMembershipViewModel') + +const SubscriptionGroupHandler = { + removeUserFromGroup(subscriptionId, userToRemove_id, callback) { + return SubscriptionUpdater.removeUserFromGroup( + subscriptionId, + userToRemove_id, + callback + ) + }, + + replaceUserReferencesInGroups(oldId, newId, callback) { + return Subscription.updateOne( + { admin_id: oldId }, + { admin_id: newId }, + function (error) { + if (error != null) { + return callback(error) + } + + return replaceInArray( + Subscription, + 'manager_ids', + oldId, + newId, + function (error) { + if (error != null) { + return callback(error) + } + + return replaceInArray( + Subscription, + 'member_ids', + oldId, + newId, + callback + ) + } + ) + } + ) + }, + + isUserPartOfGroup(user_id, subscription_id, callback) { + if (callback == null) { + callback = function (err, partOfGroup) {} + } + return SubscriptionLocator.getSubscriptionByMemberIdAndId( + user_id, + subscription_id, + function (err, subscription) { + let partOfGroup + if (subscription != null) { + partOfGroup = true + } else { + partOfGroup = false + } + return callback(err, partOfGroup) + } + ) + }, + + getTotalConfirmedUsersInGroup(subscription_id, callback) { + if (callback == null) { + callback = function (err, totalUsers) {} + } + return SubscriptionLocator.getSubscription( + subscription_id, + (err, subscription) => + callback( + err, + __guard__( + subscription != null ? subscription.member_ids : undefined, + x => x.length + ) + ) + ) + }, +} + +var replaceInArray = function (model, property, oldValue, newValue, callback) { + // Mongo won't let us pull and addToSet in the same query, so do it in + // two. Note we need to add first, since the query is based on the old user. + const query = {} + query[property] = oldValue + + const setNewValue = {} + setNewValue[property] = newValue + + const setOldValue = {} + setOldValue[property] = oldValue + + model.updateMany(query, { $addToSet: setNewValue }, function (error) { + if (error) { + return callback(error) + } + model.updateMany(query, { $pull: setOldValue }, callback) + }) +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} + +SubscriptionGroupHandler.promises = { + getTotalConfirmedUsersInGroup: promisify( + SubscriptionGroupHandler.getTotalConfirmedUsersInGroup + ), + isUserPartOfGroup: promisify(SubscriptionGroupHandler.isUserPartOfGroup), +} + +module.exports = SubscriptionGroupHandler diff --git a/services/web/app/src/Features/Subscription/SubscriptionHandler.js b/services/web/app/src/Features/Subscription/SubscriptionHandler.js new file mode 100644 index 0000000000..4d7027a261 --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionHandler.js @@ -0,0 +1,343 @@ +const async = require('async') +const RecurlyWrapper = require('./RecurlyWrapper') +const RecurlyClient = require('./RecurlyClient') +const { User } = require('../../models/User') +const { promisifyAll } = require('../../util/promises') +const logger = require('logger-sharelatex') +const SubscriptionUpdater = require('./SubscriptionUpdater') +const LimitationsManager = require('./LimitationsManager') +const EmailHandler = require('../Email/EmailHandler') +const PlansLocator = require('./PlansLocator') +const SubscriptionHelper = require('./SubscriptionHelper') + +const SubscriptionHandler = { + validateNoSubscriptionInRecurly(userId, callback) { + if (callback == null) { + callback = function () {} + } + RecurlyWrapper.listAccountActiveSubscriptions( + userId, + function (error, subscriptions) { + if (subscriptions == null) { + subscriptions = [] + } + if (error != null) { + return callback(error) + } + if (subscriptions.length > 0) { + SubscriptionUpdater.syncSubscription( + subscriptions[0], + userId, + function (error) { + if (error != null) { + return callback(error) + } + callback(null, false) + } + ) + } else { + callback(null, true) + } + } + ) + }, + + createSubscription(user, subscriptionDetails, recurlyTokenIds, callback) { + SubscriptionHandler.validateNoSubscriptionInRecurly( + user._id, + function (error, valid) { + if (error != null) { + return callback(error) + } + if (!valid) { + return callback(new Error('user already has subscription in recurly')) + } + RecurlyWrapper.createSubscription( + user, + subscriptionDetails, + recurlyTokenIds, + function (error, recurlySubscription) { + if (error != null) { + return callback(error) + } + return SubscriptionUpdater.syncSubscription( + recurlySubscription, + user._id, + function (error) { + if (error != null) { + return callback(error) + } + return callback() + } + ) + } + ) + } + ) + }, + + updateSubscription(user, planCode, couponCode, callback) { + LimitationsManager.userHasV2Subscription( + user, + function (err, hasSubscription, subscription) { + if (err) { + logger.warn( + { err, user_id: user._id, hasSubscription }, + 'there was an error checking user v2 subscription' + ) + } + if (!hasSubscription) { + return callback() + } else { + return async.series( + [ + function (cb) { + if (couponCode == null) { + return cb() + } + RecurlyWrapper.getSubscription( + subscription.recurlySubscription_id, + { includeAccount: true }, + function (err, usersSubscription) { + if (err != null) { + return cb(err) + } + RecurlyWrapper.redeemCoupon( + usersSubscription.account.account_code, + couponCode, + cb + ) + } + ) + }, + function (cb) { + let changeAtTermEnd + const currentPlan = PlansLocator.findLocalPlanInSettings( + subscription.planCode + ) + const newPlan = PlansLocator.findLocalPlanInSettings(planCode) + if (currentPlan && newPlan) { + changeAtTermEnd = SubscriptionHelper.shouldPlanChangeAtTermEnd( + currentPlan, + newPlan + ) + } else { + logger.error( + { currentPlan: subscription.planCode, newPlan: planCode }, + 'unable to locate both plans in settings' + ) + return cb( + new Error('unable to locate both plans in settings') + ) + } + const timeframe = changeAtTermEnd ? 'term_end' : 'now' + RecurlyClient.changeSubscriptionByUuid( + subscription.recurlySubscription_id, + { planCode: planCode, timeframe: timeframe }, + function (error, subscriptionChange) { + if (error != null) { + return cb(error) + } + // v2 recurly API wants a UUID, but UUID isn't included in the subscription change response + // we got the UUID from the DB using userHasV2Subscription() - it is the only property + // we need to be able to build a 'recurlySubscription' object for syncSubscription() + SubscriptionHandler.syncSubscription( + { uuid: subscription.recurlySubscription_id }, + user._id, + cb + ) + } + ) + }, + ], + callback + ) + } + } + ) + }, + + cancelPendingSubscriptionChange(user, callback) { + LimitationsManager.userHasV2Subscription( + user, + function (err, hasSubscription, subscription) { + if (err) { + return callback(err) + } + if (hasSubscription) { + RecurlyClient.removeSubscriptionChangeByUuid( + subscription.recurlySubscription_id, + function (error) { + if (error != null) { + return callback(error) + } + callback() + } + ) + } else { + callback() + } + } + ) + }, + + cancelSubscription(user, callback) { + LimitationsManager.userHasV2Subscription( + user, + function (err, hasSubscription, subscription) { + if (err) { + logger.warn( + { err, user_id: user._id, hasSubscription }, + 'there was an error checking user v2 subscription' + ) + } + if (hasSubscription) { + RecurlyClient.cancelSubscriptionByUuid( + subscription.recurlySubscription_id, + function (error) { + if (error != null) { + return callback(error) + } + const emailOpts = { + to: user.email, + first_name: user.first_name, + } + const ONE_HOUR_IN_MS = 1000 * 60 * 60 + setTimeout( + () => + EmailHandler.sendEmail( + 'canceledSubscription', + emailOpts, + err => { + if (err != null) { + logger.warn( + { err }, + 'failed to send confirmation email for subscription cancellation' + ) + } + } + ), + ONE_HOUR_IN_MS + ) + callback() + } + ) + } else { + callback() + } + } + ) + }, + + reactivateSubscription(user, callback) { + LimitationsManager.userHasV2Subscription( + user, + function (err, hasSubscription, subscription) { + if (err) { + logger.warn( + { err, user_id: user._id, hasSubscription }, + 'there was an error checking user v2 subscription' + ) + } + if (hasSubscription) { + RecurlyClient.reactivateSubscriptionByUuid( + subscription.recurlySubscription_id, + function (error) { + if (error != null) { + return callback(error) + } + EmailHandler.sendEmail( + 'reactivatedSubscription', + { to: user.email }, + err => { + if (err != null) { + logger.warn( + { err }, + 'failed to send reactivation confirmation email' + ) + } + } + ) + callback() + } + ) + } else { + callback() + } + } + ) + }, + + syncSubscription(recurlySubscription, requesterData, callback) { + RecurlyWrapper.getSubscription( + recurlySubscription.uuid, + { includeAccount: true }, + function (error, recurlySubscription) { + if (error != null) { + return callback(error) + } + User.findById( + recurlySubscription.account.account_code, + { _id: 1 }, + function (error, user) { + if (error != null) { + return callback(error) + } + if (user == null) { + return callback(new Error('no user found')) + } + SubscriptionUpdater.syncSubscription( + recurlySubscription, + user != null ? user._id : undefined, + requesterData, + callback + ) + } + ) + } + ) + }, + + // attempt to collect past due invoice for customer. Only do that when a) the + // customer is using Paypal and b) there is only one past due invoice. + // This is used because Recurly doesn't always attempt collection of paast due + // invoices after Paypal billing info were updated. + attemptPaypalInvoiceCollection(recurlyAccountCode, callback) { + RecurlyWrapper.getBillingInfo(recurlyAccountCode, (error, billingInfo) => { + if (error) { + return callback(error) + } + if (!billingInfo.paypal_billing_agreement_id) { + // this is not a Paypal user + return callback() + } + RecurlyWrapper.getAccountPastDueInvoices( + recurlyAccountCode, + (error, pastDueInvoices) => { + if (error) { + return callback(error) + } + if (pastDueInvoices.length !== 1) { + // no past due invoices, or more than one. Ignore. + return callback() + } + RecurlyWrapper.attemptInvoiceCollection( + pastDueInvoices[0].invoice_number, + callback + ) + } + ) + }) + }, + + extendTrial(subscription, daysToExend, callback) { + return RecurlyWrapper.extendTrial( + subscription.recurlySubscription_id, + daysToExend, + callback + ) + }, +} + +SubscriptionHandler.promises = promisifyAll(SubscriptionHandler) +module.exports = SubscriptionHandler diff --git a/services/web/app/src/Features/Subscription/SubscriptionHelper.js b/services/web/app/src/Features/Subscription/SubscriptionHelper.js new file mode 100644 index 0000000000..f3135f06b8 --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionHelper.js @@ -0,0 +1,11 @@ +/** + * If the user changes to a less expensive plan, we shouldn't apply the change immediately. + * This is to avoid unintended/artifical credits on users Recurly accounts. + */ +function shouldPlanChangeAtTermEnd(oldPlan, newPlan) { + return oldPlan.price > newPlan.price +} + +module.exports = { + shouldPlanChangeAtTermEnd, +} diff --git a/services/web/app/src/Features/Subscription/SubscriptionLocator.js b/services/web/app/src/Features/Subscription/SubscriptionLocator.js new file mode 100644 index 0000000000..3c7d8b5832 --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionLocator.js @@ -0,0 +1,139 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { promisify } = require('util') +const { Subscription } = require('../../models/Subscription') +const { DeletedSubscription } = require('../../models/DeletedSubscription') +const logger = require('logger-sharelatex') +require('./GroupPlansData') // make sure dynamic group plans are loaded + +const SubscriptionLocator = { + getUsersSubscription(user_or_id, callback) { + const user_id = SubscriptionLocator._getUserId(user_or_id) + return Subscription.findOne( + { admin_id: user_id }, + function (err, subscription) { + logger.log({ user_id }, 'got users subscription') + return callback(err, subscription) + } + ) + }, + + getUserIndividualSubscription(user_or_id, callback) { + const user_id = SubscriptionLocator._getUserId(user_or_id) + return Subscription.findOne( + { admin_id: user_id, groupPlan: false }, + function (err, subscription) { + logger.log({ user_id }, 'got users individual subscription') + return callback(err, subscription) + } + ) + }, + + getManagedGroupSubscriptions(user_or_id, callback) { + if (callback == null) { + callback = function (error, managedSubscriptions) {} + } + const user_id = SubscriptionLocator._getUserId(user_or_id) + return Subscription.find({ + manager_ids: user_or_id, + groupPlan: true, + }) + .populate('admin_id') + .exec(callback) + }, + + getMemberSubscriptions(user_or_id, callback) { + const user_id = SubscriptionLocator._getUserId(user_or_id) + return Subscription.find({ member_ids: user_id }) + .populate('admin_id') + .exec(callback) + }, + + getSubscription(subscription_id, callback) { + return Subscription.findOne({ _id: subscription_id }, callback) + }, + + getSubscriptionByMemberIdAndId(user_id, subscription_id, callback) { + return Subscription.findOne( + { member_ids: user_id, _id: subscription_id }, + { _id: 1 }, + callback + ) + }, + + getGroupSubscriptionsMemberOf(user_id, callback) { + return Subscription.find( + { member_ids: user_id }, + { _id: 1, planCode: 1 }, + callback + ) + }, + + getGroupsWithEmailInvite(email, callback) { + return Subscription.find({ invited_emails: email }, callback) + }, + + getGroupWithV1Id(v1TeamId, callback) { + return Subscription.findOne({ 'overleaf.id': v1TeamId }, callback) + }, + + getUserDeletedSubscriptions(userId, callback) { + DeletedSubscription.find({ 'subscription.admin_id': userId }, callback) + }, + + getDeletedSubscription(subscriptionId, callback) { + DeletedSubscription.findOne( + { + 'subscription._id': subscriptionId, + }, + callback + ) + }, + + _getUserId(user_or_id) { + if (user_or_id != null && user_or_id._id != null) { + return user_or_id._id + } else if (user_or_id != null) { + return user_or_id + } + }, +} + +SubscriptionLocator.promises = { + getUsersSubscription: promisify(SubscriptionLocator.getUsersSubscription), + getUserIndividualSubscription: promisify( + SubscriptionLocator.getUserIndividualSubscription + ), + getManagedGroupSubscriptions: promisify( + SubscriptionLocator.getManagedGroupSubscriptions + ), + getMemberSubscriptions: promisify(SubscriptionLocator.getMemberSubscriptions), + getSubscription: promisify(SubscriptionLocator.getSubscription), + getSubscriptionByMemberIdAndId: promisify( + SubscriptionLocator.getSubscriptionByMemberIdAndId + ), + getGroupSubscriptionsMemberOf: promisify( + SubscriptionLocator.getGroupSubscriptionsMemberOf + ), + getGroupsWithEmailInvite: promisify( + SubscriptionLocator.getGroupsWithEmailInvite + ), + getGroupWithV1Id: promisify(SubscriptionLocator.getGroupWithV1Id), + getUserDeletedSubscriptions: promisify( + SubscriptionLocator.getUserDeletedSubscriptions + ), + getDeletedSubscription: promisify(SubscriptionLocator.getDeletedSubscription), +} +module.exports = SubscriptionLocator diff --git a/services/web/app/src/Features/Subscription/SubscriptionRouter.js b/services/web/app/src/Features/Subscription/SubscriptionRouter.js new file mode 100644 index 0000000000..35e4671c6e --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionRouter.js @@ -0,0 +1,146 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const AuthenticationController = require('../Authentication/AuthenticationController') +const SubscriptionController = require('./SubscriptionController') +const SubscriptionGroupController = require('./SubscriptionGroupController') +const TeamInvitesController = require('./TeamInvitesController') +const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') +const Settings = require('@overleaf/settings') + +module.exports = { + apply(webRouter, privateApiRouter, publicApiRouter) { + if (!Settings.enableSubscriptions) { + return + } + + webRouter.get('/user/subscription/plans', SubscriptionController.plansPage) + + webRouter.get( + '/user/subscription', + AuthenticationController.requireLogin(), + SubscriptionController.userSubscriptionPage + ) + + webRouter.get( + '/user/subscription/new', + AuthenticationController.requireLogin(), + SubscriptionController.paymentPage + ) + + webRouter.get( + '/user/subscription/thank-you', + AuthenticationController.requireLogin(), + SubscriptionController.successfulSubscription + ) + + webRouter.get( + '/user/subscription/canceled', + AuthenticationController.requireLogin(), + SubscriptionController.canceledSubscription + ) + + webRouter.delete( + '/subscription/group/user', + AuthenticationController.requireLogin(), + SubscriptionGroupController.removeSelfFromGroup + ) + + // Team invites + webRouter.get( + '/subscription/invites/:token/', + AuthenticationController.requireLogin(), + TeamInvitesController.viewInvite + ) + webRouter.put( + '/subscription/invites/:token/', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'team-invite', + maxRequests: 10, + timeInterval: 60, + }), + TeamInvitesController.acceptInvite + ) + + // recurly callback + publicApiRouter.post( + '/user/subscription/callback', + AuthenticationController.requireBasicAuth({ + [Settings.apis.recurly.webhookUser]: Settings.apis.recurly.webhookPass, + }), + SubscriptionController.recurlyNotificationParser, + SubscriptionController.recurlyCallback + ) + + // user changes their account state + webRouter.post( + '/user/subscription/create', + AuthenticationController.requireLogin(), + SubscriptionController.createSubscription + ) + webRouter.post( + '/user/subscription/update', + AuthenticationController.requireLogin(), + SubscriptionController.updateSubscription + ) + webRouter.post( + '/user/subscription/cancel-pending', + AuthenticationController.requireLogin(), + SubscriptionController.cancelPendingSubscriptionChange + ) + webRouter.post( + '/user/subscription/cancel', + AuthenticationController.requireLogin(), + SubscriptionController.cancelSubscription + ) + webRouter.post( + '/user/subscription/reactivate', + AuthenticationController.requireLogin(), + SubscriptionController.reactivateSubscription + ) + + webRouter.post( + '/user/subscription/v1/cancel', + AuthenticationController.requireLogin(), + SubscriptionController.cancelV1Subscription + ) + + webRouter.put( + '/user/subscription/extend', + AuthenticationController.requireLogin(), + SubscriptionController.extendTrial + ) + + webRouter.get( + '/user/subscription/upgrade-annual', + AuthenticationController.requireLogin(), + SubscriptionController.renderUpgradeToAnnualPlanPage + ) + webRouter.post( + '/user/subscription/upgrade-annual', + AuthenticationController.requireLogin(), + SubscriptionController.processUpgradeToAnnualPlan + ) + + webRouter.post( + '/user/subscription/account/email', + AuthenticationController.requireLogin(), + SubscriptionController.updateAccountEmailAddress + ) + + // Currently used in acceptance tests only, as a way to trigger the syncing logic + return publicApiRouter.post( + '/user/:user_id/features/sync', + AuthenticationController.requirePrivateApiAuth(), + SubscriptionController.refreshUserFeatures + ) + }, +} diff --git a/services/web/app/src/Features/Subscription/SubscriptionUpdater.js b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js new file mode 100644 index 0000000000..050dd9a0d6 --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js @@ -0,0 +1,401 @@ +const { db, ObjectId } = require('../../infrastructure/mongodb') +const OError = require('@overleaf/o-error') +const async = require('async') +const { promisify, callbackify } = require('../../util/promises') +const { Subscription } = require('../../models/Subscription') +const SubscriptionLocator = require('./SubscriptionLocator') +const UserGetter = require('../User/UserGetter') +const PlansLocator = require('./PlansLocator') +const FeaturesUpdater = require('./FeaturesUpdater') +const AnalyticsManager = require('../Analytics/AnalyticsManager') +const { DeletedSubscription } = require('../../models/DeletedSubscription') +const logger = require('logger-sharelatex') + +/** + * Change the admin of the given subscription. + * + * If the subscription is a group, add the new admin as manager while keeping + * the old admin. Otherwise, replace the manager. + * + * Validation checks are assumed to have been made: + * * subscription exists + * * user exists + * * user does not have another subscription + * * subscription is not a Recurly subscription + * + * If the subscription is Recurly, we silently do nothing. + */ +function updateAdmin(subscription, adminId, callback) { + const query = { + _id: ObjectId(subscription._id), + customAccount: true, + } + const update = { + $set: { admin_id: ObjectId(adminId) }, + } + if (subscription.groupPlan) { + update.$addToSet = { manager_ids: ObjectId(adminId) } + } else { + update.$set.manager_ids = [ObjectId(adminId)] + } + Subscription.updateOne(query, update, callback) +} + +function syncSubscription( + recurlySubscription, + adminUserId, + requesterData, + callback +) { + if (!callback) { + callback = requesterData + requesterData = {} + } + SubscriptionLocator.getUsersSubscription( + adminUserId, + function (err, subscription) { + if (err != null) { + return callback(err) + } + if (subscription != null) { + updateSubscriptionFromRecurly( + recurlySubscription, + subscription, + requesterData, + callback + ) + } else { + _createNewSubscription(adminUserId, function (err, subscription) { + if (err != null) { + return callback(err) + } + updateSubscriptionFromRecurly( + recurlySubscription, + subscription, + requesterData, + callback + ) + }) + } + } + ) +} + +function addUserToGroup(subscriptionId, userId, callback) { + Subscription.updateOne( + { _id: subscriptionId }, + { $addToSet: { member_ids: userId } }, + function (err) { + if (err != null) { + return callback(err) + } + FeaturesUpdater.refreshFeatures(userId, 'add-to-group', function () { + callbackify(_sendUserGroupPlanCodeUserProperty)(userId, callback) + }) + } + ) +} + +function removeUserFromGroup(subscriptionId, userId, callback) { + Subscription.updateOne( + { _id: subscriptionId }, + { $pull: { member_ids: userId } }, + function (error) { + if (error) { + OError.tag(error, 'error removing user from group', { + subscriptionId, + userId, + }) + return callback(error) + } + UserGetter.getUser(userId, function (error, user) { + if (error) { + return callback(error) + } + FeaturesUpdater.refreshFeatures( + userId, + 'remove-user-from-group', + function () { + callbackify(_sendUserGroupPlanCodeUserProperty)(userId, callback) + } + ) + }) + } + ) +} + +function removeUserFromAllGroups(userId, callback) { + SubscriptionLocator.getMemberSubscriptions( + userId, + function (error, subscriptions) { + if (error) { + return callback(error) + } + if (!subscriptions) { + return callback() + } + const subscriptionIds = subscriptions.map(sub => sub._id) + const removeOperation = { $pull: { member_ids: userId } } + Subscription.updateMany( + { _id: subscriptionIds }, + removeOperation, + function (error) { + if (error) { + OError.tag(error, 'error removing user from groups', { + userId, + subscriptionIds, + }) + return callback(error) + } + UserGetter.getUser(userId, function (error, user) { + if (error) { + return callback(error) + } + FeaturesUpdater.refreshFeatures( + userId, + 'remove-user-from-groups', + function () { + callbackify(_sendUserGroupPlanCodeUserProperty)( + userId, + callback + ) + } + ) + }) + } + ) + } + ) +} + +function deleteWithV1Id(v1TeamId, callback) { + Subscription.deleteOne({ 'overleaf.id': v1TeamId }, callback) +} + +function deleteSubscription(subscription, deleterData, callback) { + if (callback == null) { + callback = function () {} + } + async.series( + [ + cb => + // 1. create deletedSubscription + createDeletedSubscription(subscription, deleterData, cb), + cb => + // 2. remove subscription + Subscription.deleteOne({ _id: subscription._id }, cb), + cb => + // 3. refresh users features + refreshUsersFeatures(subscription, cb), + ], + callback + ) +} + +function restoreSubscription(subscriptionId, callback) { + SubscriptionLocator.getDeletedSubscription( + subscriptionId, + function (err, deletedSubscription) { + if (err) { + return callback(err) + } + const subscription = deletedSubscription.subscription + async.series( + [ + cb => + // 1. upsert subscription + db.subscriptions.updateOne( + { _id: subscription._id }, + subscription, + { upsert: true }, + cb + ), + cb => + // 2. refresh users features. Do this before removing the + // subscription so the restore can be retried if this fails + refreshUsersFeatures(subscription, cb), + cb => + // 3. remove deleted subscription + DeletedSubscription.deleteOne( + { 'subscription._id': subscription._id }, + callback + ), + ], + callback + ) + } + ) +} + +function refreshUsersFeatures(subscription, callback) { + const userIds = [subscription.admin_id].concat(subscription.member_ids || []) + async.mapSeries( + userIds, + function (userId, cb) { + FeaturesUpdater.refreshFeatures(userId, 'subscription-updater', cb) + }, + callback + ) +} + +function createDeletedSubscription(subscription, deleterData, callback) { + subscription.teamInvites = [] + subscription.invited_emails = [] + const filter = { 'subscription._id': subscription._id } + const data = { + deleterData: { + deleterId: deleterData.id, + deleterIpAddress: deleterData.ip, + }, + subscription: subscription, + } + const options = { upsert: true, new: true, setDefaultsOnInsert: true } + DeletedSubscription.findOneAndUpdate(filter, data, options, callback) +} + +function _createNewSubscription(adminUserId, callback) { + const subscription = new Subscription({ + admin_id: adminUserId, + manager_ids: [adminUserId], + }) + subscription.save(err => callback(err, subscription)) +} + +function _deleteAndReplaceSubscriptionFromRecurly( + recurlySubscription, + subscription, + requesterData, + callback +) { + const adminUserId = subscription.admin_id + deleteSubscription(subscription, requesterData, err => { + if (err) { + return callback(err) + } + _createNewSubscription(adminUserId, (err, newSubscription) => { + if (err) { + return callback(err) + } + updateSubscriptionFromRecurly( + recurlySubscription, + newSubscription, + requesterData, + callback + ) + }) + }) +} + +function updateSubscriptionFromRecurly( + recurlySubscription, + subscription, + requesterData, + callback +) { + if (recurlySubscription.state === 'expired') { + return deleteSubscription(subscription, requesterData, callback) + } + const updatedPlanCode = recurlySubscription.plan.plan_code + const plan = PlansLocator.findLocalPlanInSettings(updatedPlanCode) + + if (plan == null) { + return callback(new Error(`plan code not found: ${updatedPlanCode}`)) + } + if (!plan.groupPlan && subscription.groupPlan) { + // If downgrading from group to individual plan, delete group sub and create a new one + return _deleteAndReplaceSubscriptionFromRecurly( + recurlySubscription, + subscription, + requesterData, + callback + ) + } + + subscription.recurlySubscription_id = recurlySubscription.uuid + subscription.planCode = updatedPlanCode + + if (plan.groupPlan) { + if (!subscription.groupPlan) { + subscription.member_ids = subscription.member_ids || [] + subscription.member_ids.push(subscription.admin_id) + } + + subscription.groupPlan = true + subscription.membersLimit = plan.membersLimit + + // Some plans allow adding more seats than the base plan provides. + // This is recorded as a subscription add on. + if ( + plan.membersLimitAddOn && + Array.isArray(recurlySubscription.subscription_add_ons) + ) { + recurlySubscription.subscription_add_ons.forEach(addOn => { + if (addOn.add_on_code === plan.membersLimitAddOn) { + subscription.membersLimit += addOn.quantity + } + }) + } + } + subscription.save(function (error) { + if (error) { + return callback(error) + } + refreshUsersFeatures(subscription, callback) + }) +} + +async function _sendUserGroupPlanCodeUserProperty(userId) { + try { + const subscriptions = + (await SubscriptionLocator.promises.getMemberSubscriptions(userId)) || [] + let bestPlanCode = null + let bestFeatures = {} + for (const subscription of subscriptions) { + const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode) + if ( + plan && + FeaturesUpdater.isFeatureSetBetter(plan.features, bestFeatures) + ) { + bestPlanCode = plan.planCode + bestFeatures = plan.features + } + } + AnalyticsManager.setUserProperty( + userId, + 'group-subscription-plan-code', + bestPlanCode + ) + } catch (error) { + logger.error( + { err: error }, + `Failed to update group-subscription-plan-code property for user ${userId}` + ) + } +} + +module.exports = { + updateAdmin, + syncSubscription, + deleteSubscription, + createDeletedSubscription, + addUserToGroup, + refreshUsersFeatures, + removeUserFromGroup, + removeUserFromAllGroups, + deleteWithV1Id, + restoreSubscription, + updateSubscriptionFromRecurly, + promises: { + updateAdmin: promisify(updateAdmin), + syncSubscription: promisify(syncSubscription), + addUserToGroup: promisify(addUserToGroup), + refreshUsersFeatures: promisify(refreshUsersFeatures), + removeUserFromGroup: promisify(removeUserFromGroup), + removeUserFromAllGroups: promisify(removeUserFromAllGroups), + deleteSubscription: promisify(deleteSubscription), + createDeletedSubscription: promisify(createDeletedSubscription), + deleteWithV1Id: promisify(deleteWithV1Id), + restoreSubscription: promisify(restoreSubscription), + updateSubscriptionFromRecurly: promisify(updateSubscriptionFromRecurly), + }, +} diff --git a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js new file mode 100644 index 0000000000..c7c0d922c6 --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js @@ -0,0 +1,341 @@ +const Settings = require('@overleaf/settings') +const RecurlyWrapper = require('./RecurlyWrapper') +const PlansLocator = require('./PlansLocator') +const SubscriptionFormatters = require('./SubscriptionFormatters') +const SubscriptionLocator = require('./SubscriptionLocator') +const V1SubscriptionManager = require('./V1SubscriptionManager') +const InstitutionsGetter = require('../Institutions/InstitutionsGetter') +const PublishersGetter = require('../Publishers/PublishersGetter') +const sanitizeHtml = require('sanitize-html') +const _ = require('underscore') +const async = require('async') +const SubscriptionHelper = require('./SubscriptionHelper') +const { promisify } = require('../../util/promises') + +function buildHostedLink(recurlySubscription, type) { + const recurlySubdomain = Settings.apis.recurly.subdomain + const hostedLoginToken = recurlySubscription.account.hosted_login_token + let path = '' + if (type === 'billingDetails') { + path = 'billing_info/edit?ht=' + } + if (hostedLoginToken && recurlySubdomain) { + return [ + 'https://', + recurlySubdomain, + '.recurly.com/account/', + path, + hostedLoginToken, + ].join('') + } +} + +function buildUsersSubscriptionViewModel(user, callback) { + async.auto( + { + personalSubscription(cb) { + SubscriptionLocator.getUsersSubscription(user, cb) + }, + recurlySubscription: [ + 'personalSubscription', + (cb, { personalSubscription }) => { + if ( + personalSubscription == null || + personalSubscription.recurlySubscription_id == null || + personalSubscription.recurlySubscription_id === '' + ) { + return cb(null, null) + } + RecurlyWrapper.getSubscription( + personalSubscription.recurlySubscription_id, + { includeAccount: true }, + cb + ) + }, + ], + recurlyCoupons: [ + 'recurlySubscription', + (cb, { recurlySubscription }) => { + if (!recurlySubscription) { + return cb(null, null) + } + const accountId = recurlySubscription.account.account_code + RecurlyWrapper.getAccountActiveCoupons(accountId, cb) + }, + ], + plan: [ + 'personalSubscription', + (cb, { personalSubscription }) => { + if (personalSubscription == null) { + return cb() + } + const plan = PlansLocator.findLocalPlanInSettings( + personalSubscription.planCode + ) + if (plan == null) { + return cb( + new Error( + `No plan found for planCode '${personalSubscription.planCode}'` + ) + ) + } + cb(null, plan) + }, + ], + memberGroupSubscriptions(cb) { + SubscriptionLocator.getMemberSubscriptions(user, cb) + }, + managedGroupSubscriptions(cb) { + SubscriptionLocator.getManagedGroupSubscriptions(user, cb) + }, + confirmedMemberAffiliations(cb) { + InstitutionsGetter.getConfirmedAffiliations(user._id, cb) + }, + managedInstitutions(cb) { + InstitutionsGetter.getManagedInstitutions(user._id, cb) + }, + managedPublishers(cb) { + PublishersGetter.getManagedPublishers(user._id, cb) + }, + v1SubscriptionStatus(cb) { + V1SubscriptionManager.getSubscriptionStatusFromV1( + user._id, + (error, status, v1Id) => { + if (error) { + return cb(error) + } + cb(null, status) + } + ) + }, + }, + (err, results) => { + if (err) { + return callback(err) + } + let { + personalSubscription, + memberGroupSubscriptions, + managedGroupSubscriptions, + confirmedMemberAffiliations, + managedInstitutions, + managedPublishers, + v1SubscriptionStatus, + recurlySubscription, + recurlyCoupons, + plan, + } = results + if (memberGroupSubscriptions == null) { + memberGroupSubscriptions = [] + } + if (managedGroupSubscriptions == null) { + managedGroupSubscriptions = [] + } + if (confirmedMemberAffiliations == null) { + confirmedMemberAffiliations = [] + } + if (managedInstitutions == null) { + managedInstitutions = [] + } + if (v1SubscriptionStatus == null) { + v1SubscriptionStatus = {} + } + if (recurlyCoupons == null) { + recurlyCoupons = [] + } + + if ( + personalSubscription && + typeof personalSubscription.toObject === 'function' + ) { + // Downgrade from Mongoose object, so we can add a recurly and plan attribute + personalSubscription = personalSubscription.toObject() + } + + if (plan != null) { + personalSubscription.plan = plan + } + + if (personalSubscription && recurlySubscription) { + const tax = recurlySubscription.tax_in_cents || 0 + // Some plans allow adding more seats than the base plan provides. + // This is recorded as a subscription add on. + // Note: tax_in_cents already includes the tax for any addon. + let addOnPrice = 0 + let additionalLicenses = 0 + if ( + plan.membersLimitAddOn && + Array.isArray(recurlySubscription.subscription_add_ons) + ) { + recurlySubscription.subscription_add_ons.forEach(addOn => { + if (addOn.add_on_code === plan.membersLimitAddOn) { + addOnPrice += addOn.quantity * addOn.unit_amount_in_cents + additionalLicenses += addOn.quantity + } + }) + } + const totalLicenses = (plan.membersLimit || 0) + additionalLicenses + personalSubscription.recurly = { + tax, + taxRate: recurlySubscription.tax_rate + ? parseFloat(recurlySubscription.tax_rate._) + : 0, + billingDetailsLink: buildHostedLink( + recurlySubscription, + 'billingDetails' + ), + accountManagementLink: buildHostedLink(recurlySubscription), + additionalLicenses, + totalLicenses, + nextPaymentDueAt: SubscriptionFormatters.formatDate( + recurlySubscription.current_period_ends_at + ), + currency: recurlySubscription.currency, + state: recurlySubscription.state, + trialEndsAtFormatted: SubscriptionFormatters.formatDate( + recurlySubscription.trial_ends_at + ), + trial_ends_at: recurlySubscription.trial_ends_at, + activeCoupons: recurlyCoupons, + account: recurlySubscription.account, + } + if (recurlySubscription.pending_subscription) { + const pendingPlan = PlansLocator.findLocalPlanInSettings( + recurlySubscription.pending_subscription.plan.plan_code + ) + if (pendingPlan == null) { + return callback( + new Error( + `No plan found for planCode '${personalSubscription.planCode}'` + ) + ) + } + let pendingAdditionalLicenses = 0 + let pendingAddOnTax = 0 + let pendingAddOnPrice = 0 + if (recurlySubscription.pending_subscription.subscription_add_ons) { + if ( + pendingPlan.membersLimitAddOn && + Array.isArray( + recurlySubscription.pending_subscription.subscription_add_ons + ) + ) { + recurlySubscription.pending_subscription.subscription_add_ons.forEach( + addOn => { + if (addOn.add_on_code === pendingPlan.membersLimitAddOn) { + pendingAddOnPrice += + addOn.quantity * addOn.unit_amount_in_cents + pendingAdditionalLicenses += addOn.quantity + } + } + ) + } + // Need to calculate tax ourselves as we don't get tax amounts for pending subs + pendingAddOnTax = + personalSubscription.recurly.taxRate * pendingAddOnPrice + } + const pendingSubscriptionTax = + personalSubscription.recurly.taxRate * + recurlySubscription.pending_subscription.unit_amount_in_cents + personalSubscription.recurly.price = SubscriptionFormatters.formatPrice( + recurlySubscription.pending_subscription.unit_amount_in_cents + + pendingAddOnPrice + + pendingAddOnTax + + pendingSubscriptionTax, + recurlySubscription.currency + ) + const pendingTotalLicenses = + (pendingPlan.membersLimit || 0) + pendingAdditionalLicenses + personalSubscription.recurly.pendingAdditionalLicenses = pendingAdditionalLicenses + personalSubscription.recurly.pendingTotalLicenses = pendingTotalLicenses + personalSubscription.pendingPlan = pendingPlan + } else { + personalSubscription.recurly.price = SubscriptionFormatters.formatPrice( + recurlySubscription.unit_amount_in_cents + addOnPrice + tax, + recurlySubscription.currency + ) + } + } + + for (const memberGroupSubscription of memberGroupSubscriptions) { + if (memberGroupSubscription.teamNotice) { + memberGroupSubscription.teamNotice = sanitizeHtml( + memberGroupSubscription.teamNotice + ) + } + } + + callback(null, { + personalSubscription, + managedGroupSubscriptions, + memberGroupSubscriptions, + confirmedMemberAffiliations, + managedInstitutions, + managedPublishers, + v1SubscriptionStatus, + }) + } + ) +} + +function buildPlansList(currentPlan) { + const { plans } = Settings + + const allPlans = {} + plans.forEach(plan => { + allPlans[plan.planCode] = plan + }) + + const result = { allPlans } + + if (currentPlan) { + result.planCodesChangingAtTermEnd = _.pluck( + _.filter(plans, plan => { + if (!plan.hideFromUsers) { + return SubscriptionHelper.shouldPlanChangeAtTermEnd(currentPlan, plan) + } + }), + 'planCode' + ) + } + + result.studentAccounts = _.filter( + plans, + plan => plan.planCode.indexOf('student') !== -1 + ) + + result.groupMonthlyPlans = _.filter( + plans, + plan => plan.groupPlan && !plan.annual + ) + + result.groupAnnualPlans = _.filter( + plans, + plan => plan.groupPlan && plan.annual + ) + + result.individualMonthlyPlans = _.filter( + plans, + plan => + !plan.groupPlan && + !plan.annual && + plan.planCode !== 'personal' && // Prevent the personal plan from appearing on the change-plans page + plan.planCode.indexOf('student') === -1 + ) + + result.individualAnnualPlans = _.filter( + plans, + plan => + !plan.groupPlan && plan.annual && plan.planCode.indexOf('student') === -1 + ) + + return result +} + +module.exports = { + buildUsersSubscriptionViewModel, + buildPlansList, + promises: { + buildUsersSubscriptionViewModel: promisify(buildUsersSubscriptionViewModel), + }, +} diff --git a/services/web/app/src/Features/Subscription/TeamInvitesController.js b/services/web/app/src/Features/Subscription/TeamInvitesController.js new file mode 100644 index 0000000000..bff6cd6d5c --- /dev/null +++ b/services/web/app/src/Features/Subscription/TeamInvitesController.js @@ -0,0 +1,142 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const TeamInvitesHandler = require('./TeamInvitesHandler') +const SessionManager = require('../Authentication/SessionManager') +const SubscriptionLocator = require('./SubscriptionLocator') +const ErrorController = require('../Errors/ErrorController') +const EmailHelper = require('../Helpers/EmailHelper') + +module.exports = { + createInvite(req, res, next) { + const teamManagerId = SessionManager.getLoggedInUserId(req.session) + const subscription = req.entity + const email = EmailHelper.parseEmail(req.body.email) + if (email == null) { + return res.status(422).json({ + error: { + code: 'invalid_email', + message: req.i18n.translate('invalid_email'), + }, + }) + } + + return TeamInvitesHandler.createInvite( + teamManagerId, + subscription, + email, + function (err, inviteUserData) { + if (err != null) { + if (err.alreadyInTeam) { + return res.status(400).json({ + error: { + code: 'user_already_added', + message: req.i18n.translate('user_already_added'), + }, + }) + } + if (err.limitReached) { + return res.status(400).json({ + error: { + code: 'group_full', + message: req.i18n.translate('group_full'), + }, + }) + } + return next(err) + } + return res.json({ user: inviteUserData }) + } + ) + }, + + viewInvite(req, res, next) { + const { token } = req.params + const userId = SessionManager.getLoggedInUserId(req.session) + + return TeamInvitesHandler.getInvite( + token, + function (err, invite, teamSubscription) { + if (err != null) { + return next(err) + } + + if (!invite) { + return ErrorController.notFound(req, res, next) + } + + return SubscriptionLocator.getUsersSubscription( + userId, + function (err, personalSubscription) { + if (err != null) { + return next(err) + } + + const hasIndividualRecurlySubscription = + personalSubscription != null && + personalSubscription.planCode.match(/(free|trial)/) == null && + personalSubscription.groupPlan === false && + personalSubscription.recurlySubscription_id != null && + personalSubscription.recurlySubscription_id !== '' + + return res.render('subscriptions/team/invite', { + inviterName: invite.inviterName, + inviteToken: invite.token, + hasIndividualRecurlySubscription, + appName: settings.appName, + expired: req.query.expired, + }) + } + ) + } + ) + }, + + acceptInvite(req, res, next) { + const { token } = req.params + const userId = SessionManager.getLoggedInUserId(req.session) + + return TeamInvitesHandler.acceptInvite( + token, + userId, + function (err, results) { + if (err != null) { + return next(err) + } + return res.sendStatus(204) + } + ) + }, + + revokeInvite(req, res, next) { + const subscription = req.entity + const email = EmailHelper.parseEmail(req.params.email) + const teamManagerId = SessionManager.getLoggedInUserId(req.session) + if (email == null) { + return res.sendStatus(400) + } + + return TeamInvitesHandler.revokeInvite( + teamManagerId, + subscription, + email, + function (err, results) { + if (err != null) { + return next(err) + } + return res.sendStatus(204) + } + ) + }, +} diff --git a/services/web/app/src/Features/Subscription/TeamInvitesHandler.js b/services/web/app/src/Features/Subscription/TeamInvitesHandler.js new file mode 100644 index 0000000000..3646dccd2c --- /dev/null +++ b/services/web/app/src/Features/Subscription/TeamInvitesHandler.js @@ -0,0 +1,284 @@ +let TeamInvitesHandler +const logger = require('logger-sharelatex') +const crypto = require('crypto') +const async = require('async') + +const settings = require('@overleaf/settings') +const { ObjectId } = require('mongodb') + +const { Subscription } = require('../../models/Subscription') + +const UserGetter = require('../User/UserGetter') +const SubscriptionLocator = require('./SubscriptionLocator') +const SubscriptionUpdater = require('./SubscriptionUpdater') +const LimitationsManager = require('./LimitationsManager') + +const EmailHandler = require('../Email/EmailHandler') +const EmailHelper = require('../Helpers/EmailHelper') + +const Errors = require('../Errors/Errors') + +module.exports = TeamInvitesHandler = { + getInvite(token, callback) { + return Subscription.findOne( + { 'teamInvites.token': token }, + function (err, subscription) { + if (err) { + return callback(err) + } + if (!subscription) { + return callback(new Errors.NotFoundError('team not found')) + } + + const invite = subscription.teamInvites.find(i => i.token === token) + callback(null, invite, subscription) + } + ) + }, + + createInvite(teamManagerId, subscription, email, callback) { + email = EmailHelper.parseEmail(email) + if (!email) { + return callback(new Error('invalid email')) + } + return UserGetter.getUser(teamManagerId, function (error, teamManager) { + if (error) { + return callback(error) + } + + removeLegacyInvite(subscription.id, email, function (error) { + if (error) { + return callback(error) + } + createInvite(subscription, email, teamManager, callback) + }) + }) + }, + + importInvite(subscription, inviterName, email, token, sentAt, callback) { + checkIfInviteIsPossible( + subscription, + email, + function (error, possible, reason) { + if (error) { + return callback(error) + } + if (!possible) { + return callback(reason) + } + + subscription.teamInvites.push({ + email, + inviterName, + token, + sentAt, + }) + + subscription.save(callback) + } + ) + }, + + acceptInvite(token, userId, callback) { + TeamInvitesHandler.getInvite(token, function (err, invite, subscription) { + if (err) { + return callback(err) + } + if (!invite) { + return callback(new Errors.NotFoundError('invite not found')) + } + + SubscriptionUpdater.addUserToGroup( + subscription._id, + userId, + function (err) { + if (err) { + return callback(err) + } + + removeInviteFromTeam(subscription.id, invite.email, callback) + } + ) + }) + }, + + revokeInvite(teamManagerId, subscription, email, callback) { + email = EmailHelper.parseEmail(email) + if (!email) { + return callback(new Error('invalid email')) + } + removeInviteFromTeam(subscription.id, email, callback) + }, + + // Legacy method to allow a user to receive a confirmation email if their + // email is in Subscription.invited_emails when they join. We'll remove this + // after a short while. + createTeamInvitesForLegacyInvitedEmail(email, callback) { + SubscriptionLocator.getGroupsWithEmailInvite(email, function (err, teams) { + if (err) { + return callback(err) + } + + async.map( + teams, + (team, cb) => + TeamInvitesHandler.createInvite(team.admin_id, team, email, cb), + callback + ) + }) + }, +} + +var createInvite = function (subscription, email, inviter, callback) { + checkIfInviteIsPossible( + subscription, + email, + function (error, possible, reason) { + if (error) { + return callback(error) + } + if (!possible) { + return callback(reason) + } + + // don't send invites when inviting self; add user directly to the group + const isInvitingSelf = inviter.emails.some( + emailData => emailData.email === email + ) + if (isInvitingSelf) { + return SubscriptionUpdater.addUserToGroup( + subscription._id, + inviter._id, + err => { + if (err) { + return callback(err) + } + + // legacy: remove any invite that might have been created in the past + removeInviteFromTeam(subscription._id, email, error => { + const inviteUserData = { + email: inviter.email, + first_name: inviter.first_name, + last_name: inviter.last_name, + invite: false, + } + callback(error, inviteUserData) + }) + } + ) + } + + const inviterName = getInviterName(inviter) + let invite = subscription.teamInvites.find( + invite => invite.email === email + ) + + if (invite) { + invite.sentAt = new Date() + } else { + invite = { + email, + inviterName, + token: crypto.randomBytes(32).toString('hex'), + sentAt: new Date(), + } + subscription.teamInvites.push(invite) + } + + subscription.save(function (error) { + if (error) { + return callback(error) + } + + const opts = { + to: email, + inviter, + acceptInviteUrl: `${settings.siteUrl}/subscription/invites/${invite.token}/`, + appName: settings.appName, + } + EmailHandler.sendEmail('verifyEmailToJoinTeam', opts, error => { + Object.assign(invite, { invite: true }) + callback(error, invite) + }) + }) + } + ) +} + +var removeInviteFromTeam = function (subscriptionId, email, callback) { + const searchConditions = { _id: new ObjectId(subscriptionId.toString()) } + const removeInvite = { $pull: { teamInvites: { email } } } + + async.series( + [ + cb => Subscription.updateOne(searchConditions, removeInvite, cb), + cb => removeLegacyInvite(subscriptionId, email, cb), + ], + callback + ) +} + +var removeLegacyInvite = (subscriptionId, email, callback) => + Subscription.updateOne( + { + _id: new ObjectId(subscriptionId.toString()), + }, + { + $pull: { + invited_emails: email, + }, + }, + callback + ) + +var checkIfInviteIsPossible = function (subscription, email, callback) { + if (!subscription.groupPlan) { + logger.log( + { subscriptionId: subscription.id }, + 'can not add members to a subscription that is not in a group plan' + ) + return callback(null, false, { wrongPlan: true }) + } + + if (LimitationsManager.teamHasReachedMemberLimit(subscription)) { + logger.log( + { subscriptionId: subscription.id }, + 'team has reached member limit' + ) + return callback(null, false, { limitReached: true }) + } + + UserGetter.getUserByAnyEmail(email, function (error, existingUser) { + if (error) { + return callback(error) + } + if (!existingUser) { + return callback(null, true) + } + + const existingMember = subscription.member_ids.find( + memberId => memberId.toString() === existingUser._id.toString() + ) + + if (existingMember) { + logger.log( + { subscriptionId: subscription.id, email }, + 'user already in team' + ) + callback(null, false, { alreadyInTeam: true }) + } else { + callback(null, true) + } + }) +} + +var getInviterName = function (inviter) { + let inviterName + if (inviter.first_name && inviter.last_name) { + inviterName = `${inviter.first_name} ${inviter.last_name} (${inviter.email})` + } else { + inviterName = inviter.email + } + + return inviterName +} diff --git a/services/web/app/src/Features/Subscription/UserFeaturesUpdater.js b/services/web/app/src/Features/Subscription/UserFeaturesUpdater.js new file mode 100644 index 0000000000..f9d555b1ed --- /dev/null +++ b/services/web/app/src/Features/Subscription/UserFeaturesUpdater.js @@ -0,0 +1,25 @@ +const { User } = require('../../models/User') + +module.exports = { + updateFeatures(userId, features, callback) { + const conditions = { _id: userId } + const update = { + featuresUpdatedAt: new Date(), + } + for (const key in features) { + const value = features[key] + update[`features.${key}`] = value + } + User.updateOne(conditions, update, (err, result) => + callback(err, features, (result ? result.nModified : 0) === 1) + ) + }, + + overrideFeatures(userId, features, callback) { + const conditions = { _id: userId } + const update = { features, featuresUpdatedAt: new Date() } + User.updateOne(conditions, update, (err, result) => + callback(err, (result ? result.nModified : 0) === 1) + ) + }, +} diff --git a/services/web/app/src/Features/Subscription/V1SubscriptionManager.js b/services/web/app/src/Features/Subscription/V1SubscriptionManager.js new file mode 100644 index 0000000000..ec56934f26 --- /dev/null +++ b/services/web/app/src/Features/Subscription/V1SubscriptionManager.js @@ -0,0 +1,216 @@ +/* eslint-disable + node/handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let V1SubscriptionManager +const UserGetter = require('../User/UserGetter') +const request = require('request') +const settings = require('@overleaf/settings') +const { V1ConnectionError, NotFoundError } = require('../Errors/Errors') + +module.exports = V1SubscriptionManager = { + // Returned planCode = 'v1_pro' | 'v1_pro_plus' | 'v1_student' | 'v1_free' | null + // For this to work, we need plans in settings with plan-codes: + // - 'v1_pro' + // - 'v1_pro_plus' + // - 'v1_student' + // - 'v1_free' + getPlanCodeFromV1(userId, callback) { + if (callback == null) { + callback = function (err, planCode, v1Id) {} + } + return V1SubscriptionManager._v1Request( + userId, + { + method: 'GET', + url(v1Id) { + return `/api/v1/sharelatex/users/${v1Id}/plan_code` + }, + }, + function (error, body, v1Id) { + if (error != null) { + return callback(error) + } + let planName = body != null ? body.plan_name : undefined + if (['pro', 'pro_plus', 'student', 'free'].includes(planName)) { + planName = `v1_${planName}` + } else { + // Throw away 'anonymous', etc as being equivalent to null + planName = null + } + return callback(null, planName, v1Id) + } + ) + }, + + getSubscriptionsFromV1(userId, callback) { + if (callback == null) { + callback = function (err, subscriptions, v1Id) {} + } + return V1SubscriptionManager._v1Request( + userId, + { + method: 'GET', + url(v1Id) { + return `/api/v1/sharelatex/users/${v1Id}/subscriptions` + }, + }, + callback + ) + }, + + getSubscriptionStatusFromV1(userId, callback) { + if (callback == null) { + callback = function (err, status) {} + } + return V1SubscriptionManager._v1Request( + userId, + { + method: 'GET', + url(v1Id) { + return `/api/v1/sharelatex/users/${v1Id}/subscription_status` + }, + }, + callback + ) + }, + + cancelV1Subscription(userId, callback) { + if (callback == null) { + callback = function (err) {} + } + return V1SubscriptionManager._v1Request( + userId, + { + method: 'DELETE', + url(v1Id) { + return `/api/v1/sharelatex/users/${v1Id}/subscription` + }, + }, + callback + ) + }, + + v1IdForUser(userId, callback) { + if (callback == null) { + callback = function (err, v1Id) {} + } + return UserGetter.getUser( + userId, + { 'overleaf.id': 1 }, + function (err, user) { + if (err != null) { + return callback(err) + } + const v1Id = __guard__( + user != null ? user.overleaf : undefined, + x => x.id + ) + + return callback(null, v1Id) + } + ) + }, + + // v1 accounts created before migration to v2 had github and mendeley for free + // but these are now paid-for features for new accounts (v1id > cutoff) + getGrandfatheredFeaturesForV1User(v1Id) { + const cutoff = settings.v1GrandfatheredFeaturesUidCutoff + if (cutoff == null) { + return {} + } + if (v1Id == null) { + return {} + } + + if (v1Id < cutoff) { + return settings.v1GrandfatheredFeatures || {} + } else { + return {} + } + }, + + _v1Request(userId, options, callback) { + if (callback == null) { + callback = function (err, body, v1Id) {} + } + if (!settings.apis.v1.url) { + return callback(null, null) + } + + return V1SubscriptionManager.v1IdForUser(userId, function (err, v1Id) { + if (err != null) { + return callback(err) + } + if (v1Id == null) { + return callback(null, null, null) + } + const url = options.url(v1Id) + return request( + { + baseUrl: settings.apis.v1.url, + url, + method: options.method, + auth: { + user: settings.apis.v1.user, + pass: settings.apis.v1.pass, + sendImmediately: true, + }, + json: true, + timeout: 15 * 1000, + }, + function (error, response, body) { + if (error != null) { + return callback( + new V1ConnectionError({ + message: 'no v1 connection', + info: { url }, + }).withCause(error) + ) + } + if (response && response.statusCode >= 500) { + return callback( + new V1ConnectionError({ + message: 'error from v1', + info: { + status: response.statusCode, + body: body, + }, + }) + ) + } + if (response.statusCode >= 200 && response.statusCode < 300) { + return callback(null, body, v1Id) + } else { + if (response.statusCode === 404) { + return callback(new NotFoundError(`v1 user not found: ${userId}`)) + } else { + return callback( + new Error( + `non-success code from v1: ${response.statusCode} ${ + options.method + } ${options.url(v1Id)}` + ) + ) + } + } + } + ) + }) + }, +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/Features/Subscription/planFeatures.js b/services/web/app/src/Features/Subscription/planFeatures.js new file mode 100644 index 0000000000..9b0267e3cb --- /dev/null +++ b/services/web/app/src/Features/Subscription/planFeatures.js @@ -0,0 +1,157 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +module.exports = [ + { + feature: 'number_collab', + value: 'str', + plans: { + free: '1', + personal: '1', + coll: '10', + prof: 'unlimited', + }, + student: '6', + }, + { + feature: 'unlimited_private', + value: 'bool', + info: 'unlimited_private_info', + plans: { + free: true, + personal: true, + coll: true, + prof: true, + }, + student: true, + }, + { + feature: 'realtime_collab', + value: 'bool', + info: 'realtime_collab_info', + plans: { + free: true, + personal: true, + coll: true, + prof: true, + }, + student: true, + }, + { + feature: 'thousands_templates', + value: 'bool', + info: 'hundreds_templates_info', + plans: { + free: true, + personal: true, + coll: true, + prof: true, + }, + student: true, + }, + { + feature: 'powerful_latex_editor', + value: 'bool', + info: 'latex_editor_info', + plans: { + free: true, + personal: true, + coll: true, + prof: true, + }, + student: true, + }, + { + feature: 'compile_timeout', + value: 'str', + plans: { + free: '1 min', + personal: '4 mins', + coll: '4 mins', + prof: '4 mins', + }, + student: '4 mins', + }, + { + feature: 'realtime_track_changes', + value: 'bool', + info: 'realtime_track_changes_info', + plans: { + free: false, + personal: false, + coll: true, + prof: true, + }, + student: true, + }, + { + feature: 'full_doc_history', + value: 'bool', + info: 'full_doc_history_info', + plans: { + free: false, + personal: true, + coll: true, + prof: true, + }, + student: true, + }, + { + feature: 'reference_search', + value: 'bool', + info: 'reference_search_info', + plans: { + free: false, + personal: true, + coll: true, + prof: true, + }, + student: true, + }, + { + feature: 'reference_sync', + info: 'reference_sync_info', + value: 'bool', + plans: { + free: false, + personal: true, + coll: true, + prof: true, + }, + student: true, + }, + { + feature: 'dropbox_integration_lowercase', + value: 'bool', + info: 'dropbox_integration_info', + plans: { + free: false, + personal: true, + coll: true, + prof: true, + }, + student: true, + }, + { + feature: 'github_integration_lowercase', + value: 'bool', + info: 'github_integration_info', + plans: { + free: false, + personal: true, + coll: true, + prof: true, + }, + student: true, + }, + { + feature: 'priority_support', + value: 'bool', + plans: { + free: false, + personal: true, + coll: true, + prof: true, + }, + student: true, + }, +] diff --git a/services/web/app/src/Features/SystemMessages/SystemMessageController.js b/services/web/app/src/Features/SystemMessages/SystemMessageController.js new file mode 100644 index 0000000000..2c22e97850 --- /dev/null +++ b/services/web/app/src/Features/SystemMessages/SystemMessageController.js @@ -0,0 +1,31 @@ +const Settings = require('@overleaf/settings') +const SessionManager = require('../Authentication/SessionManager') +const SystemMessageManager = require('./SystemMessageManager') + +const ProjectController = { + getMessages(req, res, next) { + if (!SessionManager.isUserLoggedIn(req.session)) { + // gracefully handle requests from anonymous users + return res.json([]) + } + SystemMessageManager.getMessages((err, messages) => { + if (err) { + next(err) + } else { + if (!Settings.siteIsOpen) { + // Override all messages with notice for admins when site is closed. + messages = [ + { + content: + 'SITE IS CLOSED TO PUBLIC. OPEN ONLY FOR SITE ADMINS. DO NOT EDIT PROJECTS.', + _id: 'protected', // prevents hiding message in frontend + }, + ] + } + res.json(messages || []) + } + }) + }, +} + +module.exports = ProjectController diff --git a/services/web/app/src/Features/SystemMessages/SystemMessageManager.js b/services/web/app/src/Features/SystemMessages/SystemMessageManager.js new file mode 100644 index 0000000000..632cb0a841 --- /dev/null +++ b/services/web/app/src/Features/SystemMessages/SystemMessageManager.js @@ -0,0 +1,56 @@ +/* eslint-disable + node/handle-callback-err, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let SystemMessageManager +const { SystemMessage } = require('../../models/SystemMessage') + +module.exports = SystemMessageManager = { + getMessages(callback) { + if (callback == null) { + callback = function (error, messages) {} + } + callback(null, this._cachedMessages) + }, + + getMessagesFromDB(callback) { + if (callback == null) { + callback = function (error, messages) {} + } + return SystemMessage.find({}, callback) + }, + + clearMessages(callback) { + if (callback == null) { + callback = function (error) {} + } + return SystemMessage.deleteMany({}, callback) + }, + + createMessage(content, callback) { + if (callback == null) { + callback = function (error) {} + } + const message = new SystemMessage({ content }) + return message.save(callback) + }, + + refreshCache() { + this.getMessagesFromDB((error, messages) => { + if (!error) { + this._cachedMessages = messages + } + }) + }, +} + +const CACHE_TIMEOUT = 10 * 1000 * (Math.random() + 2) // 20-30 seconds +SystemMessageManager.refreshCache() +setInterval(() => SystemMessageManager.refreshCache(), CACHE_TIMEOUT) diff --git a/services/web/app/src/Features/Tags/TagsController.js b/services/web/app/src/Features/Tags/TagsController.js new file mode 100644 index 0000000000..93def0564e --- /dev/null +++ b/services/web/app/src/Features/Tags/TagsController.js @@ -0,0 +1,93 @@ +const TagsHandler = require('./TagsHandler') +const SessionManager = require('../Authentication/SessionManager') +const Errors = require('../Errors/Errors') + +const TagsController = { + _getTags(userId, _req, res, next) { + if (!userId) { + return next(new Errors.NotFoundError()) + } + TagsHandler.getAllTags(userId, function (error, allTags) { + if (error != null) { + return next(error) + } + res.json(allTags) + }) + }, + + apiGetAllTags(req, res, next) { + const { userId } = req.params + TagsController._getTags(userId, req, res, next) + }, + + getAllTags(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + TagsController._getTags(userId, req, res, next) + }, + + createTag(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + const { name } = req.body + TagsHandler.createTag(userId, name, function (error, tag) { + if (error != null) { + return next(error) + } + res.json(tag) + }) + }, + + addProjectToTag(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + const { tagId, projectId } = req.params + TagsHandler.addProjectToTag(userId, tagId, projectId, function (error) { + if (error) { + return next(error) + } + res.status(204).end() + }) + }, + + removeProjectFromTag(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + const { tagId, projectId } = req.params + TagsHandler.removeProjectFromTag( + userId, + tagId, + projectId, + function (error) { + if (error) { + return next(error) + } + res.status(204).end() + } + ) + }, + + deleteTag(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + const { tagId } = req.params + TagsHandler.deleteTag(userId, tagId, function (error) { + if (error) { + return next(error) + } + res.status(204).end() + }) + }, + + renameTag(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + const { tagId } = req.params + const name = req.body != null ? req.body.name : undefined + if (!name) { + return res.status(400).end() + } + TagsHandler.renameTag(userId, tagId, name, function (error) { + if (error) { + return next(error) + } + res.status(204).end() + }) + }, +} + +module.exports = TagsController diff --git a/services/web/app/src/Features/Tags/TagsHandler.js b/services/web/app/src/Features/Tags/TagsHandler.js new file mode 100644 index 0000000000..7531e24225 --- /dev/null +++ b/services/web/app/src/Features/Tags/TagsHandler.js @@ -0,0 +1,116 @@ +const { Tag } = require('../../models/Tag') +const { promisifyAll } = require('../../util/promises') + +function getAllTags(userId, callback) { + Tag.find({ user_id: userId }, callback) +} + +function createTag(userId, name, callback) { + if (!callback) { + callback = function () {} + } + Tag.create({ user_id: userId, name }, function (err, tag) { + // on duplicate key error return existing tag + if (err && err.code === 11000) { + return Tag.findOne({ user_id: userId, name }, callback) + } + callback(err, tag) + }) +} + +function renameTag(userId, tagId, name, callback) { + if (!callback) { + callback = function () {} + } + Tag.updateOne( + { + _id: tagId, + user_id: userId, + }, + { + $set: { + name, + }, + }, + callback + ) +} + +function deleteTag(userId, tagId, callback) { + if (!callback) { + callback = function () {} + } + Tag.deleteOne( + { + _id: tagId, + user_id: userId, + }, + callback + ) +} + +// TODO: unused? +function updateTagUserIds(oldUserId, newUserId, callback) { + if (!callback) { + callback = function () {} + } + const searchOps = { user_id: oldUserId } + const updateOperation = { $set: { user_id: newUserId } } + Tag.updateMany(searchOps, updateOperation, callback) +} + +function removeProjectFromTag(userId, tagId, projectId, callback) { + if (!callback) { + callback = function () {} + } + const searchOps = { + _id: tagId, + user_id: userId, + } + const deleteOperation = { $pull: { project_ids: projectId } } + Tag.updateOne(searchOps, deleteOperation, callback) +} + +function addProjectToTag(userId, tagId, projectId, callback) { + if (!callback) { + callback = function () {} + } + const searchOps = { + _id: tagId, + user_id: userId, + } + const insertOperation = { $addToSet: { project_ids: projectId } } + Tag.findOneAndUpdate(searchOps, insertOperation, callback) +} + +function addProjectToTagName(userId, name, projectId, callback) { + if (!callback) { + callback = function () {} + } + const searchOps = { + name, + user_id: userId, + } + const insertOperation = { $addToSet: { project_ids: projectId } } + Tag.updateOne(searchOps, insertOperation, { upsert: true }, callback) +} + +function removeProjectFromAllTags(userId, projectId, callback) { + const searchOps = { user_id: userId } + const deleteOperation = { $pull: { project_ids: projectId } } + Tag.updateMany(searchOps, deleteOperation, callback) +} + +const TagsHandler = { + getAllTags, + createTag, + renameTag, + deleteTag, + updateTagUserIds, + removeProjectFromTag, + addProjectToTag, + addProjectToTagName, + removeProjectFromAllTags, +} +TagsHandler.promises = promisifyAll(TagsHandler) +module.exports = TagsHandler diff --git a/services/web/app/src/Features/Templates/TemplatesController.js b/services/web/app/src/Features/Templates/TemplatesController.js new file mode 100644 index 0000000000..27c7f14fc0 --- /dev/null +++ b/services/web/app/src/Features/Templates/TemplatesController.js @@ -0,0 +1,69 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let TemplatesController +const path = require('path') +const SessionManager = require('../Authentication/SessionManager') +const TemplatesManager = require('./TemplatesManager') +const ProjectHelper = require('../Project/ProjectHelper') +const logger = require('logger-sharelatex') + +module.exports = TemplatesController = { + getV1Template(req, res) { + const templateVersionId = req.params.Template_version_id + const templateId = req.query.id + if (!/^[0-9]+$/.test(templateVersionId) || !/^[0-9]+$/.test(templateId)) { + logger.err( + { templateVersionId, templateId }, + 'invalid template id or version' + ) + return res.sendStatus(400) + } + const data = {} + data.templateVersionId = templateVersionId + data.templateId = templateId + data.name = req.query.templateName + data.compiler = ProjectHelper.compilerFromV1Engine(req.query.latexEngine) + data.imageName = req.query.texImage + data.mainFile = req.query.mainFile + data.brandVariationId = req.query.brandVariationId + return res.render( + path.resolve( + __dirname, + '../../../views/project/editor/new_from_template' + ), + data + ) + }, + + createProjectFromV1Template(req, res, next) { + const user_id = SessionManager.getLoggedInUserId(req.session) + return TemplatesManager.createProjectFromV1Template( + req.body.brandVariationId, + req.body.compiler, + req.body.mainFile, + req.body.templateId, + req.body.templateName, + req.body.templateVersionId, + user_id, + req.body.imageName, + function (err, project) { + if (err != null) { + return next(err) + } + delete req.session.templateData + return res.redirect(`/project/${project._id}`) + } + ) + }, +} diff --git a/services/web/app/src/Features/Templates/TemplatesManager.js b/services/web/app/src/Features/Templates/TemplatesManager.js new file mode 100644 index 0000000000..38d599f934 --- /dev/null +++ b/services/web/app/src/Features/Templates/TemplatesManager.js @@ -0,0 +1,206 @@ +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { Project } = require('../../models/Project') +const OError = require('@overleaf/o-error') +const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler') +const ProjectOptionsHandler = require('../Project/ProjectOptionsHandler') +const ProjectRootDocManager = require('../Project/ProjectRootDocManager') +const ProjectUploadManager = require('../Uploads/ProjectUploadManager') +const FileWriter = require('../../infrastructure/FileWriter') +const async = require('async') +const fs = require('fs') +const util = require('util') +const logger = require('logger-sharelatex') +const request = require('request') +const requestPromise = require('request-promise-native') +const settings = require('@overleaf/settings') +const uuid = require('uuid') +const Errors = require('../Errors/Errors') +const _ = require('underscore') + +const TemplatesManager = { + createProjectFromV1Template( + brandVariationId, + compiler, + mainFile, + templateId, + templateName, + templateVersionId, + user_id, + imageName, + _callback + ) { + const callback = _.once(_callback) + const zipUrl = `${settings.apis.v1.url}/api/v1/sharelatex/templates/${templateVersionId}` + const zipReq = request(zipUrl, { + auth: { + user: settings.apis.v1.user, + pass: settings.apis.v1.pass, + }, + timeout: 60 * 1000, + }) + zipReq.on('error', function (err) { + logger.warn({ err }, 'error getting zip from template API') + return callback(err) + }) + return FileWriter.ensureDumpFolderExists(function (err) { + if (err != null) { + return callback(err) + } + + const projectName = ProjectDetailsHandler.fixProjectName(templateName) + const dumpPath = `${settings.path.dumpFolder}/${uuid.v4()}` + const writeStream = fs.createWriteStream(dumpPath) + const attributes = { + fromV1TemplateId: templateId, + fromV1TemplateVersionId: templateVersionId, + } + writeStream.on('close', function () { + if (zipReq.response.statusCode !== 200) { + logger.warn( + { uri: zipUrl, statusCode: zipReq.response.statusCode }, + 'non-success code getting zip from template API' + ) + return callback(new Error('get zip failed')) + } + return ProjectUploadManager.createProjectFromZipArchiveWithName( + user_id, + projectName, + dumpPath, + attributes, + function (err, project) { + if (err != null) { + OError.tag(err, 'problem building project from zip', { + zipReq, + }) + return callback(err) + } + return async.series( + [ + cb => TemplatesManager._setCompiler(project._id, compiler, cb), + cb => TemplatesManager._setImage(project._id, imageName, cb), + cb => TemplatesManager._setMainFile(project._id, mainFile, cb), + cb => + TemplatesManager._setBrandVariationId( + project._id, + brandVariationId, + cb + ), + ], + function (err) { + if (err != null) { + return callback(err) + } + fs.unlink(dumpPath, function (err) { + if (err != null) { + return logger.err({ err }, 'error unlinking template zip') + } + }) + const update = { + fromV1TemplateId: templateId, + fromV1TemplateVersionId: templateVersionId, + } + return Project.updateOne( + { _id: project._id }, + update, + {}, + function (err) { + if (err != null) { + return callback(err) + } + return callback(null, project) + } + ) + } + ) + } + ) + }) + return zipReq.pipe(writeStream) + }) + }, + + _setCompiler(project_id, compiler, callback) { + if (compiler == null) { + return callback() + } + return ProjectOptionsHandler.setCompiler(project_id, compiler, callback) + }, + + _setImage(project_id, imageName, callback) { + if (!imageName) { + imageName = 'wl_texlive:2018.1' + } + return ProjectOptionsHandler.setImageName(project_id, imageName, callback) + }, + + _setMainFile(project_id, mainFile, callback) { + if (mainFile == null) { + return callback() + } + return ProjectRootDocManager.setRootDocFromName( + project_id, + mainFile, + callback + ) + }, + + _setBrandVariationId(project_id, brandVariationId, callback) { + if (brandVariationId == null) { + return callback() + } + return ProjectOptionsHandler.setBrandVariationId( + project_id, + brandVariationId, + callback + ) + }, + + promises: { + async fetchFromV1(templateId) { + const { body, statusCode } = await requestPromise({ + baseUrl: settings.apis.v1.url, + url: `/api/v2/templates/${templateId}`, + method: 'GET', + auth: { + user: settings.apis.v1.user, + pass: settings.apis.v1.pass, + sendImmediately: true, + }, + resolveWithFullResponse: true, + simple: false, + json: true, + timeout: 60 * 1000, + }) + + if (statusCode === 404) { + throw new Errors.NotFoundError() + } + + if (statusCode !== 200) { + logger.warn( + { templateId }, + "[TemplateMetrics] Couldn't fetch template data from v1" + ) + throw new Error("Couldn't fetch template data from v1") + } + + return body + }, + }, +} + +TemplatesManager.fetchFromV1 = util.callbackify( + TemplatesManager.promises.fetchFromV1 +) +module.exports = TemplatesManager diff --git a/services/web/app/src/Features/Templates/TemplatesMiddleware.js b/services/web/app/src/Features/Templates/TemplatesMiddleware.js new file mode 100644 index 0000000000..8315f537b2 --- /dev/null +++ b/services/web/app/src/Features/Templates/TemplatesMiddleware.js @@ -0,0 +1,21 @@ +/* eslint-disable + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') + +module.exports = { + saveTemplateDataInSession(req, res, next) { + if (req.query.templateName) { + req.session.templateData = req.query + } + return next() + }, +} diff --git a/services/web/app/src/Features/Templates/TemplatesRouter.js b/services/web/app/src/Features/Templates/TemplatesRouter.js new file mode 100644 index 0000000000..567e0edfa9 --- /dev/null +++ b/services/web/app/src/Features/Templates/TemplatesRouter.js @@ -0,0 +1,29 @@ +const AuthenticationController = require('../Authentication/AuthenticationController') +const TemplatesController = require('./TemplatesController') +const TemplatesMiddleware = require('./TemplatesMiddleware') +const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') +const AnalyticsRegistrationSourceMiddleware = require('../Analytics/AnalyticsRegistrationSourceMiddleware') + +module.exports = { + apply(app) { + app.get( + '/project/new/template/:Template_version_id', + TemplatesMiddleware.saveTemplateDataInSession, + AuthenticationController.requireLogin(), + TemplatesController.getV1Template + ) + + app.post( + '/project/new/template', + AnalyticsRegistrationSourceMiddleware.setSource('template'), + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'create-project-from-template', + maxRequests: 20, + timeInterval: 60, + }), + TemplatesController.createProjectFromV1Template, + AnalyticsRegistrationSourceMiddleware.clearSource() + ) + }, +} diff --git a/services/web/app/src/Features/ThirdPartyDataStore/TpdsController.js b/services/web/app/src/Features/ThirdPartyDataStore/TpdsController.js new file mode 100644 index 0000000000..30741ee823 --- /dev/null +++ b/services/web/app/src/Features/ThirdPartyDataStore/TpdsController.js @@ -0,0 +1,139 @@ +let parseParams + +const TpdsUpdateHandler = require('./TpdsUpdateHandler') +const UpdateMerger = require('./UpdateMerger') +const Errors = require('../Errors/Errors') +const logger = require('logger-sharelatex') +const Path = require('path') +const metrics = require('@overleaf/metrics') +const NotificationsBuilder = require('../Notifications/NotificationsBuilder') +const SessionManager = require('../Authentication/SessionManager') +const TpdsQueueManager = require('./TpdsQueueManager').promises + +module.exports = { + // mergeUpdate and deleteUpdate are used by Dropbox, where the project is only passed as the name, as the + // first part of the file path. They have to check the project exists, find it, and create it if not. + // They also ignore 'noisy' files like .DS_Store, .gitignore, etc. + mergeUpdate(req, res) { + metrics.inc('tpds.merge-update') + const { filePath, userId, projectName } = parseParams(req) + const source = req.headers['x-sl-update-source'] || 'unknown' + + TpdsUpdateHandler.newUpdate( + userId, + projectName, + filePath, + req, + source, + err => { + if (err) { + if (err.name === 'TooManyRequestsError') { + logger.warn( + { err, userId, filePath }, + 'tpds update failed to be processed, too many requests' + ) + res.sendStatus(429) + } else if (err.message === 'project_has_too_many_files') { + logger.warn( + { err, userId, filePath }, + 'tpds trying to append to project over file limit' + ) + NotificationsBuilder.tpdsFileLimit(userId).create(projectName) + res.sendStatus(400) + } else { + logger.err( + { err, userId, filePath }, + 'error receiving update from tpds' + ) + res.sendStatus(500) + } + } else { + res.sendStatus(200) + } + } + ) + }, + + deleteUpdate(req, res) { + metrics.inc('tpds.delete-update') + const { filePath, userId, projectName } = parseParams(req) + const source = req.headers['x-sl-update-source'] || 'unknown' + TpdsUpdateHandler.deleteUpdate( + userId, + projectName, + filePath, + source, + err => { + if (err) { + logger.err( + { err, userId, filePath }, + 'error receiving update from tpds' + ) + res.sendStatus(500) + } else { + res.sendStatus(200) + } + } + ) + }, + + // updateProjectContents and deleteProjectContents are used by GitHub. The project_id is known so we + // can skip right ahead to creating/updating/deleting the file. These methods will not ignore noisy + // files like .DS_Store, .gitignore, etc because people are generally more explicit with the files they + // want in git. + updateProjectContents(req, res, next) { + const projectId = req.params.project_id + const path = `/${req.params[0]}` // UpdateMerger expects leading slash + const source = req.headers['x-sl-update-source'] || 'unknown' + UpdateMerger.mergeUpdate(null, projectId, path, req, source, error => { + if (error) { + if (error.constructor === Errors.InvalidNameError) { + return res.sendStatus(422) + } else { + return next(error) + } + } + res.sendStatus(200) + }) + }, + + deleteProjectContents(req, res, next) { + const projectId = req.params.project_id + const path = `/${req.params[0]}` // UpdateMerger expects leading slash + const source = req.headers['x-sl-update-source'] || 'unknown' + + UpdateMerger.deleteUpdate(null, projectId, path, source, error => { + if (error) { + return next(error) + } + res.sendStatus(200) + }) + }, + + async getQueues(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + try { + res.json(await TpdsQueueManager.getQueues(userId)) + } catch (err) { + next(err) + } + }, + + parseParams: (parseParams = function (req) { + let filePath, projectName + let path = req.params[0] + const userId = req.params.user_id + + path = Path.join('/', path) + if (path.substring(1).indexOf('/') === -1) { + filePath = '/' + projectName = path.substring(1) + } else { + filePath = path.substring(path.indexOf('/', 1)) + projectName = path.substring(0, path.indexOf('/', 1)) + projectName = projectName.replace('/', '') + } + + return { filePath, userId, projectName } + }), +} diff --git a/services/web/app/src/Features/ThirdPartyDataStore/TpdsProjectFlusher.js b/services/web/app/src/Features/ThirdPartyDataStore/TpdsProjectFlusher.js new file mode 100644 index 0000000000..984e1eff99 --- /dev/null +++ b/services/web/app/src/Features/ThirdPartyDataStore/TpdsProjectFlusher.js @@ -0,0 +1,99 @@ +const { callbackify } = require('util') +const logger = require('logger-sharelatex') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const ProjectGetter = require('../Project/ProjectGetter') +const ProjectEntityHandler = require('../Project/ProjectEntityHandler') +const { Project } = require('../../models/Project') +const TpdsUpdateSender = require('./TpdsUpdateSender') + +module.exports = { + flushProjectToTpds: callbackify(flushProjectToTpds), + deferProjectFlushToTpds: callbackify(deferProjectFlushToTpds), + flushProjectToTpdsIfNeeded: callbackify(flushProjectToTpdsIfNeeded), + promises: { + flushProjectToTpds, + deferProjectFlushToTpds, + flushProjectToTpdsIfNeeded, + }, +} + +/** + * Flush a complete project to the TPDS. + */ +async function flushProjectToTpds(projectId) { + const project = await ProjectGetter.promises.getProject(projectId, { + name: true, + deferredTpdsFlushCounter: true, + }) + await _flushProjectToTpds(project) +} + +/** + * Flush a project to TPDS if a flush is pending + */ +async function flushProjectToTpdsIfNeeded(projectId) { + const project = await ProjectGetter.promises.getProject(projectId, { + name: true, + deferredTpdsFlushCounter: true, + }) + if (project.deferredTpdsFlushCounter > 0) { + await _flushProjectToTpds(project) + } +} + +async function _flushProjectToTpds(project) { + logger.debug({ projectId: project._id }, 'flushing project to TPDS') + logger.debug({ projectId: project._id }, 'finished flushing project to TPDS') + await DocumentUpdaterHandler.promises.flushProjectToMongo(project._id) + const [docs, files] = await Promise.all([ + ProjectEntityHandler.promises.getAllDocs(project._id), + ProjectEntityHandler.promises.getAllFiles(project._id), + ]) + for (const [docPath, doc] of Object.entries(docs)) { + await TpdsUpdateSender.promises.addDoc({ + project_id: project._id, + doc_id: doc._id, + path: docPath, + project_name: project.name, + rev: doc.rev || 0, + }) + } + for (const [filePath, file] of Object.entries(files)) { + await TpdsUpdateSender.promises.addFile({ + project_id: project._id, + file_id: file._id, + path: filePath, + project_name: project.name, + rev: file.rev, + }) + } + await _resetDeferredTpdsFlushCounter(project) +} + +/** + * Reset the TPDS pending flush counter. + * + * To avoid concurrency problems, the flush counter is not reset if it has been + * incremented since we fetched it from the database. + */ +async function _resetDeferredTpdsFlushCounter(project) { + if (project.deferredTpdsFlushCounter > 0) { + await Project.updateOne( + { + _id: project._id, + deferredTpdsFlushCounter: { $lte: project.deferredTpdsFlushCounter }, + }, + { $set: { deferredTpdsFlushCounter: 0 } } + ).exec() + } +} + +/** + * Mark a project as pending a flush to TPDS. + */ +async function deferProjectFlushToTpds(projectId) { + await Project.updateOne( + { _id: projectId }, + { $inc: { deferredTpdsFlushCounter: 1 } } + ).exec() +} diff --git a/services/web/app/src/Features/ThirdPartyDataStore/TpdsQueueManager.js b/services/web/app/src/Features/ThirdPartyDataStore/TpdsQueueManager.js new file mode 100644 index 0000000000..084d727cd4 --- /dev/null +++ b/services/web/app/src/Features/ThirdPartyDataStore/TpdsQueueManager.js @@ -0,0 +1,15 @@ +const Settings = require('@overleaf/settings') +const request = require('request-promise-native') + +async function getQueues(userId) { + return request({ + uri: `${Settings.apis.tpdsworker.url}/queues/${userId}`, + json: true, + }) +} + +module.exports = { + promises: { + getQueues, + }, +} diff --git a/services/web/app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler.js b/services/web/app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler.js new file mode 100644 index 0000000000..7cf8f7a29d --- /dev/null +++ b/services/web/app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler.js @@ -0,0 +1,159 @@ +const UpdateMerger = require('./UpdateMerger') +const logger = require('logger-sharelatex') +const NotificationsBuilder = require('../Notifications/NotificationsBuilder') +const ProjectCreationHandler = require('../Project/ProjectCreationHandler') +const ProjectDeleter = require('../Project/ProjectDeleter') +const ProjectGetter = require('../Project/ProjectGetter') +const ProjectHelper = require('../Project/ProjectHelper') +const ProjectRootDocManager = require('../Project/ProjectRootDocManager') +const FileTypeManager = require('../Uploads/FileTypeManager') +const CooldownManager = require('../Cooldown/CooldownManager') +const Errors = require('../Errors/Errors') +const Modules = require('../../infrastructure/Modules') + +const ROOT_DOC_TIMEOUT_LENGTH = 30 * 1000 + +function newUpdate(userId, projectName, path, updateRequest, source, callback) { + getOrCreateProject(userId, projectName, (err, project) => { + if (err) { + return callback(err) + } + if (project == null) { + return callback() + } + CooldownManager.isProjectOnCooldown( + project._id, + (err, projectIsOnCooldown) => { + if (err) { + return callback(err) + } + if (projectIsOnCooldown) { + return callback( + new Errors.TooManyRequestsError('project on cooldown') + ) + } + FileTypeManager.shouldIgnore(path, (err, shouldIgnore) => { + if (shouldIgnore) { + return callback() + } + UpdateMerger.mergeUpdate( + userId, + project._id, + path, + updateRequest, + source, + callback + ) + }) + } + ) + }) +} + +function deleteUpdate(userId, projectName, path, source, callback) { + logger.debug({ userId, filePath: path }, 'handling delete update from tpds') + ProjectGetter.findUsersProjectsByName( + userId, + projectName, + (err, projects) => { + if (err) { + return callback(err) + } + const activeProjects = projects.filter( + project => !ProjectHelper.isArchivedOrTrashed(project, userId) + ) + if (activeProjects.length === 0) { + logger.debug( + { userId, filePath: path, projectName }, + 'project not found from tpds update, ignoring folder or project' + ) + return callback() + } + if (projects.length > 1) { + // There is more than one project with that name, and one of them is + // active (previous condition) + return handleDuplicateProjects(userId, projectName, callback) + } + + const project = activeProjects[0] + if (path === '/') { + logger.debug( + { userId, filePath: path, projectName, project_id: project._id }, + 'project found for delete update, path is root so marking project as deleted' + ) + ProjectDeleter.markAsDeletedByExternalSource(project._id, callback) + } else { + UpdateMerger.deleteUpdate(userId, project._id, path, source, err => { + callback(err) + }) + } + } + ) +} + +function getOrCreateProject(userId, projectName, callback) { + ProjectGetter.findUsersProjectsByName( + userId, + projectName, + (err, projects) => { + if (err) { + return callback(err) + } + + if (projects.length === 0) { + // No project with that name -- active, archived or trashed -- has been + // found. Create one. + return ProjectCreationHandler.createBlankProject( + userId, + projectName, + (err, project) => { + // have a crack at setting the root doc after a while, on creation + // we won't have it yet, but should have been sent it it within 30 + // seconds + setTimeout(() => { + ProjectRootDocManager.setRootDocAutomatically(project._id) + }, ROOT_DOC_TIMEOUT_LENGTH) + callback(err, project) + } + ) + } + const activeProjects = projects.filter( + project => !ProjectHelper.isArchivedOrTrashed(project, userId) + ) + if (activeProjects.length === 0) { + // All projects with that name are archived or trashed. Ignore. + return callback(null, null) + } + + if (projects.length > 1) { + // There is more than one project with that name, and one of them is + // active (previous condition) + return handleDuplicateProjects(userId, projectName, err => { + if (err) { + return callback(err) + } + callback(null, null) + }) + } + + callback(err, activeProjects[0]) + } + ) +} + +function handleDuplicateProjects(userId, projectName, callback) { + Modules.hooks.fire('removeDropbox', userId, 'duplicate-projects', err => { + if (err) { + return callback(err) + } + NotificationsBuilder.dropboxDuplicateProjectNames(userId).create( + projectName, + callback + ) + }) +} + +module.exports = { + newUpdate, + deleteUpdate, +} diff --git a/services/web/app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.js b/services/web/app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.js new file mode 100644 index 0000000000..7353e254e6 --- /dev/null +++ b/services/web/app/src/Features/ThirdPartyDataStore/TpdsUpdateSender.js @@ -0,0 +1,237 @@ +const { ObjectId } = require('mongodb') +const _ = require('lodash') +const { callbackify } = require('util') +const logger = require('logger-sharelatex') +const metrics = require('@overleaf/metrics') +const path = require('path') +const request = require('request-promise-native') +const settings = require('@overleaf/settings') + +const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') + .promises +const UserGetter = require('../User/UserGetter.js').promises + +const tpdsUrl = _.get(settings, ['apis', 'thirdPartyDataStore', 'url']) + +async function addDoc(options) { + metrics.inc('tpds.add-doc') + + options.streamOrigin = + settings.apis.docstore.pubUrl + + path.join( + `/project/${options.project_id}`, + `/doc/${options.doc_id}`, + '/raw' + ) + + return addEntity(options) +} + +async function addEntity(options) { + const projectUserIds = await getProjectUsersIds(options.project_id) + + for (const userId of projectUserIds) { + const job = { + method: 'post', + headers: { + sl_entity_rev: options.rev, + sl_project_id: options.project_id, + sl_all_user_ids: JSON.stringify([userId]), + sl_project_owner_user_id: projectUserIds[0], + }, + uri: buildTpdsUrl(userId, options.project_name, options.path), + title: 'addFile', + streamOrigin: options.streamOrigin, + } + + await enqueue(userId, 'pipeStreamFrom', job) + } +} + +async function addFile(options) { + metrics.inc('tpds.add-file') + + options.streamOrigin = + settings.apis.filestore.url + + path.join(`/project/${options.project_id}`, `/file/${options.file_id}`) + + return addEntity(options) +} + +function buildMovePaths(options) { + if (options.newProjectName) { + return { + startPath: path.join('/', options.project_name, '/'), + endPath: path.join('/', options.newProjectName, '/'), + } + } else { + return { + startPath: path.join('/', options.project_name, '/', options.startPath), + endPath: path.join('/', options.project_name, '/', options.endPath), + } + } +} + +function buildTpdsUrl(userId, projectName, filePath) { + const projectPath = encodeURIComponent(path.join(projectName, '/', filePath)) + return `${tpdsUrl}/user/${userId}/entity/${projectPath}` +} + +async function deleteEntity(options) { + metrics.inc('tpds.delete-entity') + + const projectUserIds = await getProjectUsersIds(options.project_id) + + for (const userId of projectUserIds) { + const job = { + method: 'delete', + headers: { + sl_project_id: options.project_id, + sl_all_user_ids: JSON.stringify([userId]), + sl_project_owner_user_id: projectUserIds[0], + }, + uri: buildTpdsUrl(userId, options.project_name, options.path), + title: 'deleteEntity', + sl_all_user_ids: JSON.stringify([userId]), + } + + await enqueue(userId, 'standardHttpRequest', job) + } +} + +async function deleteProject(options) { + // deletion only applies to project archiver + const projectArchiverUrl = _.get(settings, [ + 'apis', + 'project_archiver', + 'url', + ]) + // silently do nothing if project archiver url is not in settings + if (!projectArchiverUrl) { + return + } + metrics.inc('tpds.delete-project') + // send the request directly to project archiver, bypassing third-party-datastore + try { + const response = await request({ + uri: `${settings.apis.project_archiver.url}/project/${options.project_id}`, + method: 'delete', + }) + return response + } catch (err) { + logger.error( + { err, project_id: options.project_id }, + 'error deleting project in third party datastore (project_archiver)' + ) + } +} + +async function enqueue(group, method, job) { + const tpdsWorkerUrl = _.get(settings, ['apis', 'tpdsworker', 'url']) + // silently do nothing if worker url is not in settings + if (!tpdsWorkerUrl) { + return + } + try { + const response = await request({ + uri: `${tpdsWorkerUrl}/enqueue/web_to_tpds_http_requests`, + json: { group, job, method }, + method: 'post', + timeout: 5 * 1000, + }) + return response + } catch (err) { + // log error and continue + logger.error({ err, group, job, method }, 'error enqueueing tpdsworker job') + } +} + +async function getProjectUsersIds(projectId) { + // get list of all user ids with access to project. project owner + // will always be the first entry in the list. + const [ + ownerUserId, + ...invitedUserIds + ] = await CollaboratorsGetter.getInvitedMemberIds(projectId) + // if there are no invited users, always return the owner + if (!invitedUserIds.length) { + return [ownerUserId] + } + // filter invited users to only return those with dropbox linked + const dropboxUsers = await UserGetter.getUsers( + { + _id: { $in: invitedUserIds.map(id => ObjectId(id)) }, + 'dropbox.access_token.uid': { $ne: null }, + }, + { + _id: 1, + } + ) + const dropboxUserIds = dropboxUsers.map(user => user._id) + return [ownerUserId, ...dropboxUserIds] +} + +async function moveEntity(options) { + metrics.inc('tpds.move-entity') + + const projectUserIds = await getProjectUsersIds(options.project_id) + const { endPath, startPath } = buildMovePaths(options) + + for (const userId of projectUserIds) { + const job = { + method: 'put', + title: 'moveEntity', + uri: `${tpdsUrl}/user/${userId}/entity`, + headers: { + sl_project_id: options.project_id, + sl_entity_rev: options.rev, + sl_all_user_ids: JSON.stringify([userId]), + sl_project_owner_user_id: projectUserIds[0], + }, + json: { + user_id: userId, + endPath, + startPath, + }, + } + + await enqueue(userId, 'standardHttpRequest', job) + } +} + +async function pollDropboxForUser(userId) { + metrics.inc('tpds.poll-dropbox') + + const job = { + method: 'post', + uri: `${tpdsUrl}/user/poll`, + json: { + user_ids: [userId], + }, + } + + return enqueue(`poll-dropbox:${userId}`, 'standardHttpRequest', job) +} + +const TpdsUpdateSender = { + addDoc: callbackify(addDoc), + addEntity: callbackify(addEntity), + addFile: callbackify(addFile), + deleteEntity: callbackify(deleteEntity), + deleteProject: callbackify(deleteProject), + enqueue: callbackify(enqueue), + moveEntity: callbackify(moveEntity), + pollDropboxForUser: callbackify(pollDropboxForUser), + promises: { + addDoc, + addEntity, + addFile, + deleteEntity, + deleteProject, + enqueue, + moveEntity, + pollDropboxForUser, + }, +} + +module.exports = TpdsUpdateSender diff --git a/services/web/app/src/Features/ThirdPartyDataStore/UpdateMerger.js b/services/web/app/src/Features/ThirdPartyDataStore/UpdateMerger.js new file mode 100644 index 0000000000..5ad9585a8a --- /dev/null +++ b/services/web/app/src/Features/ThirdPartyDataStore/UpdateMerger.js @@ -0,0 +1,260 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UpdateMerger +const OError = require('@overleaf/o-error') +const _ = require('underscore') +const async = require('async') +const fs = require('fs') +const logger = require('logger-sharelatex') +const EditorController = require('../Editor/EditorController') +const FileTypeManager = require('../Uploads/FileTypeManager') +const FileWriter = require('../../infrastructure/FileWriter') +const ProjectEntityHandler = require('../Project/ProjectEntityHandler') + +module.exports = UpdateMerger = { + mergeUpdate(user_id, project_id, path, updateRequest, source, callback) { + if (callback == null) { + callback = function (error) {} + } + return FileWriter.writeStreamToDisk( + project_id, + updateRequest, + function (err, fsPath) { + if (err != null) { + return callback(err) + } + return UpdateMerger._mergeUpdate( + user_id, + project_id, + path, + fsPath, + source, + mergeErr => + fs.unlink(fsPath, function (deleteErr) { + if (deleteErr != null) { + logger.err({ project_id, fsPath }, 'error deleting file') + } + return callback(mergeErr) + }) + ) + } + ) + }, + + _findExistingFileType(project_id, path, callback) { + ProjectEntityHandler.getAllEntities( + project_id, + function (err, docs, files) { + if (err != null) { + return callback(err) + } + var existingFileType = null + if (_.some(files, f => f.path === path)) { + existingFileType = 'file' + } + if (_.some(docs, d => d.path === path)) { + existingFileType = 'doc' + } + callback(null, existingFileType) + } + ) + }, + + _determineFileType(project_id, path, fsPath, callback) { + if (callback == null) { + callback = function (err, fileType) {} + } + // check if there is an existing file with the same path (we either need + // to overwrite it or delete it) + UpdateMerger._findExistingFileType( + project_id, + path, + function (err, existingFileType) { + if (err) { + return callback(err) + } + // determine whether the update should create a doc or binary file + FileTypeManager.getType( + path, + fsPath, + function (err, { binary, encoding }) { + if (err != null) { + return callback(err) + } + + // If we receive a non-utf8 encoding, we won't be able to keep things in + // sync, so we'll treat non-utf8 files as binary + const isBinary = binary || encoding !== 'utf-8' + + // Existing | Update | Action + // ---------|-----------|------- + // file | isBinary | existing-file + // file | !isBinary | existing-file + // doc | isBinary | new-file, delete-existing-doc + // doc | !isBinary | existing-doc + // null | isBinary | new-file + // null | !isBinary | new-doc + + // if a binary file already exists, always keep it as a binary file + // even if the update looks like a text file + if (existingFileType === 'file') { + return callback(null, 'existing-file') + } + + // if there is an existing doc, keep it as a doc except when the + // incoming update is binary. In that case delete the doc and replace + // it with a new file. + if (existingFileType === 'doc') { + if (isBinary) { + return callback(null, 'new-file', 'delete-existing-doc') + } else { + return callback(null, 'existing-doc') + } + } + // if there no existing file, create a file or doc as needed + return callback(null, isBinary ? 'new-file' : 'new-doc') + } + ) + } + ) + }, + + _mergeUpdate(user_id, project_id, path, fsPath, source, callback) { + if (callback == null) { + callback = function (error) {} + } + return UpdateMerger._determineFileType( + project_id, + path, + fsPath, + function (err, fileType, deleteOriginalEntity) { + if (err != null) { + return callback(err) + } + async.series( + [ + function (cb) { + if (deleteOriginalEntity) { + // currently we only delete docs + UpdateMerger.deleteUpdate(user_id, project_id, path, source, cb) + } else { + cb() + } + }, + function (cb) { + if (['existing-file', 'new-file'].includes(fileType)) { + return UpdateMerger.p.processFile( + project_id, + fsPath, + path, + source, + user_id, + cb + ) + } else if (['existing-doc', 'new-doc'].includes(fileType)) { + return UpdateMerger.p.processDoc( + project_id, + user_id, + fsPath, + path, + source, + cb + ) + } else { + return cb(new Error('unrecognized file')) + } + }, + ], + callback + ) + } + ) + }, + + deleteUpdate(user_id, project_id, path, source, callback) { + if (callback == null) { + callback = function () {} + } + return EditorController.deleteEntityWithPath( + project_id, + path, + source, + user_id, + function () { + return callback() + } + ) + }, + + p: { + processDoc(project_id, user_id, fsPath, path, source, callback) { + return UpdateMerger.p.readFileIntoTextArray( + fsPath, + function (err, docLines) { + if (err != null) { + OError.tag( + err, + 'error reading file into text array for process doc update', + { + project_id, + } + ) + return callback(err) + } + logger.log({ docLines }, 'processing doc update from tpds') + return EditorController.upsertDocWithPath( + project_id, + path, + docLines, + source, + user_id, + function (err) { + return callback(err) + } + ) + } + ) + }, + + processFile(project_id, fsPath, path, source, user_id, callback) { + return EditorController.upsertFileWithPath( + project_id, + path, + fsPath, + null, + source, + user_id, + function (err) { + return callback(err) + } + ) + }, + + readFileIntoTextArray(path, callback) { + return fs.readFile(path, 'utf8', function (error, content) { + if (content == null) { + content = '' + } + if (error != null) { + OError.tag(error, 'error reading file into text array', { + path, + }) + return callback(error) + } + const lines = content.split(/\r\n|\n|\r/) + return callback(error, lines) + }) + }, + }, +} diff --git a/services/web/app/src/Features/TokenAccess/TokenAccessController.js b/services/web/app/src/Features/TokenAccess/TokenAccessController.js new file mode 100644 index 0000000000..aa0cb6e894 --- /dev/null +++ b/services/web/app/src/Features/TokenAccess/TokenAccessController.js @@ -0,0 +1,308 @@ +const AuthenticationController = require('../Authentication/AuthenticationController') +const SessionManager = require('../Authentication/SessionManager') +const TokenAccessHandler = require('./TokenAccessHandler') +const Errors = require('../Errors/Errors') +const logger = require('logger-sharelatex') +const settings = require('@overleaf/settings') +const OError = require('@overleaf/o-error') +const { expressify } = require('../../util/promises') +const AuthorizationManager = require('../Authorization/AuthorizationManager') +const PrivilegeLevels = require('../Authorization/PrivilegeLevels') + +const orderedPrivilegeLevels = [ + PrivilegeLevels.NONE, + PrivilegeLevels.READ_ONLY, + PrivilegeLevels.READ_AND_WRITE, + PrivilegeLevels.OWNER, +] + +async function _userAlreadyHasHigherPrivilege( + userId, + projectId, + token, + tokenType +) { + if (!Object.values(TokenAccessHandler.TOKEN_TYPES).includes(tokenType)) { + throw new Error('bad token type') + } + const privilegeLevel = await AuthorizationManager.promises.getPrivilegeLevelForProject( + userId, + projectId, + token + ) + return ( + orderedPrivilegeLevels.indexOf(privilegeLevel) >= + orderedPrivilegeLevels.indexOf(tokenType) + ) +} + +const makePostUrl = token => { + if (TokenAccessHandler.isReadAndWriteToken(token)) { + return `/${token}/grant` + } else if (TokenAccessHandler.isReadOnlyToken(token)) { + return `/read/${token}/grant` + } else { + throw new Error('invalid token type') + } +} + +async function _handleV1Project(token, userId) { + if (!userId) { + return { v1Import: { status: 'mustLogin' } } + } else { + const docInfo = await TokenAccessHandler.promises.getV1DocInfo( + token, + userId + ) + // This should not happen anymore, but it does show + // a nice "contact support" message, so it can stay + if (!docInfo) { + return { v1Import: { status: 'cannotImport' } } + } + if (!docInfo.exists) { + return null + } + if (docInfo.exported) { + return null + } + return { + v1Import: { + status: 'canDownloadZip', + projectId: token, + hasOwner: docInfo.has_owner, + name: docInfo.name || 'Untitled', + brandInfo: docInfo.brand_info, + }, + } + } +} + +async function tokenAccessPage(req, res, next) { + const { token } = req.params + if (!TokenAccessHandler.isValidToken(token)) { + return next(new Errors.NotFoundError()) + } + try { + if (TokenAccessHandler.isReadOnlyToken(token)) { + const docPublishedInfo = await TokenAccessHandler.promises.getV1DocPublishedInfo( + token + ) + if (docPublishedInfo.allow === false) { + return res.redirect(302, docPublishedInfo.published_path) + } + } + res.render('project/token/access', { + postUrl: makePostUrl(token), + }) + } catch (err) { + return next( + OError.tag(err, 'error while rendering token access page', { token }) + ) + } +} + +async function checkAndGetProjectOrResponseAction( + tokenType, + token, + userId, + req, + res, + next +) { + // Try to get the project, and/or an alternative action to take. + // Returns a tuple of [project, action] + const project = await TokenAccessHandler.promises.getProjectByToken( + tokenType, + token + ) + if (!project) { + if (settings.overleaf) { + const v1ImportData = await _handleV1Project(token, userId) + return [ + null, + () => { + if (v1ImportData) { + res.json(v1ImportData) + } else { + res.sendStatus(404) + } + }, + ] + } else { + return [null, null] + } + } + + const projectId = project._id + const isAnonymousUser = !userId + const tokenAccessEnabled = TokenAccessHandler.tokenAccessEnabledForProject( + project + ) + if (isAnonymousUser && tokenAccessEnabled) { + if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE) { + if (TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED) { + logger.info({ projectId }, 'granting read-write anonymous access') + TokenAccessHandler.grantSessionTokenAccess(req, projectId, token) + return [ + null, + () => { + res.json({ + redirect: `/project/${projectId}`, + grantAnonymousAccess: tokenType, + }) + }, + ] + } else { + logger.warn( + { token, projectId }, + '[TokenAccess] deny anonymous read-and-write token access' + ) + AuthenticationController.setRedirectInSession( + req, + TokenAccessHandler.makeTokenUrl(token) + ) + return [ + null, + () => { + res.json({ + redirect: '/restricted', + anonWriteAccessDenied: true, + }) + }, + ] + } + } else if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY) { + logger.info({ projectId }, 'granting read-only anonymous access') + TokenAccessHandler.grantSessionTokenAccess(req, projectId, token) + return [ + null, + () => { + res.json({ + redirect: `/project/${projectId}`, + grantAnonymousAccess: tokenType, + }) + }, + ] + } else { + throw new Error('unreachable') + } + } + const userHasPrivilege = await _userAlreadyHasHigherPrivilege( + userId, + projectId, + token, + tokenType + ) + if (userHasPrivilege) { + return [ + null, + () => { + res.json({ redirect: `/project/${project._id}`, higherAccess: true }) + }, + ] + } + if (!tokenAccessEnabled) { + return [ + null, + () => { + next(new Errors.NotFoundError()) + }, + ] + } + return [project, null] +} + +async function grantTokenAccessReadAndWrite(req, res, next) { + const { token } = req.params + const userId = SessionManager.getLoggedInUserId(req.session) + if (!TokenAccessHandler.isReadAndWriteToken(token)) { + return res.sendStatus(400) + } + const tokenType = TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE + try { + const [project, action] = await checkAndGetProjectOrResponseAction( + tokenType, + token, + userId, + req, + res, + next + ) + if (action) { + return action() + } + if (!project) { + return next(new Errors.NotFoundError()) + } + await TokenAccessHandler.promises.addReadAndWriteUserToProject( + userId, + project._id + ) + return res.json({ + redirect: `/project/${project._id}`, + tokenAccessGranted: tokenType, + }) + } catch (err) { + return next( + OError.tag( + err, + 'error while trying to grant read-and-write token access', + { token } + ) + ) + } +} + +async function grantTokenAccessReadOnly(req, res, next) { + const { token } = req.params + const userId = SessionManager.getLoggedInUserId(req.session) + if (!TokenAccessHandler.isReadOnlyToken(token)) { + return res.sendStatus(400) + } + const tokenType = TokenAccessHandler.TOKEN_TYPES.READ_ONLY + const docPublishedInfo = await TokenAccessHandler.promises.getV1DocPublishedInfo( + token + ) + if (docPublishedInfo.allow === false) { + return res.json({ redirect: docPublishedInfo.published_path }) + } + try { + const [project, action] = await checkAndGetProjectOrResponseAction( + tokenType, + token, + userId, + req, + res, + next + ) + if (action) { + return action() + } + if (!project) { + return next(new Errors.NotFoundError()) + } + await TokenAccessHandler.promises.addReadOnlyUserToProject( + userId, + project._id + ) + return res.json({ + redirect: `/project/${project._id}`, + tokenAccessGranted: tokenType, + }) + } catch (err) { + return next( + OError.tag(err, 'error while trying to grant read-only token access', { + token, + }) + ) + } +} + +module.exports = { + READ_ONLY_TOKEN_PATTERN: TokenAccessHandler.READ_ONLY_TOKEN_PATTERN, + READ_AND_WRITE_TOKEN_PATTERN: TokenAccessHandler.READ_AND_WRITE_TOKEN_PATTERN, + + tokenAccessPage: expressify(tokenAccessPage), + grantTokenAccessReadOnly: expressify(grantTokenAccessReadOnly), + grantTokenAccessReadAndWrite: expressify(grantTokenAccessReadAndWrite), +} diff --git a/services/web/app/src/Features/TokenAccess/TokenAccessHandler.js b/services/web/app/src/Features/TokenAccess/TokenAccessHandler.js new file mode 100644 index 0000000000..d97cb81c04 --- /dev/null +++ b/services/web/app/src/Features/TokenAccess/TokenAccessHandler.js @@ -0,0 +1,305 @@ +const { Project } = require('../../models/Project') +const PublicAccessLevels = require('../Authorization/PublicAccessLevels') +const PrivilegeLevels = require('../Authorization/PrivilegeLevels') +const { ObjectId } = require('mongodb') +const Settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const V1Api = require('../V1/V1Api') +const crypto = require('crypto') +const { promisifyAll } = require('../../util/promises') +const Analytics = require('../Analytics/AnalyticsManager') + +const READ_AND_WRITE_TOKEN_PATTERN = '([0-9]+[a-z]{6,12})' +const READ_ONLY_TOKEN_PATTERN = '([a-z]{12})' + +const TokenAccessHandler = { + TOKEN_TYPES: { + READ_ONLY: PrivilegeLevels.READ_ONLY, + READ_AND_WRITE: PrivilegeLevels.READ_AND_WRITE, + }, + + ANONYMOUS_READ_AND_WRITE_ENABLED: + Settings.allowAnonymousReadAndWriteSharing === true, + + READ_AND_WRITE_TOKEN_PATTERN, + READ_AND_WRITE_TOKEN_REGEX: new RegExp(`^${READ_AND_WRITE_TOKEN_PATTERN}$`), + READ_AND_WRITE_URL_REGEX: new RegExp(`^/${READ_AND_WRITE_TOKEN_PATTERN}$`), + + READ_ONLY_TOKEN_PATTERN, + READ_ONLY_TOKEN_REGEX: new RegExp(`^${READ_ONLY_TOKEN_PATTERN}$`), + READ_ONLY_URL_REGEX: new RegExp(`^/read/${READ_ONLY_TOKEN_PATTERN}$`), + + makeReadAndWriteTokenUrl(token) { + return `/${token}` + }, + + makeReadOnlyTokenUrl(token) { + return `/read/${token}` + }, + + makeTokenUrl(token) { + const tokenType = TokenAccessHandler.getTokenType(token) + if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE) { + return TokenAccessHandler.makeReadAndWriteTokenUrl(token) + } else if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY) { + return TokenAccessHandler.makeReadOnlyTokenUrl(token) + } else { + throw new Error('invalid token type') + } + }, + + getTokenType(token) { + if (!token) { + return null + } + if (token.match(`^${TokenAccessHandler.READ_ONLY_TOKEN_PATTERN}$`)) { + return TokenAccessHandler.TOKEN_TYPES.READ_ONLY + } else if ( + token.match(`^${TokenAccessHandler.READ_AND_WRITE_TOKEN_PATTERN}$`) + ) { + return TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE + } + return null + }, + + isReadOnlyToken(token) { + return ( + TokenAccessHandler.getTokenType(token) === + TokenAccessHandler.TOKEN_TYPES.READ_ONLY + ) + }, + + isReadAndWriteToken(token) { + return ( + TokenAccessHandler.getTokenType(token) === + TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE + ) + }, + + isValidToken(token) { + return TokenAccessHandler.getTokenType(token) != null + }, + + tokenAccessEnabledForProject(project) { + return project.publicAccesLevel === PublicAccessLevels.TOKEN_BASED + }, + + _projectFindOne(query, callback) { + Project.findOne( + query, + { + _id: 1, + tokens: 1, + publicAccesLevel: 1, + owner_ref: 1, + name: 1, + }, + callback + ) + }, + + getProjectByReadOnlyToken(token, callback) { + TokenAccessHandler._projectFindOne({ 'tokens.readOnly': token }, callback) + }, + + _extractNumericPrefix(token) { + return token.match(/^(\d+)\w+/) + }, + + _extractStringSuffix(token) { + return token.match(/^\d+(\w+)/) + }, + + getProjectByReadAndWriteToken(token, callback) { + const numericPrefixMatch = TokenAccessHandler._extractNumericPrefix(token) + if (!numericPrefixMatch) { + return callback(null, null) + } + const numerics = numericPrefixMatch[1] + TokenAccessHandler._projectFindOne( + { + 'tokens.readAndWritePrefix': numerics, + }, + function (err, project) { + if (err != null) { + return callback(err) + } + if (project == null) { + return callback(null, null) + } + try { + if ( + !crypto.timingSafeEqual( + Buffer.from(token), + Buffer.from(project.tokens.readAndWrite) + ) + ) { + logger.err( + { token }, + 'read-and-write token match on numeric section, but not on full token' + ) + return callback(null, null) + } else { + return callback(null, project) + } + } catch (error) { + err = error + logger.err({ token, cryptoErr: err }, 'error comparing tokens') + return callback(null, null) + } + } + ) + }, + + getProjectByToken(tokenType, token, callback) { + if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY) { + TokenAccessHandler.getProjectByReadOnlyToken(token, callback) + } else if (tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE) { + TokenAccessHandler.getProjectByReadAndWriteToken(token, callback) + } else { + return callback(new Error('invalid token type')) + } + }, + + addReadOnlyUserToProject(userId, projectId, callback) { + userId = ObjectId(userId.toString()) + projectId = ObjectId(projectId.toString()) + Analytics.recordEvent(userId, 'project-joined', { mode: 'read-only' }) + Project.updateOne( + { + _id: projectId, + }, + { + $addToSet: { tokenAccessReadOnly_refs: userId }, + }, + callback + ) + }, + + addReadAndWriteUserToProject(userId, projectId, callback) { + userId = ObjectId(userId.toString()) + projectId = ObjectId(projectId.toString()) + Analytics.recordEvent(userId, 'project-joined', { mode: 'read-write' }) + Project.updateOne( + { + _id: projectId, + }, + { + $addToSet: { tokenAccessReadAndWrite_refs: userId }, + }, + callback + ) + }, + + grantSessionTokenAccess(req, projectId, token) { + if (!req.session) { + return + } + if (!req.session.anonTokenAccess) { + req.session.anonTokenAccess = {} + } + req.session.anonTokenAccess[projectId.toString()] = token + }, + + getRequestToken(req, projectId) { + const token = + (req.session && + req.session.anonTokenAccess && + req.session.anonTokenAccess[projectId.toString()]) || + req.headers['x-sl-anonymous-access-token'] + return token + }, + + validateTokenForAnonymousAccess(projectId, token, callback) { + if (!token) { + return callback(null, false, false) + } + const tokenType = TokenAccessHandler.getTokenType(token) + if (!tokenType) { + return callback(new Error('invalid token type')) + } + TokenAccessHandler.getProjectByToken(tokenType, token, (err, project) => { + if (err) { + return callback(err) + } + if ( + !project || + !TokenAccessHandler.tokenAccessEnabledForProject(project) || + project._id.toString() !== projectId.toString() + ) { + return callback(null, false, false) + } + // TODO: think about cleaning up this interface and its usage in AuthorizationManager + return callback( + null, + tokenType === TokenAccessHandler.TOKEN_TYPES.READ_AND_WRITE && + TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED, + tokenType === TokenAccessHandler.TOKEN_TYPES.READ_ONLY + ) + }) + }, + + protectTokens(project, privilegeLevel) { + if (!project || !project.tokens) { + return + } + if (privilegeLevel === PrivilegeLevels.OWNER) { + return + } + if (privilegeLevel !== PrivilegeLevels.READ_AND_WRITE) { + project.tokens.readAndWrite = '' + project.tokens.readAndWritePrefix = '' + } + if (privilegeLevel !== PrivilegeLevels.READ_ONLY) { + project.tokens.readOnly = '' + } + }, + + getV1DocPublishedInfo(token, callback) { + // default to allowing access + if (!Settings.apis.v1 || !Settings.apis.v1.url) { + return callback(null, { allow: true }) + } + V1Api.request( + { url: `/api/v1/sharelatex/docs/${token}/is_published` }, + function (err, response, body) { + if (err != null) { + return callback(err) + } + callback(null, body) + } + ) + }, + + getV1DocInfo(token, v2UserId, callback) { + if (!Settings.apis || !Settings.apis.v1) { + return callback(null, { + exists: true, + exported: false, + }) + } + const v1Url = `/api/v1/sharelatex/docs/${token}/info` + V1Api.request({ url: v1Url }, function (err, response, body) { + if (err != null) { + return callback(err) + } + callback(null, body) + }) + }, +} + +TokenAccessHandler.promises = promisifyAll(TokenAccessHandler, { + without: [ + 'getTokenType', + 'tokenAccessEnabledForProject', + '_extractNumericPrefix', + '_extractStringSuffix', + '_projectFindOne', + 'grantSessionTokenAccess', + 'getRequestToken', + 'protectTokens', + 'validateTokenForAnonymousAccess', + ], +}) + +module.exports = TokenAccessHandler diff --git a/services/web/app/src/Features/TokenGenerator/TokenGenerator.js b/services/web/app/src/Features/TokenGenerator/TokenGenerator.js new file mode 100644 index 0000000000..0e47b65f8f --- /dev/null +++ b/services/web/app/src/Features/TokenGenerator/TokenGenerator.js @@ -0,0 +1,109 @@ +/* eslint-disable + node/handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const crypto = require('crypto') +const V1Api = require('../V1/V1Api') +const Features = require('../../infrastructure/Features') +const Async = require('async') +const { promisify } = require('util') + +// (From Overleaf `random_token.rb`) +// Letters (not numbers! see generate_token) used in tokens. They're all +// consonants, to avoid embarassing words (I can't think of any that use only +// a y), and lower case "l" is omitted, because in many fonts it is +// indistinguishable from an upper case "I" (and sometimes even the number 1). +const TOKEN_LOWERCASE_ALPHA = 'bcdfghjkmnpqrstvwxyz' +const TOKEN_NUMERICS = '123456789' +const TOKEN_ALPHANUMERICS = + TOKEN_LOWERCASE_ALPHA + TOKEN_LOWERCASE_ALPHA.toUpperCase() + TOKEN_NUMERICS + +// This module mirrors the token generation in Overleaf (`random_token.rb`), +// for the purposes of implementing token-based project access, like the +// 'unlisted-projects' feature in Overleaf + +const TokenGenerator = { + _randomString(length, alphabet) { + const result = crypto + .randomBytes(length) + .toJSON() + .data.map(b => alphabet[b % alphabet.length]) + .join('') + return result + }, + + // Generate a 12-char token with only characters from TOKEN_LOWERCASE_ALPHA, + // suitable for use as a read-only token for a project + readOnlyToken() { + return TokenGenerator._randomString(12, TOKEN_LOWERCASE_ALPHA) + }, + + // Generate a longer token, with a numeric prefix, + // suitable for use as a read-and-write token for a project + readAndWriteToken() { + const numerics = TokenGenerator._randomString(10, TOKEN_NUMERICS) + const token = TokenGenerator._randomString(12, TOKEN_LOWERCASE_ALPHA) + const fullToken = `${numerics}${token}` + return { token: fullToken, numericPrefix: numerics } + }, + + generateReferralId() { + return TokenGenerator._randomString(16, TOKEN_ALPHANUMERICS) + }, + + generateUniqueReadOnlyToken(callback) { + if (callback == null) { + callback = function (err, token) {} + } + return Async.retry( + 10, + function (cb) { + const token = TokenGenerator.readOnlyToken() + + if (!Features.hasFeature('overleaf-integration')) { + return cb(null, token) + } + + return V1Api.request( + { + url: `/api/v1/sharelatex/docs/read_token/${token}/exists`, + json: true, + }, + function (err, response, body) { + if (err != null) { + return cb(err) + } + if (response.statusCode !== 200) { + return cb( + new Error( + `non-200 response from v1 read-token-exists api: ${response.statusCode}` + ) + ) + } + if (body.exists === true) { + return cb(new Error(`token already exists in v1: ${token}`)) + } else { + return cb(null, token) + } + } + ) + }, + callback + ) + }, +} + +TokenGenerator.promises = { + generateUniqueReadOnlyToken: promisify( + TokenGenerator.generateUniqueReadOnlyToken + ), +} +module.exports = TokenGenerator diff --git a/services/web/app/src/Features/Uploads/ArchiveErrors.js b/services/web/app/src/Features/Uploads/ArchiveErrors.js new file mode 100644 index 0000000000..d78941ecce --- /dev/null +++ b/services/web/app/src/Features/Uploads/ArchiveErrors.js @@ -0,0 +1,34 @@ +const Errors = require('../Errors/Errors') + +class InvalidZipFileError extends Errors.BackwardCompatibleError { + constructor(options) { + super({ + message: 'invalid_zip_file', + ...options, + }) + } +} + +class EmptyZipFileError extends InvalidZipFileError { + constructor(options) { + super({ + message: 'empty_zip_file', + ...options, + }) + } +} + +class ZipContentsTooLargeError extends InvalidZipFileError { + constructor(options) { + super({ + message: 'zip_contents_too_large', + ...options, + }) + } +} + +module.exports = { + InvalidZipFileError, + EmptyZipFileError, + ZipContentsTooLargeError, +} diff --git a/services/web/app/src/Features/Uploads/ArchiveManager.js b/services/web/app/src/Features/Uploads/ArchiveManager.js new file mode 100644 index 0000000000..6efcbe97fc --- /dev/null +++ b/services/web/app/src/Features/Uploads/ArchiveManager.js @@ -0,0 +1,271 @@ +/* eslint-disable + node/handle-callback-err, + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const logger = require('logger-sharelatex') +const OError = require('@overleaf/o-error') +const metrics = require('@overleaf/metrics') +const fs = require('fs') +const Path = require('path') +const fse = require('fs-extra') +const yauzl = require('yauzl') +const Settings = require('@overleaf/settings') +const { + InvalidZipFileError, + EmptyZipFileError, + ZipContentsTooLargeError, +} = require('./ArchiveErrors') +const _ = require('underscore') +const { promisifyAll } = require('../../util/promises') + +const ONE_MEG = 1024 * 1024 + +const ArchiveManager = { + _isZipTooLarge(source, callback) { + if (callback == null) { + callback = function (err, isTooLarge) {} + } + callback = _.once(callback) + + let totalSizeInBytes = null + return yauzl.open(source, { lazyEntries: true }, function (err, zipfile) { + if (err != null) { + return callback(new InvalidZipFileError().withCause(err)) + } + + if ( + Settings.maxEntitiesPerProject != null && + zipfile.entryCount > Settings.maxEntitiesPerProject + ) { + return callback(null, true) // too many files in zip file + } + + zipfile.on('error', callback) + + // read all the entries + zipfile.readEntry() + zipfile.on('entry', function (entry) { + totalSizeInBytes += entry.uncompressedSize + return zipfile.readEntry() + }) // get the next entry + + // no more entries to read + return zipfile.on('end', function () { + if (totalSizeInBytes == null || isNaN(totalSizeInBytes)) { + logger.warn( + { source, totalSizeInBytes }, + 'error getting bytes of zip' + ) + return callback( + new InvalidZipFileError({ info: { totalSizeInBytes } }) + ) + } + const isTooLarge = totalSizeInBytes > ONE_MEG * 300 + return callback(null, isTooLarge) + }) + }) + }, + + _checkFilePath(entry, destination, callback) { + // transform backslashes to forwardslashes to accommodate badly-behaved zip archives + if (callback == null) { + callback = function (err, destFile) {} + } + const transformedFilename = entry.fileName.replace(/\\/g, '/') + // check if the entry is a directory + const endsWithSlash = /\/$/ + if (endsWithSlash.test(transformedFilename)) { + return callback() // don't give a destfile for directory + } + // check that the file does not use a relative path + for (const dir of Array.from(transformedFilename.split('/'))) { + if (dir === '..') { + return callback(new Error('relative path')) + } + } + // check that the destination file path is normalized + const dest = `${destination}/${transformedFilename}` + if (dest !== Path.normalize(dest)) { + return callback(new Error('unnormalized path')) + } else { + return callback(null, dest) + } + }, + + _writeFileEntry(zipfile, entry, destFile, callback) { + if (callback == null) { + callback = function (err) {} + } + callback = _.once(callback) + + return zipfile.openReadStream(entry, function (err, readStream) { + if (err != null) { + return callback(err) + } + readStream.on('error', callback) + readStream.on('end', callback) + + const errorHandler = function (err) { + // clean up before calling callback + readStream.unpipe() + readStream.destroy() + return callback(err) + } + + return fse.ensureDir(Path.dirname(destFile), function (err) { + if (err != null) { + return errorHandler(err) + } + const writeStream = fs.createWriteStream(destFile) + writeStream.on('error', errorHandler) + return readStream.pipe(writeStream) + }) + }) + }, + + _extractZipFiles(source, destination, callback) { + if (callback == null) { + callback = function (err) {} + } + callback = _.once(callback) + + return yauzl.open(source, { lazyEntries: true }, function (err, zipfile) { + if (err != null) { + return callback(err) + } + zipfile.on('error', callback) + // read all the entries + zipfile.readEntry() + + let entryFileCount = 0 + zipfile.on('entry', function (entry) { + return ArchiveManager._checkFilePath( + entry, + destination, + function (err, destFile) { + if (err != null) { + logger.warn( + { err, source, destination }, + 'skipping bad file path' + ) + zipfile.readEntry() // bad path, just skip to the next file + return + } + if (destFile != null) { + // only write files + return ArchiveManager._writeFileEntry( + zipfile, + entry, + destFile, + function (err) { + if (err != null) { + OError.tag(err, 'error unzipping file entry', { + source, + destFile, + }) + zipfile.close() // bail out, stop reading file entries + return callback(err) + } else { + entryFileCount++ + return zipfile.readEntry() + } + } + ) // continue to the next file + } else { + // if it's a directory, continue + return zipfile.readEntry() + } + } + ) + }) + // no more entries to read + return zipfile.on('end', () => { + if (entryFileCount > 0) { + callback() + } else { + callback(new EmptyZipFileError()) + } + }) + }) + }, + + extractZipArchive(source, destination, _callback) { + if (_callback == null) { + _callback = function (err) {} + } + const callback = function (...args) { + _callback(...Array.from(args || [])) + return (_callback = function () {}) + } + + return ArchiveManager._isZipTooLarge(source, function (err, isTooLarge) { + if (err != null) { + OError.tag(err, 'error checking size of zip file') + return callback(err) + } + + if (isTooLarge) { + return callback(new ZipContentsTooLargeError()) + } + + const timer = new metrics.Timer('unzipDirectory') + logger.log({ source, destination }, 'unzipping file') + + return ArchiveManager._extractZipFiles( + source, + destination, + function (err) { + timer.done() + if (err != null) { + OError.tag(err, 'unzip failed', { + source, + destination, + }) + return callback(err) + } else { + return callback() + } + } + ) + }) + }, + + findTopLevelDirectory(directory, callback) { + if (callback == null) { + callback = function (error, topLevelDir) {} + } + return fs.readdir(directory, function (error, files) { + if (error != null) { + return callback(error) + } + if (files.length === 1) { + const childPath = Path.join(directory, files[0]) + return fs.stat(childPath, function (error, stat) { + if (error != null) { + return callback(error) + } + if (stat.isDirectory()) { + return callback(null, childPath) + } else { + return callback(null, directory) + } + }) + } else { + return callback(null, directory) + } + }) + }, +} + +ArchiveManager.promises = promisifyAll(ArchiveManager) +module.exports = ArchiveManager diff --git a/services/web/app/src/Features/Uploads/FileSystemImportManager.js b/services/web/app/src/Features/Uploads/FileSystemImportManager.js new file mode 100644 index 0000000000..bded16e79b --- /dev/null +++ b/services/web/app/src/Features/Uploads/FileSystemImportManager.js @@ -0,0 +1,256 @@ +const fs = require('fs') +const Path = require('path') +const { callbackify } = require('util') +const EditorController = require('../Editor/EditorController') +const Errors = require('../Errors/Errors') +const FileTypeManager = require('./FileTypeManager') +const SafePath = require('../Project/SafePath') +const logger = require('logger-sharelatex') + +module.exports = { + addEntity: callbackify(addEntity), + importDir: callbackify(importDir), + promises: { + addEntity, + importDir, + }, +} + +async function addDoc(userId, projectId, folderId, name, lines, replace) { + if (replace) { + const doc = await EditorController.promises.upsertDoc( + projectId, + folderId, + name, + lines, + 'upload', + userId + ) + return doc + } else { + const doc = await EditorController.promises.addDoc( + projectId, + folderId, + name, + lines, + 'upload', + userId + ) + return doc + } +} + +async function addFile(userId, projectId, folderId, name, path, replace) { + if (replace) { + const file = await EditorController.promises.upsertFile( + projectId, + folderId, + name, + path, + null, + 'upload', + userId + ) + return file + } else { + const file = await EditorController.promises.addFile( + projectId, + folderId, + name, + path, + null, + 'upload', + userId + ) + return file + } +} + +async function addFolder(userId, projectId, folderId, name, path, replace) { + const newFolder = await EditorController.promises.addFolder( + projectId, + folderId, + name, + 'upload', + userId + ) + await addFolderContents(userId, projectId, newFolder._id, path, replace) + return newFolder +} + +async function addFolderContents( + userId, + projectId, + parentFolderId, + folderPath, + replace +) { + if (!(await _isSafeOnFileSystem(folderPath))) { + logger.log( + { userId, projectId, parentFolderId, folderPath }, + 'add folder contents is from symlink, stopping insert' + ) + throw new Error('path is symlink') + } + const entries = (await fs.promises.readdir(folderPath)) || [] + for (const entry of entries) { + if (await FileTypeManager.promises.shouldIgnore(entry)) { + continue + } + await addEntity( + userId, + projectId, + parentFolderId, + entry, + `${folderPath}/${entry}`, + replace + ) + } +} + +async function addEntity(userId, projectId, folderId, name, fsPath, replace) { + if (!(await _isSafeOnFileSystem(fsPath))) { + logger.log( + { userId, projectId, folderId, fsPath }, + 'add entry is from symlink, stopping insert' + ) + throw new Error('path is symlink') + } + + if (await FileTypeManager.promises.isDirectory(fsPath)) { + const newFolder = await addFolder( + userId, + projectId, + folderId, + name, + fsPath, + replace + ) + return newFolder + } + + // Here, we cheat a little bit and provide the project path relative to the + // folder, not the root of the project. This is because we don't know for sure + // at this point what the final path of the folder will be. The project path + // is still important for importFile() to be able to figure out if the file is + // a binary file or an editable document. + const projectPath = Path.join('/', name) + const importInfo = await importFile(fsPath, projectPath) + switch (importInfo.type) { + case 'file': { + const entity = await addFile( + userId, + projectId, + folderId, + name, + importInfo.fsPath, + replace + ) + if (entity != null) { + entity.type = 'file' + } + return entity + } + case 'doc': { + const entity = await addDoc( + userId, + projectId, + folderId, + name, + importInfo.lines, + replace + ) + if (entity != null) { + entity.type = 'doc' + } + return entity + } + default: { + throw new Error(`unknown import type: ${importInfo.type}`) + } + } +} + +async function _isSafeOnFileSystem(path) { + // Use lstat() to ensure we don't follow symlinks. Symlinks from an + // untrusted source are dangerous. + const stat = await fs.promises.lstat(path) + return stat.isFile() || stat.isDirectory() +} + +async function importFile(fsPath, projectPath) { + const stat = await fs.promises.lstat(fsPath) + if (!stat.isFile()) { + throw new Error(`can't import ${fsPath}: not a regular file`) + } + _validateProjectPath(projectPath) + const filename = Path.basename(projectPath) + + const { binary, encoding } = await FileTypeManager.promises.getType( + filename, + fsPath + ) + if (binary) { + return new FileImport(projectPath, fsPath) + } else { + const content = await fs.promises.readFile(fsPath, encoding) + // Handle Unix, DOS and classic Mac newlines + const lines = content.split(/\r\n|\n|\r/) + return new DocImport(projectPath, lines) + } +} + +async function importDir(dirPath) { + const stat = await fs.promises.lstat(dirPath) + if (!stat.isDirectory()) { + throw new Error(`can't import ${dirPath}: not a directory`) + } + const entries = [] + for await (const filePath of _walkDir(dirPath)) { + const projectPath = Path.join('/', Path.relative(dirPath, filePath)) + const importInfo = await importFile(filePath, projectPath) + entries.push(importInfo) + } + return entries +} + +function _validateProjectPath(path) { + if (!SafePath.isAllowedLength(path) || !SafePath.isCleanPath(path)) { + throw new Errors.InvalidNameError(`Invalid path: ${path}`) + } +} + +async function* _walkDir(dirPath) { + const entries = await fs.promises.readdir(dirPath) + for (const entry of entries) { + const entryPath = Path.join(dirPath, entry) + if (await FileTypeManager.promises.shouldIgnore(entryPath)) { + continue + } + + // Use lstat() to ensure we don't follow symlinks. Symlinks from an + // untrusted source are dangerous. + const stat = await fs.promises.lstat(entryPath) + if (stat.isFile()) { + yield entryPath + } else if (stat.isDirectory()) { + yield* _walkDir(entryPath) + } + } +} + +class FileImport { + constructor(projectPath, fsPath) { + this.type = 'file' + this.projectPath = projectPath + this.fsPath = fsPath + } +} + +class DocImport { + constructor(projectPath, lines) { + this.type = 'doc' + this.projectPath = projectPath + this.lines = lines + } +} diff --git a/services/web/app/src/Features/Uploads/FileTypeManager.js b/services/web/app/src/Features/Uploads/FileTypeManager.js new file mode 100644 index 0000000000..f3086b8882 --- /dev/null +++ b/services/web/app/src/Features/Uploads/FileTypeManager.js @@ -0,0 +1,139 @@ +const fs = require('fs') +const Path = require('path') +const isUtf8 = require('utf-8-validate') +const { promisifyAll } = require('../../util/promises') +const Settings = require('@overleaf/settings') + +const FileTypeManager = { + TEXT_EXTENSIONS: Settings.textExtensions.map(ext => `.${ext}`), + + IGNORE_EXTENSIONS: [ + '.dvi', + '.aux', + '.log', + '.toc', + '.out', + '.pdfsync', + // Index and glossary files + '.nlo', + '.ind', + '.glo', + '.gls', + '.glg', + // Bibtex + '.bbl', + '.blg', + // Misc/bad + '.doc', + '.docx', + '.gz', + ], + + IGNORE_FILENAMES: ['__MACOSX', '.git', '.gitignore'], + + MAX_TEXT_FILE_SIZE: 1 * 1024 * 1024, // 1 MB + + isDirectory(path, callback) { + fs.stat(path, (error, stats) => { + if (error != null) { + return callback(error) + } + callback(null, stats.isDirectory()) + }) + }, + + // returns charset as understood by fs.readFile, + getType(name, fsPath, callback) { + if (!_isTextFilename(name)) { + return callback(null, { binary: true }) + } + + fs.stat(fsPath, (err, stat) => { + if (err != null) { + return callback(err) + } + if (stat.size > FileTypeManager.MAX_TEXT_FILE_SIZE) { + return callback(null, { binary: true }) // Treat large text file as binary + } + + fs.readFile(fsPath, (err, bytes) => { + if (err != null) { + return callback(err) + } + const encoding = _detectEncoding(bytes) + const text = bytes.toString(encoding) + // For compatibility with the history service, only accept valid utf8 with no + // nulls or non-BMP characters as text, eveything else is binary. + if (text.includes('\x00')) { + return callback(null, { binary: true }) + } + if (/[\uD800-\uDFFF]/.test(text)) { + // non-BMP characters (high and low surrogate characters) + return callback(null, { binary: true }) + } + callback(null, { binary: false, encoding }) + }) + }) + }, + + getStrictTypeFromContent(name, contents) { + const isText = _isTextFilename(name) + + if (!isText) { + return false + } + if ( + Buffer.byteLength(contents, 'utf8') > FileTypeManager.MAX_TEXT_FILE_SIZE + ) { + return false + } + if (contents.indexOf('\x00') !== -1) { + return false + } + if (/[\uD800-\uDFFF]/.test(contents)) { + // non-BMP characters (high and low surrogate characters) + return false + } + return true + }, + + shouldIgnore(path, callback) { + const name = Path.basename(path) + const extension = Path.extname(name).toLowerCase() + let ignore = false + if (name.startsWith('.') && name !== '.latexmkrc') { + ignore = true + } + if (FileTypeManager.IGNORE_EXTENSIONS.includes(extension)) { + ignore = true + } + if (FileTypeManager.IGNORE_FILENAMES.includes(name)) { + ignore = true + } + callback(null, ignore) + }, +} + +function _isTextFilename(filename) { + const extension = Path.extname(filename).toLowerCase() + return ( + FileTypeManager.TEXT_EXTENSIONS.includes(extension) || + filename === 'latexmkrc' + ) +} + +function _detectEncoding(bytes) { + if (isUtf8(bytes)) { + return 'utf-8' + } + // check for little-endian unicode bom (nodejs does not support big-endian) + if (bytes[0] === 0xff && bytes[1] === 0xfe) { + return 'utf-16le' + } + return 'latin1' +} + +module.exports = FileTypeManager +module.exports.promises = promisifyAll(FileTypeManager, { + without: ['getStrictTypeFromContent'], +}) diff --git a/services/web/app/src/Features/Uploads/ProjectUploadController.js b/services/web/app/src/Features/Uploads/ProjectUploadController.js new file mode 100644 index 0000000000..69de196eb9 --- /dev/null +++ b/services/web/app/src/Features/Uploads/ProjectUploadController.js @@ -0,0 +1,148 @@ +/* eslint-disable + camelcase, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProjectUploadController +const logger = require('logger-sharelatex') +const metrics = require('@overleaf/metrics') +const fs = require('fs') +const Path = require('path') +const FileSystemImportManager = require('./FileSystemImportManager') +const ProjectUploadManager = require('./ProjectUploadManager') +const SessionManager = require('../Authentication/SessionManager') +const Settings = require('@overleaf/settings') +const { InvalidZipFileError } = require('./ArchiveErrors') +const multer = require('multer') + +const upload = multer({ + dest: Settings.path.uploadFolder, + limits: { + fileSize: Settings.maxUploadSize, + }, +}) + +module.exports = ProjectUploadController = { + uploadProject(req, res, next) { + const timer = new metrics.Timer('project-upload') + const user_id = SessionManager.getLoggedInUserId(req.session) + const { originalname, path } = req.file + const name = Path.basename(originalname, '.zip') + return ProjectUploadManager.createProjectFromZipArchive( + user_id, + name, + path, + function (error, project) { + fs.unlink(path, function () {}) + timer.done() + if (error != null) { + logger.error( + { err: error, filePath: path, fileName: name }, + 'error uploading project' + ) + if (error instanceof InvalidZipFileError) { + return res.status(422).json({ + success: false, + error: req.i18n.translate(error.message), + }) + } else { + return res.status(500).json({ + success: false, + error: req.i18n.translate('upload_failed'), + }) + } + } else { + return res.send({ success: true, project_id: project._id }) + } + } + ) + }, + + uploadFile(req, res, next) { + const timer = new metrics.Timer('file-upload') + const name = req.file != null ? req.file.originalname : undefined + const path = req.file != null ? req.file.path : undefined + const project_id = req.params.Project_id + const { folder_id } = req.query + if (name == null || name.length === 0 || name.length > 150) { + logger.err( + { projectId: project_id, fileName: name }, + 'bad name when trying to upload file' + ) + return res.status(422).send({ + success: false, + error: 'invalid_filename', + }) + } + const user_id = SessionManager.getLoggedInUserId(req.session) + + return FileSystemImportManager.addEntity( + user_id, + project_id, + folder_id, + name, + path, + true, + function (error, entity) { + fs.unlink(path, function () {}) + timer.done() + if (error != null) { + logger.error( + { + err: error, + projectId: project_id, + filePath: path, + fileName: name, + folderId: folder_id, + }, + 'error uploading file' + ) + if (error.name === 'InvalidNameError') { + return res.status(422).send({ + success: false, + error: 'invalid_filename', + }) + } else if (error.message === 'project_has_too_many_files') { + return res.status(422).send({ + success: false, + error: 'project_has_too_many_files', + }) + } else { + return res.status(422).send({ success: false }) + } + } else { + return res.send({ + success: true, + entity_id: entity != null ? entity._id : undefined, + entity_type: entity != null ? entity.type : undefined, + }) + } + } + ) + }, + + multerMiddleware(req, res, next) { + if (upload == null) { + return res + .status(500) + .json({ success: false, error: req.i18n.translate('upload_failed') }) + } + return upload.single('qqfile')(req, res, function (err) { + if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') { + return res + .status(422) + .json({ success: false, error: req.i18n.translate('file_too_large') }) + } + + return next(err) + }) + }, +} diff --git a/services/web/app/src/Features/Uploads/ProjectUploadManager.js b/services/web/app/src/Features/Uploads/ProjectUploadManager.js new file mode 100644 index 0000000000..530bb6352b --- /dev/null +++ b/services/web/app/src/Features/Uploads/ProjectUploadManager.js @@ -0,0 +1,210 @@ +const Path = require('path') +const fs = require('fs-extra') +const { callbackify } = require('util') +const ArchiveManager = require('./ArchiveManager') +const { Doc } = require('../../models/Doc') +const DocstoreManager = require('../Docstore/DocstoreManager') +const DocumentHelper = require('../Documents/DocumentHelper') +const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') +const FileStoreHandler = require('../FileStore/FileStoreHandler') +const FileSystemImportManager = require('./FileSystemImportManager') +const ProjectCreationHandler = require('../Project/ProjectCreationHandler') +const ProjectEntityMongoUpdateHandler = require('../Project/ProjectEntityMongoUpdateHandler') +const ProjectRootDocManager = require('../Project/ProjectRootDocManager') +const ProjectDetailsHandler = require('../Project/ProjectDetailsHandler') +const ProjectDeleter = require('../Project/ProjectDeleter') +const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher') +const logger = require('logger-sharelatex') + +module.exports = { + createProjectFromZipArchive: callbackify(createProjectFromZipArchive), + createProjectFromZipArchiveWithName: callbackify( + createProjectFromZipArchiveWithName + ), + promises: { + createProjectFromZipArchive, + createProjectFromZipArchiveWithName, + }, +} + +async function createProjectFromZipArchive(ownerId, defaultName, zipPath) { + const contentsPath = await _extractZip(zipPath) + const { + path, + content, + } = await ProjectRootDocManager.promises.findRootDocFileFromDirectory( + contentsPath + ) + + const projectName = + DocumentHelper.getTitleFromTexContent(content || '') || defaultName + const uniqueName = await _generateUniqueName(ownerId, projectName) + const project = await ProjectCreationHandler.promises.createBlankProject( + ownerId, + uniqueName + ) + try { + await _initializeProjectWithZipContents(ownerId, project, contentsPath) + + if (path) { + await ProjectRootDocManager.promises.setRootDocFromName(project._id, path) + } + } catch (err) { + // no need to wait for the cleanup here + ProjectDeleter.promises + .deleteProject(project._id) + .catch(err => + logger.error( + { err, projectId: project._id }, + 'there was an error cleaning up project after importing a zip failed' + ) + ) + throw err + } + await fs.remove(contentsPath) + return project +} + +async function createProjectFromZipArchiveWithName( + ownerId, + proposedName, + zipPath, + attributes = {} +) { + const contentsPath = await _extractZip(zipPath) + const uniqueName = await _generateUniqueName(ownerId, proposedName) + const project = await ProjectCreationHandler.promises.createBlankProject( + ownerId, + uniqueName, + attributes + ) + + try { + await _initializeProjectWithZipContents(ownerId, project, contentsPath) + await ProjectRootDocManager.promises.setRootDocAutomatically(project._id) + } catch (err) { + // no need to wait for the cleanup here + ProjectDeleter.promises + .deleteProject(project._id) + .catch(err => + logger.error( + { err, projectId: project._id }, + 'there was an error cleaning up project after importing a zip failed' + ) + ) + throw err + } + await fs.remove(contentsPath) + return project +} + +async function _extractZip(zipPath) { + const destination = Path.join( + Path.dirname(zipPath), + `${Path.basename(zipPath, '.zip')}-${Date.now()}` + ) + await ArchiveManager.promises.extractZipArchive(zipPath, destination) + return destination +} + +async function _generateUniqueName(ownerId, originalName) { + const fixedName = ProjectDetailsHandler.fixProjectName(originalName) + const uniqueName = await ProjectDetailsHandler.promises.generateUniqueName( + ownerId, + fixedName + ) + return uniqueName +} + +async function _initializeProjectWithZipContents( + ownerId, + project, + contentsPath +) { + const topLevelDir = await ArchiveManager.promises.findTopLevelDirectory( + contentsPath + ) + const importEntries = await FileSystemImportManager.promises.importDir( + topLevelDir + ) + const { fileEntries, docEntries } = await _createEntriesFromImports( + project._id, + importEntries + ) + const projectVersion = await ProjectEntityMongoUpdateHandler.promises.createNewFolderStructure( + project._id, + docEntries, + fileEntries + ) + await _notifyDocumentUpdater(project, ownerId, { + newFiles: fileEntries, + newDocs: docEntries, + newProject: { version: projectVersion }, + }) + await TpdsProjectFlusher.promises.flushProjectToTpds(project._id) +} + +async function _createEntriesFromImports(projectId, importEntries) { + const fileEntries = [] + const docEntries = [] + for (const importEntry of importEntries) { + switch (importEntry.type) { + case 'doc': { + const docEntry = await _createDoc( + projectId, + importEntry.projectPath, + importEntry.lines + ) + docEntries.push(docEntry) + break + } + case 'file': { + const fileEntry = await _createFile( + projectId, + importEntry.projectPath, + importEntry.fsPath + ) + fileEntries.push(fileEntry) + break + } + default: { + throw new Error(`Invalid import type: ${importEntry.type}`) + } + } + } + return { fileEntries, docEntries } +} + +async function _createDoc(projectId, projectPath, docLines) { + const docName = Path.basename(projectPath) + const doc = new Doc({ name: docName }) + await DocstoreManager.promises.updateDoc( + projectId.toString(), + doc._id.toString(), + docLines, + 0, + {} + ) + return { doc, path: projectPath, docLines: docLines.join('\n') } +} + +async function _createFile(projectId, projectPath, fsPath) { + const fileName = Path.basename(projectPath) + const { fileRef, url } = await FileStoreHandler.promises.uploadFileFromDisk( + projectId, + { name: fileName }, + fsPath + ) + return { file: fileRef, path: projectPath, url } +} + +async function _notifyDocumentUpdater(project, userId, changes) { + const projectHistoryId = + project.overleaf && project.overleaf.history && project.overleaf.history.id + await DocumentUpdaterHandler.promises.updateProjectStructure( + project._id, + projectHistoryId, + userId, + changes + ) +} diff --git a/services/web/app/src/Features/Uploads/UploadsRouter.js b/services/web/app/src/Features/Uploads/UploadsRouter.js new file mode 100644 index 0000000000..575114bba9 --- /dev/null +++ b/services/web/app/src/Features/Uploads/UploadsRouter.js @@ -0,0 +1,47 @@ +const AuthorizationMiddleware = require('../Authorization/AuthorizationMiddleware') +const AuthenticationController = require('../Authentication/AuthenticationController') +const ProjectUploadController = require('./ProjectUploadController') +const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') +const Settings = require('@overleaf/settings') + +module.exports = { + apply(webRouter, apiRouter) { + webRouter.post( + '/project/new/upload', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'project-upload', + maxRequests: 20, + timeInterval: 60, + }), + ProjectUploadController.multerMiddleware, + ProjectUploadController.uploadProject + ) + + const fileUploadEndpoint = '/Project/:Project_id/upload' + const fileUploadRateLimit = RateLimiterMiddleware.rateLimit({ + endpointName: 'file-upload', + params: ['Project_id'], + maxRequests: 200, + timeInterval: 60 * 15, + }) + if (Settings.allowAnonymousReadAndWriteSharing) { + webRouter.post( + fileUploadEndpoint, + fileUploadRateLimit, + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + ProjectUploadController.multerMiddleware, + ProjectUploadController.uploadFile + ) + } else { + webRouter.post( + fileUploadEndpoint, + fileUploadRateLimit, + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + ProjectUploadController.multerMiddleware, + ProjectUploadController.uploadFile + ) + } + }, +} diff --git a/services/web/app/src/Features/User/SAMLIdentityManager.js b/services/web/app/src/Features/User/SAMLIdentityManager.js new file mode 100644 index 0000000000..58108553d1 --- /dev/null +++ b/services/web/app/src/Features/User/SAMLIdentityManager.js @@ -0,0 +1,399 @@ +const { ObjectId } = require('mongodb') +const EmailHandler = require('../Email/EmailHandler') +const Errors = require('../Errors/Errors') +const InstitutionsAPI = require('../Institutions/InstitutionsAPI') +const NotificationsBuilder = require('../Notifications/NotificationsBuilder') +const OError = require('@overleaf/o-error') +const SubscriptionLocator = require('../Subscription/SubscriptionLocator') +const UserAuditLogHandler = require('../User/UserAuditLogHandler') +const UserGetter = require('../User/UserGetter') +const UserUpdater = require('../User/UserUpdater') +const logger = require('logger-sharelatex') +const { User } = require('../../models/User') + +async function _addAuditLogEntry( + link, + userId, + auditLog, + institutionEmail, + providerId, + providerName +) { + const operation = link ? 'link-institution-sso' : 'unlink-institution-sso' + await UserAuditLogHandler.promises.addEntry( + userId, + operation, + auditLog.initiatorId, + auditLog.ipAddress, + { + institutionEmail, + providerId, + providerName, + } + ) +} + +async function _ensureCanAddIdentifier(userId, institutionEmail, providerId) { + const userWithProvider = await UserGetter.promises.getUser( + { _id: ObjectId(userId), 'samlIdentifiers.providerId': providerId }, + { _id: 1 } + ) + + if (userWithProvider) { + throw new Errors.SAMLAlreadyLinkedError() + } + + const userWithEmail = await UserGetter.promises.getUserByAnyEmail( + institutionEmail + ) + + if (!userWithEmail) { + // email doesn't exist; all good + return + } + + const emailBelongToUser = userWithEmail._id.toString() === userId.toString() + const existingEmailData = userWithEmail.emails.find( + emailData => emailData.email === institutionEmail + ) + + if (!emailBelongToUser && existingEmailData.samlProviderId) { + // email exists and institution link. + // Return back to requesting page with error + throw new Errors.SAMLIdentityExistsError() + } + + if (!emailBelongToUser) { + // email exists but not linked, so redirect to linking page + // which will tell this user to log out to link + throw new Errors.EmailExistsError() + } + + // email belongs to user. Make sure it's already affiliated with the provider + const fullEmails = await UserGetter.promises.getUserFullEmails( + userWithEmail._id + ) + const existingFullEmailData = fullEmails.find( + emailData => emailData.email === institutionEmail + ) + + if (!existingFullEmailData.affiliation) { + throw new Errors.SAMLEmailNotAffiliatedError() + } + + if ( + existingFullEmailData.affiliation.institution.id.toString() !== providerId + ) { + throw new Errors.SAMLEmailAffiliatedWithAnotherInstitutionError() + } +} + +async function _addIdentifier( + userId, + externalUserId, + providerId, + hasEntitlement, + institutionEmail, + providerName, + auditLog +) { + providerId = providerId.toString() + + await _ensureCanAddIdentifier(userId, institutionEmail, providerId) + + await _addAuditLogEntry( + true, + userId, + auditLog, + institutionEmail, + providerId, + providerName + ) + + hasEntitlement = !!hasEntitlement + const query = { + _id: userId, + 'samlIdentifiers.providerId': { + $ne: providerId, + }, + } + const update = { + $push: { + samlIdentifiers: { + hasEntitlement, + externalUserId, + providerId, + }, + }, + } + + try { + // update v2 user record + const updatedUser = await User.findOneAndUpdate(query, update, { + new: true, + }).exec() + if (!updatedUser) { + throw new OError('No update while linking user') + } + return updatedUser + } catch (err) { + if (err.code === 11000) { + throw new Errors.SAMLIdentityExistsError() + } else { + throw OError.tag(err) + } + } +} + +async function _addInstitutionEmail(userId, email, providerId, auditLog) { + const user = await UserGetter.promises.getUser(userId) + const query = { + _id: userId, + 'emails.email': email, + } + const update = { + $set: { + 'emails.$.samlProviderId': providerId.toString(), + }, + } + if (user == null) { + throw new Errors.NotFoundError('user not found') + } + const emailAlreadyAssociated = user.emails.find(e => e.email === email) + if (emailAlreadyAssociated && emailAlreadyAssociated.confirmedAt) { + await UserUpdater.promises.updateUser(query, update) + } else if (emailAlreadyAssociated) { + await UserUpdater.promises.updateUser(query, update) + } else { + await UserUpdater.promises.addEmailAddress( + user._id, + email, + { university: { id: providerId }, rejectIfBlocklisted: true }, + auditLog + ) + await UserUpdater.promises.updateUser(query, update) + } +} + +async function _sendLinkedEmail(userId, providerName, institutionEmail) { + const user = await UserGetter.promises.getUser(userId, { email: 1 }) + const emailOptions = { + to: user.email, + actionDescribed: `an Institutional SSO account at ${providerName} was linked to your account ${user.email}`, + action: 'institutional SSO account linked', + message: [ + `Linked:
${institutionEmail}
`, + ], + } + EmailHandler.sendEmail('securityAlert', emailOptions, error => { + if (error) { + logger.warn({ err: error }) + } + }) +} + +function _sendUnlinkedEmail(primaryEmail, providerName, institutionEmail) { + const emailOptions = { + to: primaryEmail, + actionDescribed: `an Institutional SSO account at ${providerName} was unlinked from your account ${primaryEmail}`, + action: 'institutional SSO account no longer linked', + message: [ + `No longer linked:
${institutionEmail}
`, + ], + } + EmailHandler.sendEmail('securityAlert', emailOptions, error => { + if (error) { + logger.warn({ err: error }) + } + }) +} + +async function getUser(providerId, externalUserId) { + if (!providerId || !externalUserId) { + throw new Error( + `invalid arguments: providerId: ${providerId}, externalUserId: ${externalUserId}` + ) + } + const user = await User.findOne({ + 'samlIdentifiers.externalUserId': externalUserId.toString(), + 'samlIdentifiers.providerId': providerId.toString(), + }).exec() + + return user +} + +async function redundantSubscription(userId, providerId, providerName) { + const subscription = await SubscriptionLocator.promises.getUserIndividualSubscription( + userId + ) + + if (subscription) { + await NotificationsBuilder.promises + .redundantPersonalSubscription( + { + institutionId: providerId, + institutionName: providerName, + }, + { _id: userId } + ) + .create() + } +} + +async function linkAccounts( + userId, + externalUserId, + institutionEmail, + providerId, + providerName, + hasEntitlement, + auditLog +) { + await _addIdentifier( + userId, + externalUserId, + providerId, + hasEntitlement, + institutionEmail, + providerName, + auditLog + ) + try { + await _addInstitutionEmail(userId, institutionEmail, providerId, auditLog) + } catch (error) { + await _removeIdentifier(userId, providerId) + throw error + } + await UserUpdater.promises.confirmEmail(userId, institutionEmail) // will set confirmedAt if not set, and will always update reconfirmedAt + await _sendLinkedEmail(userId, providerName, institutionEmail) + // update v1 affiliations record + if (hasEntitlement) { + await InstitutionsAPI.promises.addEntitlement(userId, institutionEmail) + try { + await redundantSubscription(userId, providerId, providerName) + } catch (error) { + logger.err({ err: error }, 'error checking redundant subscription') + } + } else { + await InstitutionsAPI.promises.removeEntitlement(userId, institutionEmail) + } +} + +async function unlinkAccounts( + userId, + institutionEmail, + primaryEmail, + providerId, + providerName, + auditLog +) { + providerId = providerId.toString() + + await _addAuditLogEntry( + false, + userId, + auditLog, + institutionEmail, + providerId, + providerName + ) + // update v2 user + await _removeIdentifier(userId, providerId) + // update v1 affiliations record + await InstitutionsAPI.promises.removeEntitlement(userId, institutionEmail) + // send email + _sendUnlinkedEmail(primaryEmail, providerName, institutionEmail) +} + +async function _removeIdentifier(userId, providerId) { + providerId = providerId.toString() + + const query = { + _id: userId, + } + const update = { + $pull: { + samlIdentifiers: { + providerId, + }, + }, + } + await User.updateOne(query, update).exec() +} + +async function updateEntitlement( + userId, + institutionEmail, + providerId, + hasEntitlement +) { + providerId = providerId.toString() + hasEntitlement = !!hasEntitlement + const query = { + _id: userId, + 'samlIdentifiers.providerId': providerId.toString(), + } + const update = { + $set: { + 'samlIdentifiers.$.hasEntitlement': hasEntitlement, + }, + } + // update v2 user + await User.updateOne(query, update).exec() + // update v1 affiliations record + if (hasEntitlement) { + await InstitutionsAPI.promises.addEntitlement(userId, institutionEmail) + } else { + await InstitutionsAPI.promises.removeEntitlement(userId, institutionEmail) + } +} + +function entitlementAttributeMatches(entitlementAttribute, entitlementMatcher) { + if (Array.isArray(entitlementAttribute)) { + entitlementAttribute = entitlementAttribute.join(' ') + } + if ( + typeof entitlementAttribute !== 'string' || + typeof entitlementMatcher !== 'string' + ) { + return false + } + try { + const entitlementRegExp = new RegExp(entitlementMatcher) + return !!entitlementAttribute.match(entitlementRegExp) + } catch (err) { + logger.error({ err }, 'Invalid SAML entitlement matcher') + // this is likely caused by an invalid regex in the matcher string + // log the error but do not bubble so that user can still sign in + // even if they don't have the entitlement + return false + } +} + +function userHasEntitlement(user, providerId) { + providerId = providerId.toString() + if (!user || !Array.isArray(user.samlIdentifiers)) { + return false + } + for (const samlIdentifier of user.samlIdentifiers) { + if (providerId && samlIdentifier.providerId !== providerId) { + continue + } + if (samlIdentifier.hasEntitlement) { + return true + } + } + return false +} + +const SAMLIdentityManager = { + entitlementAttributeMatches, + getUser, + linkAccounts, + redundantSubscription, + unlinkAccounts, + updateEntitlement, + userHasEntitlement, +} + +module.exports = SAMLIdentityManager diff --git a/services/web/app/src/Features/User/ThirdPartyIdentityManager.js b/services/web/app/src/Features/User/ThirdPartyIdentityManager.js new file mode 100644 index 0000000000..c1054d8741 --- /dev/null +++ b/services/web/app/src/Features/User/ThirdPartyIdentityManager.js @@ -0,0 +1,237 @@ +const APP_ROOT = '../../../../app/src' +const UserAuditLogHandler = require(`${APP_ROOT}/Features/User/UserAuditLogHandler`) +const EmailHandler = require(`${APP_ROOT}/Features/Email/EmailHandler`) +const EmailOptionsHelper = require(`${APP_ROOT}/Features/Email/EmailOptionsHelper`) +const Errors = require('../Errors/Errors') +const _ = require('lodash') +const logger = require('logger-sharelatex') +const OError = require('@overleaf/o-error') +const settings = require('@overleaf/settings') +const { User } = require(`${APP_ROOT}/models/User`) +const { promisifyAll } = require(`${APP_ROOT}/util/promises`) + +const oauthProviders = settings.oauthProviders || {} + +function getUser(providerId, externalUserId, callback) { + if (providerId == null || externalUserId == null) { + return callback( + new OError('invalid SSO arguments', { + externalUserId, + providerId, + }) + ) + } + const query = _getUserQuery(providerId, externalUserId) + User.findOne(query, function (err, user) { + if (err != null) { + return callback(err) + } + if (!user) { + return callback(new Errors.ThirdPartyUserNotFoundError()) + } + callback(null, user) + }) +} + +function login(providerId, externalUserId, externalData, callback) { + ThirdPartyIdentityManager.getUser( + providerId, + externalUserId, + function (err, user) { + if (err != null) { + return callback(err) + } + if (!externalData) { + return callback(null, user) + } + const query = _getUserQuery(providerId, externalUserId) + const update = _thirdPartyIdentifierUpdate( + user, + providerId, + externalUserId, + externalData + ) + User.findOneAndUpdate(query, update, { new: true }, callback) + } + ) +} + +function link( + userId, + providerId, + externalUserId, + externalData, + auditLog, + callback, + retry +) { + const accountLinked = true + if (!oauthProviders[providerId]) { + return callback(new Error('Not a valid provider')) + } + + UserAuditLogHandler.addEntry( + userId, + 'link-sso', + auditLog.initiatorId, + auditLog.ipAddress, + { + providerId, + }, + error => { + if (error) { + return callback(error) + } + const query = { + _id: userId, + 'thirdPartyIdentifiers.providerId': { + $ne: providerId, + }, + } + const update = { + $push: { + thirdPartyIdentifiers: { + externalUserId, + externalData, + providerId, + }, + }, + } + // add new tpi only if an entry for the provider does not exist + // projection includes thirdPartyIdentifiers for tests + User.findOneAndUpdate(query, update, { new: 1 }, (err, res) => { + if (err && err.code === 11000) { + callback(new Errors.ThirdPartyIdentityExistsError()) + } else if (err != null) { + callback(err) + } else if (res) { + _sendSecurityAlert(accountLinked, providerId, res, userId) + callback(null, res) + } else if (retry) { + // if already retried then throw error + callback(new Error('update failed')) + } else { + // attempt to clear existing entry then retry + ThirdPartyIdentityManager.unlink( + userId, + providerId, + auditLog, + function (err) { + if (err != null) { + return callback(err) + } + ThirdPartyIdentityManager.link( + userId, + providerId, + externalUserId, + externalData, + auditLog, + callback, + true + ) + } + ) + } + }) + } + ) +} + +function unlink(userId, providerId, auditLog, callback) { + const accountLinked = false + if (!oauthProviders[providerId]) { + return callback(new Error('Not a valid provider')) + } + UserAuditLogHandler.addEntry( + userId, + 'unlink-sso', + auditLog.initiatorId, + auditLog.ipAddress, + { + providerId, + }, + error => { + if (error) { + return callback(error) + } + const query = { + _id: userId, + } + const update = { + $pull: { + thirdPartyIdentifiers: { + providerId, + }, + }, + } + // projection includes thirdPartyIdentifiers for tests + User.findOneAndUpdate(query, update, { new: 1 }, (err, res) => { + if (err != null) { + callback(err) + } else if (!res) { + callback(new Error('update failed')) + } else { + // no need to wait, errors are logged and not passed back + _sendSecurityAlert(accountLinked, providerId, res, userId) + callback(null, res) + } + }) + } + ) +} + +function _getUserQuery(providerId, externalUserId) { + externalUserId = externalUserId.toString() + providerId = providerId.toString() + const query = { + 'thirdPartyIdentifiers.externalUserId': externalUserId, + 'thirdPartyIdentifiers.providerId': providerId, + } + return query +} + +function _sendSecurityAlert(accountLinked, providerId, user, userId) { + const providerName = oauthProviders[providerId].name + const emailOptions = EmailOptionsHelper.linkOrUnlink( + accountLinked, + providerName, + user.email + ) + EmailHandler.sendEmail('securityAlert', emailOptions, error => { + if (error) { + logger.error( + { err: error, userId }, + `could not send security alert email when ${emailOptions.action}` + ) + } + }) +} + +function _thirdPartyIdentifierUpdate( + user, + providerId, + externalUserId, + externalData +) { + providerId = providerId.toString() + // get third party identifier object from array + const thirdPartyIdentifier = user.thirdPartyIdentifiers.find( + tpi => + tpi.externalUserId === externalUserId && tpi.providerId === providerId + ) + // do recursive merge of new data over existing data + _.merge(thirdPartyIdentifier.externalData, externalData) + const update = { 'thirdPartyIdentifiers.$': thirdPartyIdentifier } + return update +} + +const ThirdPartyIdentityManager = { + getUser, + login, + link, + unlink, +} + +ThirdPartyIdentityManager.promises = promisifyAll(ThirdPartyIdentityManager) + +module.exports = ThirdPartyIdentityManager diff --git a/services/web/app/src/Features/User/UserAuditLogHandler.js b/services/web/app/src/Features/User/UserAuditLogHandler.js new file mode 100644 index 0000000000..5ba1e3b8dd --- /dev/null +++ b/services/web/app/src/Features/User/UserAuditLogHandler.js @@ -0,0 +1,67 @@ +const OError = require('@overleaf/o-error') +const { User } = require('../../models/User') +const { callbackify } = require('util') + +const MAX_AUDIT_LOG_ENTRIES = 200 + +function _canHaveNoInitiatorId(operation, info) { + if (operation === 'reset-password') return true + if (operation === 'unlink-sso' && info.providerId === 'collabratec') + return true +} + +/** + * Add an audit log entry + * + * The entry should include at least the following fields: + * + * - userId: the user on behalf of whom the operation was performed + * - operation: a string identifying the type of operation + * - initiatorId: who performed the operation + * - ipAddress: the IP address of the initiator + * - info: an object detailing what happened + */ +async function addEntry(userId, operation, initiatorId, ipAddress, info = {}) { + if (!operation || !ipAddress) + throw new OError('missing required audit log data', { + operation, + initiatorId, + ipAddress, + }) + + if (!initiatorId && !_canHaveNoInitiatorId(operation, info)) { + throw new OError('missing initiatorId for audit log', { + operation, + ipAddress, + }) + } + + const timestamp = new Date() + const entry = { + operation, + initiatorId, + info, + ipAddress, + timestamp, + } + const result = await User.updateOne( + { _id: userId }, + { + $push: { + auditLog: { $each: [entry], $slice: -MAX_AUDIT_LOG_ENTRIES }, + }, + } + ).exec() + if (result.nModified === 0) { + throw new OError('user not found', { userId }) + } +} + +const UserAuditLogHandler = { + addEntry: callbackify(addEntry), + promises: { + addEntry, + }, +} + +module.exports = UserAuditLogHandler diff --git a/services/web/app/src/Features/User/UserController.js b/services/web/app/src/Features/User/UserController.js new file mode 100644 index 0000000000..4be2977624 --- /dev/null +++ b/services/web/app/src/Features/User/UserController.js @@ -0,0 +1,487 @@ +const UserHandler = require('./UserHandler') +const UserDeleter = require('./UserDeleter') +const UserGetter = require('./UserGetter') +const { User } = require('../../models/User') +const NewsletterManager = require('../Newsletter/NewsletterManager') +const UserRegistrationHandler = require('./UserRegistrationHandler') +const logger = require('logger-sharelatex') +const metrics = require('@overleaf/metrics') +const AuthenticationManager = require('../Authentication/AuthenticationManager') +const SessionManager = require('../Authentication/SessionManager') +const Features = require('../../infrastructure/Features') +const UserAuditLogHandler = require('./UserAuditLogHandler') +const UserSessionsManager = require('./UserSessionsManager') +const UserUpdater = require('./UserUpdater') +const Errors = require('../Errors/Errors') +const HttpErrorHandler = require('../Errors/HttpErrorHandler') +const OError = require('@overleaf/o-error') +const EmailHandler = require('../Email/EmailHandler') +const UrlHelper = require('../Helpers/UrlHelper') +const { promisify } = require('util') +const { expressify } = require('../../util/promises') + +async function _sendSecurityAlertClearedSessions(user) { + const emailOptions = { + to: user.email, + actionDescribed: `active sessions were cleared on your account ${user.email}`, + action: 'active sessions cleared', + } + try { + await EmailHandler.promises.sendEmail('securityAlert', emailOptions) + } catch (error) { + // log error when sending security alert email but do not pass back + logger.error( + { error, userId: user._id }, + 'could not send security alert email when sessions cleared' + ) + } +} + +function _sendSecurityAlertPasswordChanged(user) { + const emailOptions = { + to: user.email, + actionDescribed: `your password has been changed on your account ${user.email}`, + action: 'password changed', + } + EmailHandler.sendEmail('securityAlert', emailOptions, error => { + if (error) { + // log error when sending security alert email but do not pass back + logger.error( + { error, userId: user._id }, + 'could not send security alert email when password changed' + ) + } + }) +} + +async function _ensureAffiliation(userId, emailData) { + if (emailData.samlProviderId) { + await UserUpdater.promises.confirmEmail(userId, emailData.email) + } else { + await UserUpdater.promises.addAffiliationForNewUser(userId, emailData.email) + } +} + +async function changePassword(req, res, next) { + metrics.inc('user.password-change') + const userId = SessionManager.getLoggedInUserId(req.session) + + const user = await AuthenticationManager.promises.authenticate( + { _id: userId }, + req.body.currentPassword + ) + if (!user) { + return HttpErrorHandler.badRequest(req, res, 'Your old password is wrong') + } + + if (req.body.newPassword1 !== req.body.newPassword2) { + return HttpErrorHandler.badRequest( + req, + res, + req.i18n.translate('password_change_passwords_do_not_match') + ) + } + + try { + await AuthenticationManager.promises.setUserPassword( + user, + req.body.newPassword1 + ) + } catch (error) { + if (error.name === 'InvalidPasswordError') { + return HttpErrorHandler.badRequest(req, res, error.message) + } else { + throw error + } + } + await UserAuditLogHandler.promises.addEntry( + user._id, + 'update-password', + user._id, + req.ip + ) + + // no need to wait, errors are logged and not passed back + _sendSecurityAlertPasswordChanged(user) + + await UserSessionsManager.promises.revokeAllUserSessions(user, [ + req.sessionID, + ]) + + return res.json({ + message: { + type: 'success', + email: user.email, + text: req.i18n.translate('password_change_successful'), + }, + }) +} + +async function clearSessions(req, res, next) { + metrics.inc('user.clear-sessions') + const userId = SessionManager.getLoggedInUserId(req.session) + const user = await UserGetter.promises.getUser(userId, { email: 1 }) + const sessions = await UserSessionsManager.promises.getAllUserSessions(user, [ + req.sessionID, + ]) + await UserAuditLogHandler.promises.addEntry( + user._id, + 'clear-sessions', + user._id, + req.ip, + { sessions } + ) + await UserSessionsManager.promises.revokeAllUserSessions(user, [ + req.sessionID, + ]) + + await _sendSecurityAlertClearedSessions(user) + + res.sendStatus(201) +} + +async function ensureAffiliation(user) { + if (!Features.hasFeature('affiliations')) { + return + } + + const flaggedEmails = user.emails.filter(email => email.affiliationUnchecked) + if (flaggedEmails.length === 0) { + return + } + + if (flaggedEmails.length > 1) { + logger.error( + { userId: user._id }, + `Unexpected number of flagged emails: ${flaggedEmails.length}` + ) + } + + await _ensureAffiliation(user._id, flaggedEmails[0]) +} + +async function ensureAffiliationMiddleware(req, res, next) { + let user + if (!Features.hasFeature('affiliations') || !req.query.ensureAffiliation) { + return next() + } + const userId = SessionManager.getLoggedInUserId(req.session) + try { + user = await UserGetter.promises.getUser(userId) + } catch (error) { + return new Errors.UserNotFoundError({ info: { userId } }) + } + try { + await ensureAffiliation(user) + } catch (error) { + return next(error) + } + return next() +} + +const UserController = { + clearSessions: expressify(clearSessions), + + tryDeleteUser(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + const { password } = req.body + + if (password == null || password === '') { + logger.err( + { userId }, + 'no password supplied for attempt to delete account' + ) + return res.sendStatus(403) + } + AuthenticationManager.authenticate( + { _id: userId }, + password, + (err, user) => { + if (err != null) { + OError.tag( + err, + 'error authenticating during attempt to delete account', + { + userId, + } + ) + return next(err) + } + if (!user) { + logger.err({ userId }, 'auth failed during attempt to delete account') + return res.sendStatus(403) + } + UserDeleter.deleteUser( + userId, + { deleterUser: user, ipAddress: req.ip }, + err => { + if (err) { + const errorData = { + message: 'error while deleting user account', + info: { userId }, + } + if (err instanceof Errors.SubscriptionAdminDeletionError) { + // set info.public.error for JSON response so frontend can display + // a specific message + errorData.info.public = { + error: 'SubscriptionAdminDeletionError', + } + logger.warn(OError.tag(err, errorData.message, errorData.info)) + return HttpErrorHandler.unprocessableEntity( + req, + res, + errorData.message, + errorData.info.public + ) + } else { + return next(OError.tag(err, errorData.message, errorData.info)) + } + } + const sessionId = req.sessionID + if (typeof req.logout === 'function') { + req.logout() + } + req.session.destroy(err => { + if (err != null) { + OError.tag(err, 'error destroying session') + return next(err) + } + UserSessionsManager.untrackSession(user, sessionId) + res.sendStatus(200) + }) + } + ) + } + ) + }, + + unsubscribe(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + UserGetter.getUser(userId, (err, user) => { + if (err != null) { + return next(err) + } + NewsletterManager.unsubscribe(user, err => { + if (err != null) { + logger.warn( + { err, user }, + 'Failed to unsubscribe user from newsletter' + ) + } + res.sendStatus(200) + }) + }) + }, + + updateUserSettings(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + User.findById(userId, (err, user) => { + if (err != null || user == null) { + logger.err({ err, userId }, 'problem updaing user settings') + return res.sendStatus(500) + } + + if (req.body.first_name != null) { + user.first_name = req.body.first_name.trim() + } + if (req.body.last_name != null) { + user.last_name = req.body.last_name.trim() + } + if (req.body.role != null) { + user.role = req.body.role.trim() + } + if (req.body.institution != null) { + user.institution = req.body.institution.trim() + } + if (req.body.mode != null) { + user.ace.mode = req.body.mode + } + if (req.body.editorTheme != null) { + user.ace.theme = req.body.editorTheme + } + if (req.body.overallTheme != null) { + user.ace.overallTheme = req.body.overallTheme + } + if (req.body.fontSize != null) { + user.ace.fontSize = req.body.fontSize + } + if (req.body.autoComplete != null) { + user.ace.autoComplete = req.body.autoComplete + } + if (req.body.autoPairDelimiters != null) { + user.ace.autoPairDelimiters = req.body.autoPairDelimiters + } + if (req.body.spellCheckLanguage != null) { + user.ace.spellCheckLanguage = req.body.spellCheckLanguage + } + if (req.body.pdfViewer != null) { + user.ace.pdfViewer = req.body.pdfViewer + } + if (req.body.syntaxValidation != null) { + user.ace.syntaxValidation = req.body.syntaxValidation + } + if (req.body.fontFamily != null) { + user.ace.fontFamily = req.body.fontFamily + } + if (req.body.lineHeight != null) { + user.ace.lineHeight = req.body.lineHeight + } + + user.save(err => { + if (err != null) { + return next(err) + } + const newEmail = + req.body.email != null + ? req.body.email.trim().toLowerCase() + : undefined + if ( + newEmail == null || + newEmail === user.email || + req.externalAuthenticationSystemUsed() + ) { + // end here, don't update email + SessionManager.setInSessionUser(req.session, { + first_name: user.first_name, + last_name: user.last_name, + }) + res.sendStatus(200) + } else if (newEmail.indexOf('@') === -1) { + // email invalid + res.sendStatus(400) + } else { + // update the user email + const auditLog = { + initiatorId: userId, + ipAddress: req.ip, + } + UserUpdater.changeEmailAddress(userId, newEmail, auditLog, err => { + if (err) { + if (err instanceof Errors.EmailExistsError) { + const translation = req.i18n.translate( + 'email_already_registered' + ) + return HttpErrorHandler.conflict(req, res, translation) + } else { + return HttpErrorHandler.legacyInternal( + req, + res, + req.i18n.translate('problem_changing_email_address'), + OError.tag(err, 'problem_changing_email_address', { + userId, + newEmail, + }) + ) + } + } + User.findById(userId, (err, user) => { + if (err != null) { + logger.err( + { err, userId }, + 'error getting user for email update' + ) + return res.sendStatus(500) + } + SessionManager.setInSessionUser(req.session, { + email: user.email, + first_name: user.first_name, + last_name: user.last_name, + }) + UserHandler.populateTeamInvites(user, err => { + // need to refresh this in the background + if (err != null) { + logger.err({ err }, 'error populateTeamInvites') + } + res.sendStatus(200) + }) + }) + }) + } + }) + }) + }, + + doLogout(req, cb) { + metrics.inc('user.logout') + const user = SessionManager.getSessionUser(req.session) + logger.log({ user }, 'logging out') + const sessionId = req.sessionID + if (typeof req.logout === 'function') { + req.logout() + } // passport logout + req.session.destroy(err => { + if (err) { + OError.tag(err, 'error destroying session') + return cb(err) + } + if (user != null) { + UserSessionsManager.untrackSession(user, sessionId) + } + cb() + }) + }, + + logout(req, res, next) { + const requestedRedirect = req.body.redirect + ? UrlHelper.getSafeRedirectPath(req.body.redirect) + : undefined + const redirectUrl = requestedRedirect || '/login' + + UserController.doLogout(req, err => { + if (err != null) { + return next(err) + } + res.redirect(redirectUrl) + }) + }, + + expireDeletedUser(req, res, next) { + const userId = req.params.userId + UserDeleter.expireDeletedUser(userId, error => { + if (error) { + return next(error) + } + + res.sendStatus(204) + }) + }, + + expireDeletedUsersAfterDuration(req, res, next) { + UserDeleter.expireDeletedUsersAfterDuration(error => { + if (error) { + return next(error) + } + + res.sendStatus(204) + }) + }, + + register(req, res, next) { + const { email } = req.body + if (email == null || email === '') { + return res.sendStatus(422) // Unprocessable Entity + } + UserRegistrationHandler.registerNewUserAndSendActivationEmail( + email, + (error, user, setNewPasswordUrl) => { + if (error != null) { + return next(error) + } + res.json({ + email: user.email, + setNewPasswordUrl, + }) + } + ) + }, + + changePassword: expressify(changePassword), +} + +UserController.promises = { + doLogout: promisify(UserController.doLogout), + ensureAffiliation, + ensureAffiliationMiddleware, +} + +module.exports = UserController diff --git a/services/web/app/src/Features/User/UserCreator.js b/services/web/app/src/Features/User/UserCreator.js new file mode 100644 index 0000000000..95f6917b89 --- /dev/null +++ b/services/web/app/src/Features/User/UserCreator.js @@ -0,0 +1,115 @@ +const logger = require('logger-sharelatex') +const util = require('util') +const { AffiliationError } = require('../Errors/Errors') +const Features = require('../../infrastructure/Features') +const { User } = require('../../models/User') +const UserDeleter = require('./UserDeleter') +const UserGetter = require('./UserGetter') +const UserUpdater = require('./UserUpdater') +const Analytics = require('../Analytics/AnalyticsManager') +const UserOnboardingEmailQueueManager = require('./UserOnboardingEmailManager') +const UserPostRegistrationAnalyticsManager = require('./UserPostRegistrationAnalyticsManager') +const OError = require('@overleaf/o-error') + +async function _addAffiliation(user, affiliationOptions) { + try { + await UserUpdater.promises.addAffiliationForNewUser( + user._id, + user.email, + affiliationOptions + ) + } catch (error) { + throw new AffiliationError('add affiliation failed').withCause(error) + } + + try { + user = await UserGetter.promises.getUser(user._id) + } catch (error) { + logger.error( + OError.tag(error, 'could not get fresh user data', { + userId: user._id, + email: user.email, + }) + ) + } + return user +} + +async function createNewUser(attributes, options = {}) { + let user = new User() + + if (attributes.first_name == null || attributes.first_name === '') { + attributes.first_name = attributes.email.split('@')[0] + } + + Object.assign(user, attributes) + + user.ace.syntaxValidation = true + if (user.featureSwitches != null) { + user.featureSwitches.pdfng = true + } + + const reversedHostname = user.email.split('@')[1].split('').reverse().join('') + + const emailData = { + email: user.email, + createdAt: new Date(), + reversedHostname, + } + if (Features.hasFeature('affiliations')) { + emailData.affiliationUnchecked = true + } + if ( + attributes.samlIdentifiers && + attributes.samlIdentifiers[0] && + attributes.samlIdentifiers[0].providerId + ) { + emailData.samlProviderId = attributes.samlIdentifiers[0].providerId + } + + user.emails = [emailData] + + user = await user.save() + + if (Features.hasFeature('affiliations')) { + try { + user = await _addAffiliation(user, options.affiliationOptions || {}) + } catch (error) { + if (options.requireAffiliation) { + await UserDeleter.promises.deleteMongoUser(user._id) + throw OError.tag(error) + } else { + logger.error(OError.tag(error)) + } + } + } + + Analytics.recordEvent(user._id, 'user-registered') + Analytics.setUserProperty(user._id, 'created-at', new Date()) + + if (Features.hasFeature('saas')) { + try { + await UserOnboardingEmailQueueManager.scheduleOnboardingEmail(user) + await UserPostRegistrationAnalyticsManager.schedulePostRegistrationAnalytics( + user + ) + } catch (error) { + logger.error( + OError.tag(error, 'Failed to schedule sending of onboarding email', { + userId: user._id, + }) + ) + } + } + + return user +} + +const UserCreator = { + createNewUser: util.callbackify(createNewUser), + promises: { + createNewUser: createNewUser, + }, +} + +module.exports = UserCreator diff --git a/services/web/app/src/Features/User/UserDeleter.js b/services/web/app/src/Features/User/UserDeleter.js new file mode 100644 index 0000000000..a183978cb8 --- /dev/null +++ b/services/web/app/src/Features/User/UserDeleter.js @@ -0,0 +1,133 @@ +const { callbackify } = require('util') +const logger = require('logger-sharelatex') +const moment = require('moment') +const { User } = require('../../models/User') +const { DeletedUser } = require('../../models/DeletedUser') +const NewsletterManager = require('../Newsletter/NewsletterManager') +const ProjectDeleter = require('../Project/ProjectDeleter') +const SubscriptionHandler = require('../Subscription/SubscriptionHandler') +const SubscriptionUpdater = require('../Subscription/SubscriptionUpdater') +const SubscriptionLocator = require('../Subscription/SubscriptionLocator') +const UserMembershipsHandler = require('../UserMembership/UserMembershipsHandler') +const UserSessionsManager = require('./UserSessionsManager') +const InstitutionsAPI = require('../Institutions/InstitutionsAPI') +const Errors = require('../Errors/Errors') + +module.exports = { + deleteUser: callbackify(deleteUser), + deleteMongoUser: callbackify(deleteMongoUser), + expireDeletedUser: callbackify(expireDeletedUser), + ensureCanDeleteUser: callbackify(ensureCanDeleteUser), + expireDeletedUsersAfterDuration: callbackify(expireDeletedUsersAfterDuration), + + promises: { + deleteUser: deleteUser, + deleteMongoUser: deleteMongoUser, + expireDeletedUser: expireDeletedUser, + ensureCanDeleteUser: ensureCanDeleteUser, + expireDeletedUsersAfterDuration: expireDeletedUsersAfterDuration, + }, +} + +async function deleteUser(userId, options = {}) { + if (!userId) { + logger.warn('user_id is null when trying to delete user') + throw new Error('no user_id') + } + + try { + const user = await User.findById(userId).exec() + logger.log({ user }, 'deleting user') + + await ensureCanDeleteUser(user) + await _cleanupUser(user) + await _createDeletedUser(user, options) + await ProjectDeleter.promises.deleteUsersProjects(user._id) + await deleteMongoUser(user._id) + } catch (error) { + logger.warn({ error, userId }, 'something went wrong deleting the user') + throw error + } +} + +/** + * delete a user document only + */ +async function deleteMongoUser(userId) { + if (!userId) { + throw new Error('no user_id') + } + + await User.deleteOne({ _id: userId }).exec() +} + +async function expireDeletedUser(userId) { + const deletedUser = await DeletedUser.findOne({ + 'deleterData.deletedUserId': userId, + }).exec() + + deletedUser.user = undefined + deletedUser.deleterData.deleterIpAddress = undefined + await deletedUser.save() +} + +async function expireDeletedUsersAfterDuration() { + const DURATION = 90 + const deletedUsers = await DeletedUser.find({ + 'deleterData.deletedAt': { + $lt: new Date(moment().subtract(DURATION, 'days')), + }, + user: { + $ne: null, + }, + }).exec() + + if (deletedUsers.length === 0) { + return + } + + for (let i = 0; i < deletedUsers.length; i++) { + await expireDeletedUser(deletedUsers[i].deleterData.deletedUserId) + } +} + +async function ensureCanDeleteUser(user) { + const subscription = await SubscriptionLocator.promises.getUsersSubscription( + user + ) + if (subscription) { + throw new Errors.SubscriptionAdminDeletionError({}) + } +} + +async function _createDeletedUser(user, options) { + await DeletedUser.updateOne( + { 'deleterData.deletedUserId': user._id }, + { + user: user, + deleterData: { + deletedAt: new Date(), + deleterId: options.deleterUser ? options.deleterUser._id : undefined, + deleterIpAddress: options.ipAddress, + deletedUserId: user._id, + deletedUserLastLoggedIn: user.lastLoggedIn, + deletedUserSignUpDate: user.signUpDate, + deletedUserLoginCount: user.loginCount, + deletedUserReferralId: user.referal_id, + deletedUserReferredUsers: user.refered_users, + deletedUserReferredUserCount: user.refered_user_count, + deletedUserOverleafId: user.overleaf ? user.overleaf.id : undefined, + }, + }, + { upsert: true } + ) +} + +async function _cleanupUser(user) { + await UserSessionsManager.promises.revokeAllUserSessions(user._id, []) + await NewsletterManager.promises.unsubscribe(user, { delete: true }) + await SubscriptionHandler.promises.cancelSubscription(user) + await InstitutionsAPI.promises.deleteAffiliations(user._id) + await SubscriptionUpdater.promises.removeUserFromAllGroups(user._id) + await UserMembershipsHandler.promises.removeUserFromAllEntities(user._id) +} diff --git a/services/web/app/src/Features/User/UserEmailsConfirmationHandler.js b/services/web/app/src/Features/User/UserEmailsConfirmationHandler.js new file mode 100644 index 0000000000..f204482d54 --- /dev/null +++ b/services/web/app/src/Features/User/UserEmailsConfirmationHandler.js @@ -0,0 +1,117 @@ +const EmailHelper = require('../Helpers/EmailHelper') +const EmailHandler = require('../Email/EmailHandler') +const OneTimeTokenHandler = require('../Security/OneTimeTokenHandler') +const settings = require('@overleaf/settings') +const Errors = require('../Errors/Errors') +const UserUpdater = require('./UserUpdater') +const UserGetter = require('./UserGetter') +const { callbackify, promisify } = require('util') + +// Reject email confirmation tokens after 90 days +const TOKEN_EXPIRY_IN_S = 90 * 24 * 60 * 60 +const TOKEN_USE = 'email_confirmation' + +function sendConfirmationEmail(userId, email, emailTemplate, callback) { + if (arguments.length === 3) { + callback = emailTemplate + emailTemplate = 'confirmEmail' + } + + // when force-migrating accounts to v2 from v1, we don't want to send confirmation messages - + // setting this env var allows us to turn this behaviour off + if (process.env.SHARELATEX_NO_CONFIRMATION_MESSAGES != null) { + return callback(null) + } + + email = EmailHelper.parseEmail(email) + if (!email) { + return callback(new Error('invalid email')) + } + const data = { user_id: userId, email } + OneTimeTokenHandler.getNewToken( + TOKEN_USE, + data, + { expiresIn: TOKEN_EXPIRY_IN_S }, + function (err, token) { + if (err) { + return callback(err) + } + const emailOptions = { + to: email, + confirmEmailUrl: `${settings.siteUrl}/user/emails/confirm?token=${token}`, + sendingUser_id: userId, + } + EmailHandler.sendEmail(emailTemplate, emailOptions, callback) + } + ) +} + +async function sendReconfirmationEmail(userId, email) { + email = EmailHelper.parseEmail(email) + if (!email) { + throw new Error('invalid email') + } + + const data = { user_id: userId, email } + const token = await OneTimeTokenHandler.promises.getNewToken( + TOKEN_USE, + data, + { expiresIn: TOKEN_EXPIRY_IN_S } + ) + + const emailOptions = { + to: email, + confirmEmailUrl: `${settings.siteUrl}/user/emails/confirm?token=${token}`, + sendingUser_id: userId, + } + + await EmailHandler.promises.sendEmail('reconfirmEmail', emailOptions) +} + +const UserEmailsConfirmationHandler = { + sendConfirmationEmail, + + sendReconfirmationEmail: callbackify(sendReconfirmationEmail), + + confirmEmailFromToken(token, callback) { + OneTimeTokenHandler.getValueFromTokenAndExpire( + TOKEN_USE, + token, + function (error, data) { + if (error) { + return callback(error) + } + if (!data) { + return callback(new Errors.NotFoundError('no token found')) + } + const userId = data.user_id + const email = data.email + + if (!userId || email !== EmailHelper.parseEmail(email)) { + return callback(new Errors.NotFoundError('invalid data')) + } + UserGetter.getUser(userId, {}, function (error, user) { + if (error) { + return callback(error) + } + if (!user) { + return callback(new Errors.NotFoundError('user not found')) + } + const emailExists = user.emails.some( + emailData => emailData.email === email + ) + if (!emailExists) { + return callback(new Errors.NotFoundError('email missing for user')) + } + UserUpdater.confirmEmail(userId, email, callback) + }) + } + ) + }, +} + +UserEmailsConfirmationHandler.promises = { + sendConfirmationEmail: promisify(sendConfirmationEmail), +} + +module.exports = UserEmailsConfirmationHandler diff --git a/services/web/app/src/Features/User/UserEmailsController.js b/services/web/app/src/Features/User/UserEmailsController.js new file mode 100644 index 0000000000..cff626fb0d --- /dev/null +++ b/services/web/app/src/Features/User/UserEmailsController.js @@ -0,0 +1,253 @@ +const logger = require('logger-sharelatex') +const SessionManager = require('../Authentication/SessionManager') +const UserGetter = require('./UserGetter') +const UserUpdater = require('./UserUpdater') +const UserSessionsManager = require('./UserSessionsManager') +const EmailHandler = require('../Email/EmailHandler') +const EmailHelper = require('../Helpers/EmailHelper') +const UserEmailsConfirmationHandler = require('./UserEmailsConfirmationHandler') +const { endorseAffiliation } = require('../Institutions/InstitutionsAPI') +const Errors = require('../Errors/Errors') +const HttpErrorHandler = require('../Errors/HttpErrorHandler') +const { expressify } = require('../../util/promises') + +async function _sendSecurityAlertEmail(user, email) { + const emailOptions = { + to: user.email, + actionDescribed: `a secondary email address has been added to your account ${user.email}`, + message: [ + `Added:
${email}
`, + ], + action: 'secondary email address added', + } + await EmailHandler.promises.sendEmail('securityAlert', emailOptions) +} + +async function add(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + const email = EmailHelper.parseEmail(req.body.email) + if (!email) { + return res.sendStatus(422) + } + const user = await UserGetter.promises.getUser(userId, { email: 1 }) + + const affiliationOptions = { + university: req.body.university, + role: req.body.role, + department: req.body.department, + } + + try { + await UserUpdater.promises.addEmailAddress( + userId, + email, + affiliationOptions, + { + initiatorId: user._id, + ipAddress: req.ip, + } + ) + } catch (error) { + return UserEmailsController._handleEmailError(error, req, res, next) + } + + await _sendSecurityAlertEmail(user, email) + + await UserEmailsConfirmationHandler.promises.sendConfirmationEmail( + userId, + email + ) + + res.sendStatus(204) +} + +function resendConfirmation(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + const email = EmailHelper.parseEmail(req.body.email) + if (!email) { + return res.sendStatus(422) + } + UserGetter.getUserByAnyEmail(email, { _id: 1 }, function (error, user) { + if (error) { + return next(error) + } + if (!user || user._id.toString() !== userId) { + return res.sendStatus(422) + } + UserEmailsConfirmationHandler.sendConfirmationEmail( + userId, + email, + function (error) { + if (error) { + return next(error) + } + res.sendStatus(200) + } + ) + }) +} + +function sendReconfirmation(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + const email = EmailHelper.parseEmail(req.body.email) + if (!email) { + return res.sendStatus(400) + } + UserGetter.getUserByAnyEmail(email, { _id: 1 }, function (error, user) { + if (error) { + return next(error) + } + if (!user || user._id.toString() !== userId) { + return res.sendStatus(422) + } + UserEmailsConfirmationHandler.sendReconfirmationEmail( + userId, + email, + function (error) { + if (error) { + return next(error) + } + res.sendStatus(200) + } + ) + }) +} + +const UserEmailsController = { + list(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + UserGetter.getUserFullEmails(userId, function (error, fullEmails) { + if (error) { + return next(error) + } + res.json(fullEmails) + }) + }, + + add: expressify(add), + + remove(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + const email = EmailHelper.parseEmail(req.body.email) + if (!email) { + return res.sendStatus(422) + } + + UserUpdater.removeEmailAddress(userId, email, function (error) { + if (error) { + return next(error) + } + res.sendStatus(200) + }) + }, + + setDefault(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + const email = EmailHelper.parseEmail(req.body.email) + if (!email) { + return res.sendStatus(422) + } + const auditLog = { + initiatorId: userId, + ipAddress: req.ip, + } + UserUpdater.setDefaultEmailAddress( + userId, + email, + false, + auditLog, + true, + err => { + if (err) { + return UserEmailsController._handleEmailError(err, req, res, next) + } + SessionManager.setInSessionUser(req.session, { email: email }) + const user = SessionManager.getSessionUser(req.session) + UserSessionsManager.revokeAllUserSessions( + user, + [req.sessionID], + err => { + if (err) + logger.warn( + { err }, + 'failed revoking secondary sessions after changing default email' + ) + } + ) + res.sendStatus(200) + } + ) + }, + + endorse(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + const email = EmailHelper.parseEmail(req.body.email) + if (!email) { + return res.sendStatus(422) + } + + endorseAffiliation( + userId, + email, + req.body.role, + req.body.department, + function (error) { + if (error) { + return next(error) + } + res.sendStatus(204) + } + ) + }, + + resendConfirmation, + + sendReconfirmation, + + showConfirm(req, res, next) { + res.render('user/confirm_email', { + token: req.query.token, + title: 'confirm_email', + }) + }, + + confirm(req, res, next) { + const { token } = req.body + if (!token) { + return res.status(422).json({ + message: req.i18n.translate('confirmation_link_broken'), + }) + } + UserEmailsConfirmationHandler.confirmEmailFromToken( + token, + function (error) { + if (error) { + if (error instanceof Errors.NotFoundError) { + res.status(404).json({ + message: req.i18n.translate('confirmation_token_invalid'), + }) + } else { + next(error) + } + } else { + res.sendStatus(200) + } + } + ) + }, + + _handleEmailError(error, req, res, next) { + if (error instanceof Errors.UnconfirmedEmailError) { + return HttpErrorHandler.conflict(req, res, 'email must be confirmed') + } else if (error instanceof Errors.EmailExistsError) { + const message = req.i18n.translate('email_already_registered') + return HttpErrorHandler.conflict(req, res, message) + } else if (error.message === '422: Email does not belong to university') { + const message = req.i18n.translate('email_does_not_belong_to_university') + return HttpErrorHandler.conflict(req, res, message) + } + next(error) + }, +} + +module.exports = UserEmailsController diff --git a/services/web/app/src/Features/User/UserGetter.js b/services/web/app/src/Features/User/UserGetter.js new file mode 100644 index 0000000000..6649459b0b --- /dev/null +++ b/services/web/app/src/Features/User/UserGetter.js @@ -0,0 +1,277 @@ +const { callbackify } = require('util') +const { db } = require('../../infrastructure/mongodb') +const metrics = require('@overleaf/metrics') +const logger = require('logger-sharelatex') +const moment = require('moment') +const settings = require('@overleaf/settings') +const { promisifyAll } = require('../../util/promises') +const { + promises: InstitutionsAPIPromises, +} = require('../Institutions/InstitutionsAPI') +const InstitutionsHelper = require('../Institutions/InstitutionsHelper') +const Errors = require('../Errors/Errors') +const Features = require('../../infrastructure/Features') +const { User } = require('../../models/User') +const { normalizeQuery, normalizeMultiQuery } = require('../Helpers/Mongo') + +function _lastDayToReconfirm(emailData, institutionData) { + const globalReconfirmPeriod = settings.reconfirmNotificationDays + if (!globalReconfirmPeriod) return undefined + + // only show notification for institutions with reconfirmation enabled + if (!institutionData || !institutionData.maxConfirmationMonths) + return undefined + + if (!emailData.confirmedAt) return undefined + + if (institutionData.ssoEnabled && !emailData.samlProviderId) { + // For SSO, only show notification for linked email + return false + } + + // reconfirmedAt will not always be set, use confirmedAt as fallback + const lastConfirmed = emailData.reconfirmedAt || emailData.confirmedAt + + return moment(lastConfirmed) + .add(institutionData.maxConfirmationMonths, 'months') + .toDate() +} + +function _pastReconfirmDate(lastDayToReconfirm) { + if (!lastDayToReconfirm) return false + return moment(lastDayToReconfirm).isBefore() +} + +function _emailInReconfirmNotificationPeriod(lastDayToReconfirm) { + const globalReconfirmPeriod = settings.reconfirmNotificationDays + + if (!globalReconfirmPeriod || !lastDayToReconfirm) return false + + const notificationStarts = moment(lastDayToReconfirm).subtract( + globalReconfirmPeriod, + 'days' + ) + + return moment().isAfter(notificationStarts) +} + +async function getUserFullEmails(userId) { + const user = await UserGetter.promises.getUser(userId, { + email: 1, + emails: 1, + samlIdentifiers: 1, + }) + + if (!user) { + throw new Error('User not Found') + } + + if (!Features.hasFeature('affiliations')) { + return decorateFullEmails(user.email, user.emails, [], []) + } + + const affiliationsData = await InstitutionsAPIPromises.getUserAffiliations( + userId + ) + + return decorateFullEmails( + user.email, + user.emails || [], + affiliationsData, + user.samlIdentifiers || [] + ) +} + +async function getSsoUsersAtInstitution(institutionId, projection) { + if (!projection) { + throw new Error('missing projection') + } + + return await User.find( + { + 'samlIdentifiers.providerId': institutionId.toString(), + }, + projection + ).exec() +} + +const UserGetter = { + getSsoUsersAtInstitution: callbackify(getSsoUsersAtInstitution), + + getUser(query, projection, callback) { + if (arguments.length === 2) { + callback = projection + projection = {} + } + try { + query = normalizeQuery(query) + db.users.findOne(query, { projection }, callback) + } catch (err) { + callback(err) + } + }, + + getUserEmail(userId, callback) { + this.getUser(userId, { email: 1 }, (error, user) => + callback(error, user && user.email) + ) + }, + + getUserFullEmails: callbackify(getUserFullEmails), + + getUserByMainEmail(email, projection, callback) { + email = email.trim() + if (arguments.length === 2) { + callback = projection + projection = {} + } + db.users.findOne({ email }, { projection }, callback) + }, + + getUserByAnyEmail(email, projection, callback) { + email = email.trim() + if (arguments.length === 2) { + callback = projection + projection = {} + } + // $exists: true MUST be set to use the partial index + const query = { emails: { $exists: true }, 'emails.email': email } + db.users.findOne(query, { projection }, (error, user) => { + if (error || user) { + return callback(error, user) + } + + // While multiple emails are being rolled out, check for the main email as + // well + this.getUserByMainEmail(email, projection, callback) + }) + }, + + getUsersByAnyConfirmedEmail(emails, projection, callback) { + if (arguments.length === 2) { + callback = projection + projection = {} + } + + const query = { + 'emails.email': { $in: emails }, // use the index on emails.email + emails: { + $exists: true, + $elemMatch: { + email: { $in: emails }, + confirmedAt: { $exists: true }, + }, + }, + } + + db.users.find(query, { projection }).toArray(callback) + }, + + getUsersByV1Ids(v1Ids, projection, callback) { + if (arguments.length === 2) { + callback = projection + projection = {} + } + const query = { 'overleaf.id': { $in: v1Ids } } + db.users.find(query, { projection }).toArray(callback) + }, + + getUsersByHostname(hostname, projection, callback) { + const reversedHostname = hostname.trim().split('').reverse().join('') + const query = { + emails: { $exists: true }, + 'emails.reversedHostname': reversedHostname, + } + db.users.find(query, { projection }).toArray(callback) + }, + + getUsers(query, projection, callback) { + try { + query = normalizeMultiQuery(query) + db.users.find(query, { projection }).toArray(callback) + } catch (err) { + callback(err) + } + }, + + // check for duplicate email address. This is also enforced at the DB level + ensureUniqueEmailAddress(newEmail, callback) { + this.getUserByAnyEmail(newEmail, function (error, user) { + if (user) { + return callback(new Errors.EmailExistsError()) + } + callback(error) + }) + }, +} + +var decorateFullEmails = ( + defaultEmail, + emailsData, + affiliationsData, + samlIdentifiers +) => { + emailsData.forEach(function (emailData) { + emailData.default = emailData.email === defaultEmail + + const affiliation = affiliationsData.find( + aff => aff.email === emailData.email + ) + if (affiliation) { + const { + institution, + inferred, + role, + department, + licence, + portal, + } = affiliation + const lastDayToReconfirm = _lastDayToReconfirm(emailData, institution) + const pastReconfirmDate = _pastReconfirmDate(lastDayToReconfirm) + const inReconfirmNotificationPeriod = _emailInReconfirmNotificationPeriod( + lastDayToReconfirm + ) + emailData.affiliation = { + institution, + inferred, + inReconfirmNotificationPeriod, + lastDayToReconfirm, + pastReconfirmDate, + role, + department, + licence, + portal, + } + } + + if (emailData.samlProviderId) { + emailData.samlIdentifier = samlIdentifiers.find( + samlIdentifier => samlIdentifier.providerId === emailData.samlProviderId + ) + } + + emailData.emailHasInstitutionLicence = InstitutionsHelper.emailHasLicence( + emailData + ) + }) + + return emailsData +} +;[ + 'getUser', + 'getUserEmail', + 'getUserByMainEmail', + 'getUserByAnyEmail', + 'getUsers', + 'ensureUniqueEmailAddress', +].map(method => + metrics.timeAsyncMethod(UserGetter, method, 'mongo.UserGetter', logger) +) + +UserGetter.promises = promisifyAll(UserGetter, { + without: ['getSsoUsersAtInstitution', 'getUserFullEmails'], +}) +UserGetter.promises.getUserFullEmails = getUserFullEmails +UserGetter.promises.getSsoUsersAtInstitution = getSsoUsersAtInstitution + +module.exports = UserGetter diff --git a/services/web/app/src/Features/User/UserHandler.js b/services/web/app/src/Features/User/UserHandler.js new file mode 100644 index 0000000000..0966664c33 --- /dev/null +++ b/services/web/app/src/Features/User/UserHandler.js @@ -0,0 +1,15 @@ +const TeamInvitesHandler = require('../Subscription/TeamInvitesHandler') + +const UserHandler = { + populateTeamInvites(user, callback) { + TeamInvitesHandler.createTeamInvitesForLegacyInvitedEmail( + user.email, + callback + ) + }, + + setupLoginData(user, callback) { + this.populateTeamInvites(user, callback) + }, +} +module.exports = UserHandler diff --git a/services/web/app/src/Features/User/UserInfoController.js b/services/web/app/src/Features/User/UserInfoController.js new file mode 100644 index 0000000000..8de15065c7 --- /dev/null +++ b/services/web/app/src/Features/User/UserInfoController.js @@ -0,0 +1,82 @@ +let UserController +const UserGetter = require('./UserGetter') +const SessionManager = require('../Authentication/SessionManager') +const { ObjectId } = require('mongodb') + +module.exports = UserController = { + getLoggedInUsersPersonalInfo(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + if (!userId) { + return next(new Error('User is not logged in')) + } + UserGetter.getUser( + userId, + { + first_name: true, + last_name: true, + role: true, + institution: true, + email: true, + signUpDate: true, + }, + function (error, user) { + if (error) { + return next(error) + } + UserController.sendFormattedPersonalInfo(user, res, next) + } + ) + }, + + getPersonalInfo(req, res, next) { + let query + const userId = req.params.user_id + + if (/^\d+$/.test(userId)) { + query = { 'overleaf.id': parseInt(userId, 10) } + } else if (/^[a-f0-9]{24}$/.test(userId)) { + query = { _id: ObjectId(userId) } + } else { + return res.sendStatus(400) + } + + UserGetter.getUser( + query, + { _id: true, first_name: true, last_name: true, email: true }, + function (error, user) { + if (error) { + return next(error) + } + if (!user) { + return res.sendStatus(404) + } + UserController.sendFormattedPersonalInfo(user, res, next) + } + ) + }, + + sendFormattedPersonalInfo(user, res, next) { + const info = UserController.formatPersonalInfo(user) + res.json(info) + }, + + formatPersonalInfo(user, callback) { + if (!user) { + return {} + } + const formattedUser = { id: user._id.toString() } + for (const key of [ + 'first_name', + 'last_name', + 'email', + 'signUpDate', + 'role', + 'institution', + ]) { + if (user[key]) { + formattedUser[key] = user[key] + } + } + return formattedUser + }, +} diff --git a/services/web/app/src/Features/User/UserInfoManager.js b/services/web/app/src/Features/User/UserInfoManager.js new file mode 100644 index 0000000000..d1a44e22c7 --- /dev/null +++ b/services/web/app/src/Features/User/UserInfoManager.js @@ -0,0 +1,13 @@ +const UserGetter = require('./UserGetter') + +const UserInfoManager = { + getPersonalInfo(userId, callback) { + UserGetter.getUser( + userId, + { _id: true, first_name: true, last_name: true, email: true }, + callback + ) + }, +} + +module.exports = UserInfoManager diff --git a/services/web/app/src/Features/User/UserOnboardingEmailManager.js b/services/web/app/src/Features/User/UserOnboardingEmailManager.js new file mode 100644 index 0000000000..fb990a525d --- /dev/null +++ b/services/web/app/src/Features/User/UserOnboardingEmailManager.js @@ -0,0 +1,44 @@ +const Features = require('../../infrastructure/Features') +const Queues = require('../../infrastructure/Queues') +const EmailHandler = require('../Email/EmailHandler') +const UserUpdater = require('./UserUpdater') +const UserGetter = require('./UserGetter') + +const ONE_DAY_MS = 24 * 60 * 60 * 1000 + +class UserOnboardingEmailManager { + constructor() { + this.queue = Queues.getOnboardingEmailsQueue() + this.queue.process(async job => { + const { userId } = job.data + await this._sendOnboardingEmail(userId) + }) + } + + async scheduleOnboardingEmail(user) { + await this.queue.add({ userId: user._id }, { delay: ONE_DAY_MS }) + } + + async _sendOnboardingEmail(userId) { + const user = await UserGetter.promises.getUser( + { _id: userId }, + { email: 1 } + ) + if (user) { + await EmailHandler.promises.sendEmail('userOnboardingEmail', { + to: user.email, + }) + await UserUpdater.promises.updateUser(user._id, { + $set: { onboardingEmailSentAt: new Date() }, + }) + } + } +} + +class NoopManager { + async scheduleOnboardingEmail() {} +} + +module.exports = Features.hasFeature('saas') + ? new UserOnboardingEmailManager() + : new NoopManager() diff --git a/services/web/app/src/Features/User/UserPagesController.js b/services/web/app/src/Features/User/UserPagesController.js new file mode 100644 index 0000000000..975f13ac1b --- /dev/null +++ b/services/web/app/src/Features/User/UserPagesController.js @@ -0,0 +1,190 @@ +const UserGetter = require('./UserGetter') +const OError = require('@overleaf/o-error') +const UserSessionsManager = require('./UserSessionsManager') +const logger = require('logger-sharelatex') +const Settings = require('@overleaf/settings') +const AuthenticationController = require('../Authentication/AuthenticationController') +const SessionManager = require('../Authentication/SessionManager') +const _ = require('lodash') + +const UserPagesController = { + registerPage(req, res) { + const sharedProjectData = { + project_name: req.query.project_name, + user_first_name: req.query.user_first_name, + } + + const newTemplateData = {} + if (req.session.templateData != null) { + newTemplateData.templateName = req.session.templateData.templateName + } + + res.render('user/register', { + title: 'register', + sharedProjectData, + newTemplateData, + samlBeta: req.session.samlBeta, + }) + }, + + loginPage(req, res) { + // if user is being sent to /login with explicit redirect (redir=/foo), + // such as being sent from the editor to /login, then set the redirect explicitly + if ( + req.query.redir != null && + AuthenticationController._getRedirectFromSession(req) == null + ) { + AuthenticationController.setRedirectInSession(req, req.query.redir) + } + res.render('user/login', { + title: 'login', + }) + }, + + /** + * Landing page for users who may have received one-time login + * tokens from the read-only maintenance site. + * + * We tell them that Overleaf is back up and that they can login normally. + */ + oneTimeLoginPage(req, res, next) { + res.render('user/one_time_login') + }, + + logoutPage(req, res) { + res.render('user/logout') + }, + + renderReconfirmAccountPage(req, res) { + const pageData = { + reconfirm_email: req.session.reconfirm_email, + } + // when a user must reconfirm their account + res.render('user/reconfirm', pageData) + }, + + settingsPage(req, res, next) { + const userId = SessionManager.getLoggedInUserId(req.session) + const reconfirmationRemoveEmail = req.query.remove + // SSO + const ssoError = req.session.ssoError + if (ssoError) { + delete req.session.ssoError + } + // Institution SSO + let institutionLinked = _.get(req.session, ['saml', 'linked']) + if (institutionLinked) { + // copy object if exists because _.get does not + institutionLinked = Object.assign( + { + hasEntitlement: _.get(req.session, ['saml', 'hasEntitlement']), + }, + institutionLinked + ) + } + const samlError = _.get(req.session, ['saml', 'error']) + const institutionEmailNonCanonical = _.get(req.session, [ + 'saml', + 'emailNonCanonical', + ]) + const institutionRequestedEmail = _.get(req.session, [ + 'saml', + 'requestedEmail', + ]) + + const reconfirmedViaSAML = _.get(req.session, ['saml', 'reconfirmed']) + delete req.session.saml + let shouldAllowEditingDetails = true + if (Settings.ldap && Settings.ldap.updateUserDetailsOnLogin) { + shouldAllowEditingDetails = false + } + if (Settings.saml && Settings.saml.updateUserDetailsOnLogin) { + shouldAllowEditingDetails = false + } + const oauthProviders = Settings.oauthProviders || {} + + UserGetter.getUser(userId, (err, user) => { + if (err != null) { + return next(err) + } + res.render('user/settings', { + title: 'account_settings', + user, + hasPassword: !!user.hashedPassword, + shouldAllowEditingDetails, + languages: Settings.languages, + accountSettingsTabActive: true, + oauthProviders: UserPagesController._translateProviderDescriptions( + oauthProviders, + req + ), + oauthUseV2: Settings.oauthUseV2 || false, + institutionLinked, + samlError, + institutionEmailNonCanonical: + institutionEmailNonCanonical && institutionRequestedEmail + ? institutionEmailNonCanonical + : undefined, + reconfirmedViaSAML, + reconfirmationRemoveEmail, + samlBeta: req.session.samlBeta, + ssoError: ssoError, + thirdPartyIds: UserPagesController._restructureThirdPartyIds(user), + }) + }) + }, + + sessionsPage(req, res, next) { + const user = SessionManager.getSessionUser(req.session) + logger.log({ userId: user._id }, 'loading sessions page') + UserSessionsManager.getAllUserSessions( + user, + [req.sessionID], + (err, sessions) => { + if (err != null) { + OError.tag(err, 'error getting all user sessions', { + userId: user._id, + }) + return next(err) + } + res.render('user/sessions', { + title: 'sessions', + sessions, + }) + } + ) + }, + + _restructureThirdPartyIds(user) { + // 3rd party identifiers are an array of objects + // this turn them into a single object, which + // makes data easier to use in template + if ( + !user.thirdPartyIdentifiers || + user.thirdPartyIdentifiers.length === 0 + ) { + return null + } + return user.thirdPartyIdentifiers.reduce((obj, identifier) => { + obj[identifier.providerId] = identifier.externalUserId + return obj + }, {}) + }, + + _translateProviderDescriptions(providers, req) { + const result = {} + if (providers) { + for (const provider in providers) { + const data = providers[provider] + data.description = req.i18n.translate( + data.descriptionKey, + Object.assign({}, data.descriptionOptions) + ) + result[provider] = data + } + } + return result + }, +} + +module.exports = UserPagesController diff --git a/services/web/app/src/Features/User/UserPostRegistrationAnalyticsManager.js b/services/web/app/src/Features/User/UserPostRegistrationAnalyticsManager.js new file mode 100644 index 0000000000..d378d9257d --- /dev/null +++ b/services/web/app/src/Features/User/UserPostRegistrationAnalyticsManager.js @@ -0,0 +1,57 @@ +const Queues = require('../../infrastructure/Queues') +const UserGetter = require('./UserGetter') +const { + promises: InstitutionsAPIPromises, +} = require('../Institutions/InstitutionsAPI') +const AnalyticsManager = require('../Analytics/AnalyticsManager') +const Features = require('../../infrastructure/Features') + +const ONE_DAY_MS = 24 * 60 * 60 * 1000 + +class UserPostRegistrationAnalyticsManager { + constructor() { + this.queue = Queues.getPostRegistrationAnalyticsQueue() + this.queue.process(async job => { + const { userId } = job.data + await postRegistrationAnalytics(userId) + }) + } + + async schedulePostRegistrationAnalytics(user) { + await this.queue.add({ userId: user._id }, { delay: ONE_DAY_MS }) + } +} + +async function postRegistrationAnalytics(userId) { + const user = await UserGetter.promises.getUser({ _id: userId }, { email: 1 }) + if (!user) { + return + } + await checkAffiliations(userId) +} + +async function checkAffiliations(userId) { + const affiliationsData = await InstitutionsAPIPromises.getUserAffiliations( + userId + ) + const hasCommonsAccountAffiliation = affiliationsData.some( + affiliationData => + affiliationData.institution && affiliationData.institution.commonsAccount + ) + + if (hasCommonsAccountAffiliation) { + await AnalyticsManager.setUserProperty( + userId, + 'registered-from-commons-account', + true + ) + } +} + +class NoopManager { + async schedulePostRegistrationAnalytics() {} +} + +module.exports = Features.hasFeature('saas') + ? new UserPostRegistrationAnalyticsManager() + : new NoopManager() diff --git a/services/web/app/src/Features/User/UserRegistrationHandler.js b/services/web/app/src/Features/User/UserRegistrationHandler.js new file mode 100644 index 0000000000..d935e4cd7b --- /dev/null +++ b/services/web/app/src/Features/User/UserRegistrationHandler.js @@ -0,0 +1,146 @@ +const { User } = require('../../models/User') +const UserCreator = require('./UserCreator') +const UserGetter = require('./UserGetter') +const AuthenticationManager = require('../Authentication/AuthenticationManager') +const NewsletterManager = require('../Newsletter/NewsletterManager') +const async = require('async') +const logger = require('logger-sharelatex') +const crypto = require('crypto') +const EmailHandler = require('../Email/EmailHandler') +const OneTimeTokenHandler = require('../Security/OneTimeTokenHandler') +const Analytics = require('../Analytics/AnalyticsManager') +const settings = require('@overleaf/settings') +const EmailHelper = require('../Helpers/EmailHelper') + +const UserRegistrationHandler = { + _registrationRequestIsValid(body) { + const invalidEmail = AuthenticationManager.validateEmail(body.email || '') + const invalidPassword = AuthenticationManager.validatePassword( + body.password || '', + body.email + ) + return !(invalidEmail || invalidPassword) + }, + + _createNewUserIfRequired(user, userDetails, callback) { + if (!user) { + userDetails.holdingAccount = false + UserCreator.createNewUser( + { + holdingAccount: false, + email: userDetails.email, + first_name: userDetails.first_name, + last_name: userDetails.last_name, + }, + {}, + callback + ) + } else { + callback(null, user) + } + }, + + registerNewUser(userDetails, callback) { + const self = this + const requestIsValid = this._registrationRequestIsValid(userDetails) + if (!requestIsValid) { + return callback(new Error('request is not valid')) + } + userDetails.email = EmailHelper.parseEmail(userDetails.email) + UserGetter.getUserByAnyEmail(userDetails.email, (error, user) => { + if (error) { + return callback(error) + } + if (user && user.holdingAccount === false) { + return callback(new Error('EmailAlreadyRegistered'), user) + } + self._createNewUserIfRequired(user, userDetails, (error, user) => { + if (error) { + return callback(error) + } + async.series( + [ + callback => + User.updateOne( + { _id: user._id }, + { $set: { holdingAccount: false } }, + callback + ), + callback => + AuthenticationManager.setUserPassword( + user, + userDetails.password, + callback + ), + callback => { + if (userDetails.subscribeToNewsletter === 'true') { + NewsletterManager.subscribe(user, error => { + if (error) { + logger.warn( + { err: error, user }, + 'Failed to subscribe user to newsletter' + ) + } + }) + } + callback() + }, // this can be slow, just fire it off + ], + error => { + Analytics.recordEvent(user._id, 'user-registered') + callback(error, user) + } + ) + }) + }) + }, + + registerNewUserAndSendActivationEmail(email, callback) { + UserRegistrationHandler.registerNewUser( + { + email, + password: crypto.randomBytes(32).toString('hex'), + }, + (error, user) => { + if (error && error.message !== 'EmailAlreadyRegistered') { + return callback(error) + } + + if (error && error.message === 'EmailAlreadyRegistered') { + logger.log({ email }, 'user already exists, resending welcome email') + } + + const ONE_WEEK = 7 * 24 * 60 * 60 // seconds + OneTimeTokenHandler.getNewToken( + 'password', + { user_id: user._id.toString(), email: user.email }, + { expiresIn: ONE_WEEK }, + (error, token) => { + if (error) { + return callback(error) + } + + const setNewPasswordUrl = `${settings.siteUrl}/user/activate?token=${token}&user_id=${user._id}` + + EmailHandler.sendEmail( + 'registered', + { + to: user.email, + setNewPasswordUrl, + }, + error => { + if (error) { + logger.warn({ err: error }, 'failed to send activation email') + } + } + ) + + callback(null, user, setNewPasswordUrl) + } + ) + } + ) + }, +} + +module.exports = UserRegistrationHandler diff --git a/services/web/app/src/Features/User/UserSessionsManager.js b/services/web/app/src/Features/User/UserSessionsManager.js new file mode 100644 index 0000000000..ce1abf1f0c --- /dev/null +++ b/services/web/app/src/Features/User/UserSessionsManager.js @@ -0,0 +1,247 @@ +const OError = require('@overleaf/o-error') +const Settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const Async = require('async') +const _ = require('underscore') +const { promisify } = require('util') +const UserSessionsRedis = require('./UserSessionsRedis') +const rclient = UserSessionsRedis.client() + +const UserSessionsManager = { + // mimic the key used by the express sessions + _sessionKey(sessionId) { + return `sess:${sessionId}` + }, + + trackSession(user, sessionId, callback) { + if (!user) { + return callback(null) + } + if (!sessionId) { + return callback(null) + } + const sessionSetKey = UserSessionsRedis.sessionSetKey(user) + const value = UserSessionsManager._sessionKey(sessionId) + rclient + .multi() + .sadd(sessionSetKey, value) + .pexpire(sessionSetKey, `${Settings.cookieSessionLength}`) // in milliseconds + .exec(function (err, response) { + if (err) { + OError.tag( + err, + 'error while adding session key to UserSessions set', + { + user_id: user._id, + sessionSetKey, + } + ) + return callback(err) + } + UserSessionsManager._checkSessions(user, function () {}) + callback() + }) + }, + + untrackSession(user, sessionId, callback) { + if (!callback) { + callback = function () {} + } + if (!user) { + return callback(null) + } + if (!sessionId) { + return callback(null) + } + const sessionSetKey = UserSessionsRedis.sessionSetKey(user) + const value = UserSessionsManager._sessionKey(sessionId) + rclient + .multi() + .srem(sessionSetKey, value) + .pexpire(sessionSetKey, `${Settings.cookieSessionLength}`) // in milliseconds + .exec(function (err, response) { + if (err) { + OError.tag( + err, + 'error while removing session key from UserSessions set', + { + user_id: user._id, + sessionSetKey, + } + ) + return callback(err) + } + UserSessionsManager._checkSessions(user, function () {}) + callback() + }) + }, + + getAllUserSessions(user, exclude, callback) { + exclude = _.map(exclude, UserSessionsManager._sessionKey) + const sessionSetKey = UserSessionsRedis.sessionSetKey(user) + rclient.smembers(sessionSetKey, function (err, sessionKeys) { + if (err) { + OError.tag(err, 'error getting all session keys for user from redis', { + user_id: user._id, + }) + return callback(err) + } + sessionKeys = _.filter(sessionKeys, k => !_.contains(exclude, k)) + if (sessionKeys.length === 0) { + logger.log({ user_id: user._id }, 'no other sessions found, returning') + return callback(null, []) + } + + Async.mapSeries( + sessionKeys, + (k, cb) => rclient.get(k, cb), + function (err, sessions) { + if (err) { + OError.tag(err, 'error getting all sessions for user from redis', { + user_id: user._id, + }) + return callback(err) + } + + const result = [] + for (let session of Array.from(sessions)) { + if (!session) { + continue + } + session = JSON.parse(session) + let sessionUser = session.passport && session.passport.user + if (!sessionUser) { + sessionUser = session.user + } + + result.push({ + ip_address: sessionUser.ip_address, + session_created: sessionUser.session_created, + }) + } + + callback(null, result) + } + ) + }) + }, + + revokeAllUserSessions(user, retain, callback) { + if (!retain) { + retain = [] + } + retain = retain.map(i => UserSessionsManager._sessionKey(i)) + if (!user) { + return callback(null) + } + const sessionSetKey = UserSessionsRedis.sessionSetKey(user) + rclient.smembers(sessionSetKey, function (err, sessionKeys) { + if (err) { + OError.tag(err, 'error getting contents of UserSessions set', { + user_id: user._id, + sessionSetKey, + }) + return callback(err) + } + const keysToDelete = _.filter( + sessionKeys, + k => !Array.from(retain).includes(k) + ) + if (keysToDelete.length === 0) { + logger.log( + { user_id: user._id }, + 'no sessions in UserSessions set to delete, returning' + ) + return callback(null) + } + logger.log( + { user_id: user._id, count: keysToDelete.length }, + 'deleting sessions for user' + ) + + const deletions = keysToDelete.map(k => cb => rclient.del(k, cb)) + + Async.series(deletions, function (err, _result) { + if (err) { + OError.tag(err, 'error revoking all sessions for user', { + user_id: user._id, + sessionSetKey, + }) + return callback(err) + } + rclient.srem(sessionSetKey, keysToDelete, function (err) { + if (err) { + OError.tag(err, 'error removing session set for user', { + user_id: user._id, + sessionSetKey, + }) + return callback(err) + } + callback(null) + }) + }) + }) + }, + + touch(user, callback) { + if (!user) { + return callback(null) + } + const sessionSetKey = UserSessionsRedis.sessionSetKey(user) + rclient.pexpire( + sessionSetKey, + `${Settings.cookieSessionLength}`, // in milliseconds + function (err, response) { + if (err) { + OError.tag(err, 'error while updating ttl on UserSessions set', { + user_id: user._id, + }) + return callback(err) + } + callback(null) + } + ) + }, + + _checkSessions(user, callback) { + if (!user) { + return callback(null) + } + const sessionSetKey = UserSessionsRedis.sessionSetKey(user) + rclient.smembers(sessionSetKey, function (err, sessionKeys) { + if (err) { + OError.tag(err, 'error getting contents of UserSessions set', { + user_id: user._id, + sessionSetKey, + }) + return callback(err) + } + Async.series( + sessionKeys.map(key => next => + rclient.get(key, function (err, val) { + if (err) { + return next(err) + } + if (!val) { + rclient.srem(sessionSetKey, key, function (err, result) { + return next(err) + }) + } else { + next() + } + }) + ), + function (err, results) { + callback(err) + } + ) + }) + }, +} + +UserSessionsManager.promises = { + getAllUserSessions: promisify(UserSessionsManager.getAllUserSessions), + revokeAllUserSessions: promisify(UserSessionsManager.revokeAllUserSessions), +} + +module.exports = UserSessionsManager diff --git a/services/web/app/src/Features/User/UserSessionsRedis.js b/services/web/app/src/Features/User/UserSessionsRedis.js new file mode 100644 index 0000000000..689e4148c0 --- /dev/null +++ b/services/web/app/src/Features/User/UserSessionsRedis.js @@ -0,0 +1,13 @@ +const RedisWrapper = require('../../infrastructure/RedisWrapper') +const rclient = RedisWrapper.client('websessions') + +const UserSessionsRedis = { + client() { + return rclient + }, + + sessionSetKey(user) { + return `UserSessions:{${user._id}}` + }, +} +module.exports = UserSessionsRedis diff --git a/services/web/app/src/Features/User/UserUpdater.js b/services/web/app/src/Features/User/UserUpdater.js new file mode 100644 index 0000000000..12ecf266e6 --- /dev/null +++ b/services/web/app/src/Features/User/UserUpdater.js @@ -0,0 +1,392 @@ +const logger = require('logger-sharelatex') +const OError = require('@overleaf/o-error') +const { db } = require('../../infrastructure/mongodb') +const { normalizeQuery } = require('../Helpers/Mongo') +const metrics = require('@overleaf/metrics') +const async = require('async') +const { callbackify, promisify } = require('util') +const UserGetter = require('./UserGetter') +const { + addAffiliation, + removeAffiliation, + promises: InstitutionsAPIPromises, +} = require('../Institutions/InstitutionsAPI') +const Features = require('../../infrastructure/Features') +const FeaturesUpdater = require('../Subscription/FeaturesUpdater') +const EmailHandler = require('../Email/EmailHandler') +const EmailHelper = require('../Helpers/EmailHelper') +const Errors = require('../Errors/Errors') +const NewsletterManager = require('../Newsletter/NewsletterManager') +const RecurlyWrapper = require('../Subscription/RecurlyWrapper') +const UserAuditLogHandler = require('./UserAuditLogHandler') + +async function _sendSecurityAlertPrimaryEmailChanged(userId, oldEmail, email) { + // send email to both old and new primary email + const emailOptions = { + actionDescribed: `the primary email address on your account was changed to ${email}`, + action: 'change of primary email address', + } + const toOld = Object.assign({}, emailOptions, { to: oldEmail }) + const toNew = Object.assign({}, emailOptions, { to: email }) + + try { + await EmailHandler.promises.sendEmail('securityAlert', toOld) + await EmailHandler.promises.sendEmail('securityAlert', toNew) + } catch (error) { + logger.error( + { error, userId }, + 'could not send security alert email when primary email changed' + ) + } +} + +async function addEmailAddress(userId, newEmail, affiliationOptions, auditLog) { + newEmail = EmailHelper.parseEmail(newEmail) + if (!newEmail) { + throw new Error('invalid email') + } + + await UserGetter.promises.ensureUniqueEmailAddress(newEmail) + + await UserAuditLogHandler.promises.addEntry( + userId, + 'add-email', + auditLog.initiatorId, + auditLog.ipAddress, + { + newSecondaryEmail: newEmail, + } + ) + + try { + await InstitutionsAPIPromises.addAffiliation( + userId, + newEmail, + affiliationOptions + ) + } catch (error) { + throw OError.tag(error, 'problem adding affiliation while adding email') + } + + try { + const reversedHostname = newEmail.split('@')[1].split('').reverse().join('') + const update = { + $push: { + emails: { email: newEmail, createdAt: new Date(), reversedHostname }, + }, + } + await UserUpdater.promises.updateUser(userId, update) + } catch (error) { + throw OError.tag(error, 'problem updating users emails') + } +} + +async function clearSAMLData(userId, auditLog, sendEmail) { + const user = await UserGetter.promises.getUser(userId, { + email: 1, + emails: 1, + }) + + await UserAuditLogHandler.promises.addEntry( + userId, + 'clear-institution-sso-data', + auditLog.initiatorId, + auditLog.ipAddress, + {} + ) + + const update = { + $unset: { + samlIdentifiers: 1, + 'emails.$[].samlProviderId': 1, + }, + } + await UserUpdater.promises.updateUser(userId, update) + + for (const emailData of user.emails) { + await InstitutionsAPIPromises.removeEntitlement(userId, emailData.email) + } + + await FeaturesUpdater.promises.refreshFeatures( + userId, + 'clear-institution-sso-data' + ) + + if (sendEmail) { + await EmailHandler.promises.sendEmail('SAMLDataCleared', { to: user.email }) + } +} + +async function setDefaultEmailAddress( + userId, + email, + allowUnconfirmed, + auditLog, + sendSecurityAlert +) { + email = EmailHelper.parseEmail(email) + if (email == null) { + throw new Error('invalid email') + } + + const user = await UserGetter.promises.getUser(userId, { + email: 1, + emails: 1, + }) + if (!user) { + throw new Error('invalid userId') + } + + const oldEmail = user.email + const userEmail = user.emails.find(e => e.email === email) + if (!userEmail) { + throw new Error('Default email does not belong to user') + } + if (!userEmail.confirmedAt && !allowUnconfirmed) { + throw new Errors.UnconfirmedEmailError() + } + + await UserAuditLogHandler.promises.addEntry( + userId, + 'change-primary-email', + auditLog.initiatorId, + auditLog.ipAddress, + { + newPrimaryEmail: email, + oldPrimaryEmail: oldEmail, + } + ) + + const query = { _id: userId, 'emails.email': email } + const update = { $set: { email } } + const res = await UserUpdater.promises.updateUser(query, update) + + // this should not happen + if (res.n === 0) { + throw new Error('email update error') + } + + if (sendSecurityAlert) { + // no need to wait, errors are logged and not passed back + _sendSecurityAlertPrimaryEmailChanged(userId, oldEmail, email) + } + + try { + await NewsletterManager.promises.changeEmail(user, email) + } catch (error) { + logger.warn( + { err: error, oldEmail, newEmail: email }, + 'Failed to change email in newsletter subscription' + ) + } + + try { + await RecurlyWrapper.promises.updateAccountEmailAddress(user._id, email) + } catch (error) { + // errors are ignored + } +} + +async function confirmEmail(userId, email) { + // used for initial email confirmation (non-SSO and SSO) + // also used for reconfirmation of non-SSO emails + const confirmedAt = new Date() + email = EmailHelper.parseEmail(email) + if (email == null) { + throw new Error('invalid email') + } + logger.log({ userId, email }, 'confirming user email') + + try { + await InstitutionsAPIPromises.addAffiliation(userId, email, { confirmedAt }) + } catch (error) { + throw OError.tag(error, 'problem adding affiliation while confirming email') + } + + const query = { + _id: userId, + 'emails.email': email, + } + + // only update confirmedAt if it was not previously set + const update = { + $set: { + 'emails.$.reconfirmedAt': confirmedAt, + }, + $min: { + 'emails.$.confirmedAt': confirmedAt, + }, + } + + if (Features.hasFeature('affiliations')) { + update.$unset = { + 'emails.$.affiliationUnchecked': 1, + } + } + + const res = await UserUpdater.promises.updateUser(query, update) + + if (res.n === 0) { + throw new Errors.NotFoundError('user id and email do no match') + } + await FeaturesUpdater.promises.refreshFeatures(userId, 'confirm-email') +} + +const UserUpdater = { + addAffiliationForNewUser(userId, email, affiliationOptions, callback) { + if (callback == null) { + // affiliationOptions is optional + callback = affiliationOptions + affiliationOptions = {} + } + addAffiliation(userId, email, affiliationOptions, error => { + if (error) { + return callback(error) + } + UserUpdater.updateUser( + { _id: userId, 'emails.email': email }, + { $unset: { 'emails.$.affiliationUnchecked': 1 } }, + error => { + if (error) { + callback( + OError.tag( + error, + 'could not remove affiliationUnchecked flag for user on create', + { + userId, + email, + } + ) + ) + } else { + callback() + } + } + ) + }) + }, + + updateUser(query, update, callback) { + if (callback == null) { + callback = () => {} + } + + try { + query = normalizeQuery(query) + } catch (err) { + return callback(err) + } + + db.users.updateOne(query, update, callback) + }, + + // + // DEPRECATED + // + // Change the user's main email address by adding a new email, switching the + // default email and removing the old email. Prefer manipulating multiple + // emails and the default rather than calling this method directly + // + changeEmailAddress(userId, newEmail, auditLog, callback) { + newEmail = EmailHelper.parseEmail(newEmail) + if (newEmail == null) { + return callback(new Error('invalid email')) + } + + let oldEmail = null + async.series( + [ + cb => + UserGetter.getUserEmail(userId, (error, email) => { + oldEmail = email + cb(error) + }), + cb => UserUpdater.addEmailAddress(userId, newEmail, {}, auditLog, cb), + cb => + UserUpdater.setDefaultEmailAddress( + userId, + newEmail, + true, + auditLog, + true, + cb + ), + cb => UserUpdater.removeEmailAddress(userId, oldEmail, cb), + ], + callback + ) + }, + + // Add a new email address for the user. Email cannot be already used by this + // or any other user + addEmailAddress: callbackify(addEmailAddress), + + // remove one of the user's email addresses. The email cannot be the user's + // default email address + removeEmailAddress(userId, email, callback) { + email = EmailHelper.parseEmail(email) + if (email == null) { + return callback(new Error('invalid email')) + } + removeAffiliation(userId, email, error => { + if (error != null) { + OError.tag(error, 'problem removing affiliation') + return callback(error) + } + + const query = { _id: userId, email: { $ne: email } } + const update = { $pull: { emails: { email } } } + UserUpdater.updateUser(query, update, (error, res) => { + if (error != null) { + OError.tag(error, 'problem removing users email') + return callback(error) + } + if (res.n === 0) { + return callback(new Error('Cannot remove email')) + } + FeaturesUpdater.refreshFeatures(userId, 'remove-email', callback) + }) + }) + }, + + clearSAMLData: callbackify(clearSAMLData), + + // set the default email address by setting the `email` attribute. The email + // must be one of the user's multiple emails (`emails` attribute) + setDefaultEmailAddress: callbackify(setDefaultEmailAddress), + + confirmEmail: callbackify(confirmEmail), + + removeReconfirmFlag(userId, callback) { + UserUpdater.updateUser( + userId.toString(), + { + $set: { must_reconfirm: false }, + }, + error => callback(error) + ) + }, +} +;[ + 'updateUser', + 'changeEmailAddress', + 'setDefaultEmailAddress', + 'addEmailAddress', + 'removeEmailAddress', + 'removeReconfirmFlag', +].map(method => + metrics.timeAsyncMethod(UserUpdater, method, 'mongo.UserUpdater', logger) +) + +const promises = { + addAffiliationForNewUser: promisify(UserUpdater.addAffiliationForNewUser), + addEmailAddress, + confirmEmail, + setDefaultEmailAddress, + updateUser: promisify(UserUpdater.updateUser), + removeReconfirmFlag: promisify(UserUpdater.removeReconfirmFlag), +} + +UserUpdater.promises = promises + +module.exports = UserUpdater diff --git a/services/web/app/src/Features/UserMembership/UserMembershipAuthorization.js b/services/web/app/src/Features/UserMembership/UserMembershipAuthorization.js new file mode 100644 index 0000000000..23530c3a40 --- /dev/null +++ b/services/web/app/src/Features/UserMembership/UserMembershipAuthorization.js @@ -0,0 +1,29 @@ +const UserMembershipAuthorization = { + hasStaffAccess(requiredStaffAccess) { + return req => { + if (!req.user) { + return false + } + if (req.user.isAdmin) { + return true + } + return ( + requiredStaffAccess && + req.user.staffAccess && + req.user.staffAccess[requiredStaffAccess] + ) + } + }, + + hasEntityAccess() { + return req => { + if (!req.entity) { + return false + } + return req.entity[req.entityConfig.fields.access].some(accessUserId => + accessUserId.equals(req.user._id) + ) + } + }, +} +module.exports = UserMembershipAuthorization diff --git a/services/web/app/src/Features/UserMembership/UserMembershipController.js b/services/web/app/src/Features/UserMembership/UserMembershipController.js new file mode 100644 index 0000000000..050b235509 --- /dev/null +++ b/services/web/app/src/Features/UserMembership/UserMembershipController.js @@ -0,0 +1,179 @@ +/* eslint-disable + max-len, + */ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const SessionManager = require('../Authentication/SessionManager') +const UserMembershipHandler = require('./UserMembershipHandler') +const Errors = require('../Errors/Errors') +const EmailHelper = require('../Helpers/EmailHelper') +const CSVParser = require('json2csv').Parser + +module.exports = { + index(req, res, next) { + const { entity, entityConfig } = req + return entity.fetchV1Data(function (error, entity) { + if (error != null) { + return next(error) + } + return UserMembershipHandler.getUsers( + entity, + entityConfig, + function (error, users) { + let entityName + if (error != null) { + return next(error) + } + const entityPrimaryKey = entity[ + entityConfig.fields.primaryKey + ].toString() + if (entityConfig.fields.name) { + entityName = entity[entityConfig.fields.name] + } + return res.render('user_membership/index', { + name: entityName, + users, + groupSize: entityConfig.hasMembersLimit + ? entity.membersLimit + : undefined, + translations: entityConfig.translations, + paths: entityConfig.pathsFor(entityPrimaryKey), + }) + } + ) + }) + }, + + add(req, res, next) { + const { entity, entityConfig } = req + const email = EmailHelper.parseEmail(req.body.email) + if (email == null) { + return res.status(400).json({ + error: { + code: 'invalid_email', + message: req.i18n.translate('invalid_email'), + }, + }) + } + + if (entityConfig.readOnly) { + return next(new Errors.NotFoundError('Cannot add users to entity')) + } + + return UserMembershipHandler.addUser( + entity, + entityConfig, + email, + function (error, user) { + if (error != null ? error.alreadyAdded : undefined) { + return res.status(400).json({ + error: { + code: 'user_already_added', + message: req.i18n.translate('user_already_added'), + }, + }) + } + if (error != null ? error.userNotFound : undefined) { + return res.status(404).json({ + error: { + code: 'user_not_found', + message: req.i18n.translate('user_not_found'), + }, + }) + } + if (error != null) { + return next(error) + } + return res.json({ user }) + } + ) + }, + + remove(req, res, next) { + const { entity, entityConfig } = req + const { userId } = req.params + + if (entityConfig.readOnly) { + return next(new Errors.NotFoundError('Cannot remove users from entity')) + } + + const loggedInUserId = SessionManager.getLoggedInUserId(req.session) + if (loggedInUserId === userId) { + return res.status(400).json({ + error: { + code: 'managers_cannot_remove_self', + message: req.i18n.translate('managers_cannot_remove_self'), + }, + }) + } + + return UserMembershipHandler.removeUser( + entity, + entityConfig, + userId, + function (error, user) { + if (error != null ? error.isAdmin : undefined) { + return res.status(400).json({ + error: { + code: 'managers_cannot_remove_admin', + message: req.i18n.translate('managers_cannot_remove_admin'), + }, + }) + } + if (error != null) { + return next(error) + } + return res.sendStatus(200) + } + ) + }, + + exportCsv(req, res, next) { + const { entity, entityConfig } = req + const fields = ['email', 'last_logged_in_at'] + + return UserMembershipHandler.getUsers( + entity, + entityConfig, + function (error, users) { + if (error != null) { + return next(error) + } + const csvParser = new CSVParser({ fields }) + res.header('Content-Disposition', 'attachment; filename=Group.csv') + res.contentType('text/csv') + return res.send(csvParser.parse(users)) + } + ) + }, + + new(req, res, next) { + return res.render('user_membership/new', { + entityName: req.params.name, + entityId: req.params.id, + }) + }, + + create(req, res, next) { + const entityId = req.params.id + const entityConfig = req.entityConfig + + return UserMembershipHandler.createEntity( + entityId, + entityConfig, + function (error, entity) { + if (error != null) { + return next(error) + } + return res.redirect(entityConfig.pathsFor(entityId).index) + } + ) + }, +} diff --git a/services/web/app/src/Features/UserMembership/UserMembershipEntityConfigs.js b/services/web/app/src/Features/UserMembership/UserMembershipEntityConfigs.js new file mode 100644 index 0000000000..e74b32d7ad --- /dev/null +++ b/services/web/app/src/Features/UserMembership/UserMembershipEntityConfigs.js @@ -0,0 +1,113 @@ +module.exports = { + group: { + modelName: 'Subscription', + readOnly: true, + hasMembersLimit: true, + fields: { + primaryKey: '_id', + read: ['invited_emails', 'teamInvites', 'member_ids'], + write: null, + access: 'manager_ids', + name: 'teamName', + }, + baseQuery: { + groupPlan: true, + }, + translations: { + title: 'group_account', + subtitle: 'members_management', + remove: 'remove_from_group', + }, + pathsFor(id) { + return { + addMember: `/manage/groups/${id}/invites`, + removeMember: `/manage/groups/${id}/user`, + removeInvite: `/manage/groups/${id}/invites`, + exportMembers: `/manage/groups/${id}/members/export`, + } + }, + }, + + team: { + // for metrics only + modelName: 'Subscription', + fields: { + primaryKey: 'overleaf.id', + access: 'manager_ids', + }, + baseQuery: { + groupPlan: true, + }, + }, + + groupManagers: { + modelName: 'Subscription', + fields: { + primaryKey: '_id', + read: ['manager_ids'], + write: 'manager_ids', + access: 'manager_ids', + name: 'teamName', + }, + baseQuery: { + groupPlan: true, + }, + translations: { + title: 'group_account', + subtitle: 'managers_management', + remove: 'remove_manager', + }, + pathsFor(id) { + return { + addMember: `/manage/groups/${id}/managers`, + removeMember: `/manage/groups/${id}/managers`, + } + }, + }, + + institution: { + modelName: 'Institution', + fields: { + primaryKey: 'v1Id', + read: ['managerIds'], + write: 'managerIds', + access: 'managerIds', + name: 'name', + }, + translations: { + title: 'institution_account', + subtitle: 'managers_management', + remove: 'remove_manager', + }, + pathsFor(id) { + return { + index: `/manage/institutions/${id}/managers`, + addMember: `/manage/institutions/${id}/managers`, + removeMember: `/manage/institutions/${id}/managers`, + } + }, + }, + + publisher: { + modelName: 'Publisher', + fields: { + primaryKey: 'slug', + read: ['managerIds'], + write: 'managerIds', + access: 'managerIds', + name: 'name', + }, + translations: { + title: 'publisher_account', + subtitle: 'managers_management', + remove: 'remove_manager', + }, + pathsFor(id) { + return { + index: `/manage/publishers/${id}/managers`, + addMember: `/manage/publishers/${id}/managers`, + removeMember: `/manage/publishers/${id}/managers`, + } + }, + }, +} diff --git a/services/web/app/src/Features/UserMembership/UserMembershipHandler.js b/services/web/app/src/Features/UserMembership/UserMembershipHandler.js new file mode 100644 index 0000000000..7c224dec61 --- /dev/null +++ b/services/web/app/src/Features/UserMembership/UserMembershipHandler.js @@ -0,0 +1,135 @@ +/* eslint-disable + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const { ObjectId } = require('mongodb') +const async = require('async') +const { promisifyAll } = require('../../util/promises') +const Errors = require('../Errors/Errors') +const EntityModels = { + Institution: require('../../models/Institution').Institution, + Subscription: require('../../models/Subscription').Subscription, + Publisher: require('../../models/Publisher').Publisher, +} +const UserMembershipViewModel = require('./UserMembershipViewModel') +const UserGetter = require('../User/UserGetter') +const logger = require('logger-sharelatex') +const UserMembershipEntityConfigs = require('./UserMembershipEntityConfigs') + +const UserMembershipHandler = { + getEntityWithoutAuthorizationCheck(entityId, entityConfig, callback) { + if (callback == null) { + callback = function (error, entity) {} + } + const query = buildEntityQuery(entityId, entityConfig) + return EntityModels[entityConfig.modelName].findOne(query, callback) + }, + + createEntity(entityId, entityConfig, callback) { + if (callback == null) { + callback = function (error, entity) {} + } + const data = buildEntityQuery(entityId, entityConfig) + return EntityModels[entityConfig.modelName].create(data, callback) + }, + + getUsers(entity, entityConfig, callback) { + if (callback == null) { + callback = function (error, users) {} + } + const attributes = entityConfig.fields.read + return getPopulatedListOfMembers(entity, attributes, callback) + }, + + addUser(entity, entityConfig, email, callback) { + if (callback == null) { + callback = function (error, user) {} + } + const attribute = entityConfig.fields.write + return UserGetter.getUserByAnyEmail(email, function (error, user) { + if (error != null) { + return callback(error) + } + if (!user) { + return callback({ userNotFound: true }) + } + if (entity[attribute].some(managerId => managerId.equals(user._id))) { + return callback({ alreadyAdded: true }) + } + + return addUserToEntity(entity, attribute, user, error => + callback(error, UserMembershipViewModel.build(user)) + ) + }) + }, + + removeUser(entity, entityConfig, userId, callback) { + if (callback == null) { + callback = function (error) {} + } + const attribute = entityConfig.fields.write + if (entity.admin_id != null ? entity.admin_id.equals(userId) : undefined) { + return callback({ isAdmin: true }) + } + return removeUserFromEntity(entity, attribute, userId, callback) + }, +} + +UserMembershipHandler.promises = promisifyAll(UserMembershipHandler) +module.exports = UserMembershipHandler + +var getPopulatedListOfMembers = function (entity, attributes, callback) { + if (callback == null) { + callback = function (error, users) {} + } + const userObjects = [] + + for (const attribute of Array.from(attributes)) { + for (const userObject of Array.from(entity[attribute] || [])) { + // userObject can be an email as String, a user id as ObjectId or an + // invite as Object with an email attribute as String. We want to pass to + // UserMembershipViewModel either an email as (String) or a user id (ObjectId) + const userIdOrEmail = userObject.email || userObject + userObjects.push(userIdOrEmail) + } + } + + return async.map(userObjects, UserMembershipViewModel.buildAsync, callback) +} + +var addUserToEntity = function (entity, attribute, user, callback) { + if (callback == null) { + callback = function (error) {} + } + const fieldUpdate = {} + fieldUpdate[attribute] = user._id + return entity.updateOne({ $addToSet: fieldUpdate }, callback) +} + +var removeUserFromEntity = function (entity, attribute, userId, callback) { + if (callback == null) { + callback = function (error) {} + } + const fieldUpdate = {} + fieldUpdate[attribute] = userId + return entity.updateOne({ $pull: fieldUpdate }, callback) +} + +var buildEntityQuery = function (entityId, entityConfig, loggedInUser) { + if (ObjectId.isValid(entityId.toString())) { + entityId = ObjectId(entityId) + } + const query = Object.assign({}, entityConfig.baseQuery) + query[entityConfig.fields.primaryKey] = entityId + return query +} diff --git a/services/web/app/src/Features/UserMembership/UserMembershipMiddleware.js b/services/web/app/src/Features/UserMembership/UserMembershipMiddleware.js new file mode 100644 index 0000000000..76f9e8285b --- /dev/null +++ b/services/web/app/src/Features/UserMembership/UserMembershipMiddleware.js @@ -0,0 +1,291 @@ +const { expressify } = require('../../util/promises') +const async = require('async') +const UserMembershipAuthorization = require('./UserMembershipAuthorization') +const AuthenticationController = require('../Authentication/AuthenticationController') +const UserMembershipHandler = require('./UserMembershipHandler') +const EntityConfigs = require('./UserMembershipEntityConfigs') +const Errors = require('../Errors/Errors') +const HttpErrorHandler = require('../Errors/HttpErrorHandler') +const TemplatesManager = require('../Templates/TemplatesManager') + +// set of middleware arrays or functions that checks user access to an entity +// (publisher, institution, group, template, etc.) +const UserMembershipMiddleware = { + requireTeamMetricsAccess: [ + AuthenticationController.requireLogin(), + fetchEntityConfig('team'), + fetchEntity(), + requireEntity(), + allowAccessIfAny([ + UserMembershipAuthorization.hasEntityAccess(), + UserMembershipAuthorization.hasStaffAccess('groupMetrics'), + ]), + ], + + requireGroupManagementAccess: [ + AuthenticationController.requireLogin(), + fetchEntityConfig('group'), + fetchEntity(), + requireEntity(), + allowAccessIfAny([ + UserMembershipAuthorization.hasEntityAccess(), + UserMembershipAuthorization.hasStaffAccess('groupManagement'), + ]), + ], + + requireGroupMetricsAccess: [ + AuthenticationController.requireLogin(), + fetchEntityConfig('group'), + fetchEntity(), + requireEntity(), + allowAccessIfAny([ + UserMembershipAuthorization.hasEntityAccess(), + UserMembershipAuthorization.hasStaffAccess('groupMetrics'), + ]), + ], + + requireGroupManagersManagementAccess: [ + AuthenticationController.requireLogin(), + fetchEntityConfig('groupManagers'), + fetchEntity(), + requireEntity(), + allowAccessIfAny([ + UserMembershipAuthorization.hasEntityAccess(), + UserMembershipAuthorization.hasStaffAccess('groupManagement'), + ]), + ], + + requireInstitutionMetricsAccess: [ + AuthenticationController.requireLogin(), + fetchEntityConfig('institution'), + fetchEntity(), + requireEntityOrCreate('institutionManagement'), + allowAccessIfAny([ + UserMembershipAuthorization.hasEntityAccess(), + UserMembershipAuthorization.hasStaffAccess('institutionMetrics'), + ]), + ], + + requireInstitutionManagementAccess: [ + AuthenticationController.requireLogin(), + fetchEntityConfig('institution'), + fetchEntity(), + requireEntityOrCreate('institutionManagement'), + allowAccessIfAny([ + UserMembershipAuthorization.hasEntityAccess(), + UserMembershipAuthorization.hasStaffAccess('institutionManagement'), + ]), + ], + + requireInstitutionManagementStaffAccess: [ + AuthenticationController.requireLogin(), + allowAccessIfAny([ + UserMembershipAuthorization.hasStaffAccess('institutionManagement'), + ]), + fetchEntityConfig('institution'), + fetchEntity(), + requireEntityOrCreate('institutionManagement'), + ], + + requirePublisherMetricsAccess: [ + AuthenticationController.requireLogin(), + fetchEntityConfig('publisher'), + fetchEntity(), + requireEntityOrCreate('publisherManagement'), + allowAccessIfAny([ + UserMembershipAuthorization.hasEntityAccess(), + UserMembershipAuthorization.hasStaffAccess('publisherMetrics'), + ]), + ], + + requirePublisherManagementAccess: [ + AuthenticationController.requireLogin(), + fetchEntityConfig('publisher'), + fetchEntity(), + requireEntityOrCreate('publisherManagement'), + allowAccessIfAny([ + UserMembershipAuthorization.hasEntityAccess(), + UserMembershipAuthorization.hasStaffAccess('publisherManagement'), + ]), + ], + + requireConversionMetricsAccess: [ + AuthenticationController.requireLogin(), + fetchEntityConfig('publisher'), + fetchEntity(), + requireEntityOrCreate('publisherManagement'), + allowAccessIfAny([ + UserMembershipAuthorization.hasEntityAccess(), + UserMembershipAuthorization.hasStaffAccess('publisherMetrics'), + ]), + ], + + requireAdminMetricsAccess: [ + AuthenticationController.requireLogin(), + allowAccessIfAny([ + UserMembershipAuthorization.hasStaffAccess('adminMetrics'), + ]), + ], + + requireTemplateMetricsAccess: [ + AuthenticationController.requireLogin(), + fetchV1Template(), + requireV1Template(), + fetchEntityConfig('publisher'), + fetchPublisherFromTemplate(), + allowAccessIfAny([ + UserMembershipAuthorization.hasEntityAccess(), + UserMembershipAuthorization.hasStaffAccess('publisherMetrics'), + ]), + ], + + requirePublisherCreationAccess: [ + AuthenticationController.requireLogin(), + allowAccessIfAny([ + UserMembershipAuthorization.hasStaffAccess('publisherManagement'), + ]), + fetchEntityConfig('publisher'), + ], + + requireInstitutionCreationAccess: [ + AuthenticationController.requireLogin(), + allowAccessIfAny([ + UserMembershipAuthorization.hasStaffAccess('institutionManagement'), + ]), + fetchEntityConfig('institution'), + ], + + // graphs access is an edge-case: + // - the entity id is in `req.query.resource_id`. It must be set as + // `req.params.id` + // - the entity name is in `req.query.resource_type` and is used to find the + // require middleware depending on the entity name + requireGraphAccess(req, res, next) { + req.params.id = req.query.resource_id + let entityName = req.query.resource_type + if (!entityName) { + return HttpErrorHandler.notFound(req, res, 'resource_type param missing') + } + entityName = entityName.charAt(0).toUpperCase() + entityName.slice(1) + + const middleware = + UserMembershipMiddleware[`require${entityName}MetricsAccess`] + if (!middleware) { + return HttpErrorHandler.notFound( + req, + res, + `incorrect entity name: ${entityName}` + ) + } + // run the list of middleware functions in series. This is essencially + // a poor man's middleware runner + async.eachSeries(middleware, (fn, callback) => fn(req, res, callback), next) + }, +} + +module.exports = UserMembershipMiddleware + +// fetch entity config and set it in the request +function fetchEntityConfig(entityName) { + return (req, res, next) => { + const entityConfig = EntityConfigs[entityName] + req.entityName = entityName + req.entityConfig = entityConfig + next() + } +} + +// fetch the entity with id and config, and set it in the request +function fetchEntity() { + return expressify(async (req, res, next) => { + const entity = await UserMembershipHandler.promises.getEntityWithoutAuthorizationCheck( + req.params.id, + req.entityConfig + ) + req.entity = entity + next() + }) +} + +function fetchPublisherFromTemplate() { + return (req, res, next) => { + if (req.template.brand.slug) { + // set the id as the publisher's id as it's the entity used for access + // control + req.params.id = req.template.brand.slug + return fetchEntity()(req, res, next) + } else { + return next() + } + } +} + +// ensure an entity was found, or fail with 404 +function requireEntity() { + return (req, res, next) => { + if (req.entity) { + return next() + } + + throw new Errors.NotFoundError( + `no '${req.entityName}' entity with '${req.params.id}'` + ) + } +} + +// ensure an entity was found or redirect to entity creation page if the user +// has permissions to create the entity, or fail with 404 +function requireEntityOrCreate(creationStaffAccess) { + return (req, res, next) => { + if (req.entity) { + return next() + } + + if (UserMembershipAuthorization.hasStaffAccess(creationStaffAccess)(req)) { + res.redirect(`/entities/${req.entityName}/create/${req.params.id}`) + return + } + + throw new Errors.NotFoundError( + `no '${req.entityName}' entity with '${req.params.id}'` + ) + } +} + +// fetch the template from v1, and set it in the request +function fetchV1Template() { + return expressify(async (req, res, next) => { + const templateId = req.params.id + const body = await TemplatesManager.promises.fetchFromV1(templateId) + req.template = { + id: body.id, + title: body.title, + brand: body.brand, + } + next() + }) +} + +// ensure a template was found, or fail with 404 +function requireV1Template() { + return (req, res, next) => { + if (req.template.id) { + return next() + } + + throw new Errors.NotFoundError('no template found') + } +} + +// run a serie of synchronous access functions and call `next` if any of the +// retur values is truly. Redirect to restricted otherwise +function allowAccessIfAny(accessFunctions) { + return (req, res, next) => { + for (const accessFunction of accessFunctions) { + if (accessFunction(req)) { + return next() + } + } + HttpErrorHandler.forbidden(req, res) + } +} diff --git a/services/web/app/src/Features/UserMembership/UserMembershipRouter.js b/services/web/app/src/Features/UserMembership/UserMembershipRouter.js new file mode 100644 index 0000000000..f5492fe0ee --- /dev/null +++ b/services/web/app/src/Features/UserMembership/UserMembershipRouter.js @@ -0,0 +1,128 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const UserMembershipMiddleware = require('./UserMembershipMiddleware') +const UserMembershipController = require('./UserMembershipController') +const SubscriptionGroupController = require('../Subscription/SubscriptionGroupController') +const TeamInvitesController = require('../Subscription/TeamInvitesController') +const RateLimiterMiddleware = require('../Security/RateLimiterMiddleware') + +module.exports = { + apply(webRouter) { + // group members routes + webRouter.get( + '/manage/groups/:id/members', + UserMembershipMiddleware.requireGroupManagementAccess, + UserMembershipController.index + ) + webRouter.post( + '/manage/groups/:id/invites', + UserMembershipMiddleware.requireGroupManagementAccess, + RateLimiterMiddleware.rateLimit({ + endpointName: 'create-team-invite', + maxRequests: 200, + timeInterval: 60, + }), + TeamInvitesController.createInvite + ) + webRouter.delete( + '/manage/groups/:id/user/:user_id', + UserMembershipMiddleware.requireGroupManagementAccess, + SubscriptionGroupController.removeUserFromGroup + ) + webRouter.delete( + '/manage/groups/:id/invites/:email', + UserMembershipMiddleware.requireGroupManagementAccess, + TeamInvitesController.revokeInvite + ) + webRouter.get( + '/manage/groups/:id/members/export', + UserMembershipMiddleware.requireGroupManagementAccess, + RateLimiterMiddleware.rateLimit({ + endpointName: 'export-team-csv', + maxRequests: 30, + timeInterval: 60, + }), + UserMembershipController.exportCsv + ) + + // group managers routes + webRouter.get( + '/manage/groups/:id/managers', + UserMembershipMiddleware.requireGroupManagersManagementAccess, + UserMembershipController.index + ) + webRouter.post( + '/manage/groups/:id/managers', + UserMembershipMiddleware.requireGroupManagersManagementAccess, + UserMembershipController.add + ) + webRouter.delete( + '/manage/groups/:id/managers/:userId', + UserMembershipMiddleware.requireGroupManagersManagementAccess, + UserMembershipController.remove + ) + + // institution members routes + webRouter.get( + '/manage/institutions/:id/managers', + UserMembershipMiddleware.requireInstitutionManagementAccess, + UserMembershipController.index + ) + webRouter.post( + '/manage/institutions/:id/managers', + UserMembershipMiddleware.requireInstitutionManagementAccess, + UserMembershipController.add + ) + webRouter.delete( + '/manage/institutions/:id/managers/:userId', + UserMembershipMiddleware.requireInstitutionManagementAccess, + UserMembershipController.remove + ) + + // publisher members routes + webRouter.get( + '/manage/publishers/:id/managers', + UserMembershipMiddleware.requirePublisherManagementAccess, + UserMembershipController.index + ) + webRouter.post( + '/manage/publishers/:id/managers', + UserMembershipMiddleware.requirePublisherManagementAccess, + UserMembershipController.add + ) + webRouter.delete( + '/manage/publishers/:id/managers/:userId', + UserMembershipMiddleware.requirePublisherManagementAccess, + UserMembershipController.remove + ) + + // publisher creation routes + webRouter.get( + '/entities/publisher/create/:id', + UserMembershipMiddleware.requirePublisherCreationAccess, + UserMembershipController.new + ) + webRouter.post( + '/entities/publisher/create/:id', + UserMembershipMiddleware.requirePublisherCreationAccess, + UserMembershipController.create + ) + + // institution creation routes + webRouter.get( + '/entities/institution/create/:id', + UserMembershipMiddleware.requireInstitutionCreationAccess, + UserMembershipController.new + ) + webRouter.post( + '/entities/institution/create/:id', + UserMembershipMiddleware.requireInstitutionCreationAccess, + UserMembershipController.create + ) + }, +} diff --git a/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js b/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js new file mode 100644 index 0000000000..1af1637777 --- /dev/null +++ b/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js @@ -0,0 +1,67 @@ +/* eslint-disable + node/handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let UserMembershipViewModel +const UserGetter = require('../User/UserGetter') +const { isObjectIdInstance } = require('../Helpers/Mongo') + +module.exports = UserMembershipViewModel = { + build(userOrEmail) { + if (userOrEmail._id) { + return buildUserViewModel(userOrEmail) + } else { + return buildUserViewModelWithEmail(userOrEmail) + } + }, + + buildAsync(userOrIdOrEmail, callback) { + if (callback == null) { + callback = function (error, viewModel) {} + } + if (!isObjectIdInstance(userOrIdOrEmail)) { + // userOrIdOrEmail is a user or an email and can be parsed by #build + return callback(null, UserMembershipViewModel.build(userOrIdOrEmail)) + } + + const userId = userOrIdOrEmail + const projection = { + email: 1, + first_name: 1, + last_name: 1, + lastLoggedIn: 1, + } + return UserGetter.getUser(userId, projection, function (error, user) { + if (error != null || user == null) { + return callback(null, buildUserViewModelWithId(userId.toString())) + } + return callback(null, buildUserViewModel(user)) + }) + }, +} + +var buildUserViewModel = function (user, isInvite) { + if (isInvite == null) { + isInvite = false + } + return { + _id: user._id || null, + email: user.email || null, + first_name: user.first_name || null, + last_name: user.last_name || null, + last_logged_in_at: user.lastLoggedIn || null, + invite: isInvite, + } +} + +var buildUserViewModelWithEmail = email => buildUserViewModel({ email }, true) + +var buildUserViewModelWithId = id => buildUserViewModel({ _id: id }, false) diff --git a/services/web/app/src/Features/UserMembership/UserMembershipsHandler.js b/services/web/app/src/Features/UserMembership/UserMembershipsHandler.js new file mode 100644 index 0000000000..004d8acdac --- /dev/null +++ b/services/web/app/src/Features/UserMembership/UserMembershipsHandler.js @@ -0,0 +1,88 @@ +/* eslint-disable + node/handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const async = require('async') +const { promisifyAll } = require('../../util/promises') +const EntityModels = { + Institution: require('../../models/Institution').Institution, + Subscription: require('../../models/Subscription').Subscription, + Publisher: require('../../models/Publisher').Publisher, +} +const UserMembershipEntityConfigs = require('./UserMembershipEntityConfigs') + +const UserMembershipsHandler = { + removeUserFromAllEntities(userId, callback) { + // get all writable entity types + if (callback == null) { + callback = function (error) {} + } + const entityConfigs = [] + for (const key in UserMembershipEntityConfigs) { + const entityConfig = UserMembershipEntityConfigs[key] + if (entityConfig.fields && entityConfig.fields.write != null) { + entityConfigs.push(entityConfig) + } + } + + // remove the user from all entities types + return async.map( + entityConfigs, + (entityConfig, innerCallback) => + UserMembershipsHandler.removeUserFromEntities( + entityConfig, + userId, + innerCallback + ), + callback + ) + }, + + removeUserFromEntities(entityConfig, userId, callback) { + if (callback == null) { + callback = function (error) {} + } + const removeOperation = { $pull: {} } + removeOperation.$pull[entityConfig.fields.write] = userId + return EntityModels[entityConfig.modelName].updateMany( + {}, + removeOperation, + callback + ) + }, + + getEntitiesByUser(entityConfig, userId, callback) { + if (callback == null) { + callback = function (error, entities) {} + } + const query = Object.assign({}, entityConfig.baseQuery) + query[entityConfig.fields.access] = userId + return EntityModels[entityConfig.modelName].find( + query, + function (error, entities) { + if (entities == null) { + entities = [] + } + if (error != null) { + return callback(error) + } + return async.mapSeries( + entities, + (entity, cb) => entity.fetchV1Data(cb), + callback + ) + } + ) + }, +} + +UserMembershipsHandler.promises = promisifyAll(UserMembershipsHandler) +module.exports = UserMembershipsHandler diff --git a/services/web/app/src/Features/V1/V1Api.js b/services/web/app/src/Features/V1/V1Api.js new file mode 100644 index 0000000000..df86d22f3d --- /dev/null +++ b/services/web/app/src/Features/V1/V1Api.js @@ -0,0 +1,109 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const request = require('request') +const settings = require('@overleaf/settings') +const Errors = require('../Errors/Errors') +const { promisifyAll } = require('../../util/promises') + +// TODO: check what happens when these settings aren't defined +const DEFAULT_V1_PARAMS = { + baseUrl: settings.apis.v1.url, + auth: { + user: settings.apis.v1.user, + pass: settings.apis.v1.pass, + }, + json: true, + timeout: 30 * 1000, +} + +const v1Request = request.defaults(DEFAULT_V1_PARAMS) + +const DEFAULT_V1_OAUTH_PARAMS = { + baseUrl: settings.apis.v1.url, + json: true, + timeout: 30 * 1000, +} + +const v1OauthRequest = request.defaults(DEFAULT_V1_OAUTH_PARAMS) + +const V1Api = { + request(options, callback) { + if (callback == null) { + return request(options) + } + return v1Request(options, (error, response, body) => + V1Api._responseHandler(options, error, response, body, callback) + ) + }, + + oauthRequest(options, token, callback) { + if (options.uri == null) { + return callback(new Error('uri required')) + } + if (options.method == null) { + options.method = 'GET' + } + options.auth = { bearer: token } + return v1OauthRequest(options, (error, response, body) => + V1Api._responseHandler(options, error, response, body, callback) + ) + }, + + _responseHandler(options, error, response, body, callback) { + if (error != null) { + return callback( + new Errors.V1ConnectionError('error from V1 API').withCause(error) + ) + } + if (response && response.statusCode >= 500) { + return callback( + new Errors.V1ConnectionError({ + message: 'error from V1 API', + info: { status: response.statusCode, body: body }, + }) + ) + } + if ( + (response.statusCode >= 200 && response.statusCode < 300) || + Array.from(options.expectedStatusCodes || []).includes( + response.statusCode + ) + ) { + return callback(null, response, body) + } else if (response.statusCode === 403) { + error = new Errors.ForbiddenError('overleaf v1 returned forbidden') + error.statusCode = response.statusCode + return callback(error) + } else if (response.statusCode === 404) { + error = new Errors.NotFoundError( + `overleaf v1 returned non-success code: ${response.statusCode} ${options.method} ${options.uri}` + ) + error.statusCode = response.statusCode + return callback(error) + } else { + error = new Error( + `overleaf v1 returned non-success code: ${response.statusCode} ${options.method} ${options.uri}` + ) + error.statusCode = response.statusCode + return callback(error) + } + }, +} + +V1Api.promises = promisifyAll(V1Api, { + multiResult: { + request: ['response', 'body'], + oauthRequest: ['response', 'body'], + }, +}) +module.exports = V1Api diff --git a/services/web/app/src/Features/V1/V1Handler.js b/services/web/app/src/Features/V1/V1Handler.js new file mode 100644 index 0000000000..e5e33f51dd --- /dev/null +++ b/services/web/app/src/Features/V1/V1Handler.js @@ -0,0 +1,109 @@ +/* eslint-disable + camelcase, + node/handle-callback-err, + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let V1Handler +const OError = require('@overleaf/o-error') +const V1Api = require('./V1Api') +const Settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') + +module.exports = V1Handler = { + authWithV1(email, password, callback) { + if (callback == null) { + callback = function (err, isValid, v1Profile) {} + } + return V1Api.request( + { + method: 'POST', + url: '/api/v1/sharelatex/login', + json: { email, password }, + expectedStatusCodes: [403], + }, + function (err, response, body) { + if (err != null) { + OError.tag(err, '[V1Handler] error while talking to v1 login api', { + email, + }) + return callback(err) + } + if ([200, 403].includes(response.statusCode)) { + const isValid = body.valid + const userProfile = body.user_profile + logger.log( + { + email, + isValid, + v1UserId: __guard__( + body != null ? body.user_profile : undefined, + x => x.id + ), + }, + '[V1Handler] got response from v1 login api' + ) + return callback(null, isValid, userProfile) + } else { + err = new Error( + `Unexpected status from v1 login api: ${response.statusCode}` + ) + return callback(err) + } + } + ) + }, + + doPasswordReset(v1_user_id, password, callback) { + if (callback == null) { + callback = function (err, created) {} + } + + return V1Api.request( + { + method: 'POST', + url: '/api/v1/sharelatex/reset_password', + json: { + user_id: v1_user_id, + password, + }, + expectedStatusCodes: [200], + }, + function (err, response, body) { + if (err != null) { + OError.tag(err, 'error while talking to v1 password reset api', { + v1_user_id, + }) + return callback(err, false) + } + if ([200].includes(response.statusCode)) { + logger.log( + { v1_user_id, changed: true }, + 'got success response from v1 password reset api' + ) + return callback(null, true) + } else { + err = new Error( + `Unexpected status from v1 password reset api: ${response.statusCode}` + ) + return callback(err, false) + } + } + ) + }, +} + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/app/src/infrastructure/BodyParserWrapper.js b/services/web/app/src/infrastructure/BodyParserWrapper.js new file mode 100644 index 0000000000..def1148ceb --- /dev/null +++ b/services/web/app/src/infrastructure/BodyParserWrapper.js @@ -0,0 +1,35 @@ +const bodyParser = require('body-parser') +const HttpErrorHandler = require('../Features/Errors/HttpErrorHandler') + +function isBodyParserError(nextArg) { + if (nextArg instanceof Error) { + return ( + nextArg.statusCode && + nextArg.statusCode >= 400 && + nextArg.statusCode < 600 + ) + } + return false +} + +const wrapBodyParser = method => opts => { + const middleware = bodyParser[method](opts) + return (req, res, next) => { + middleware(req, res, nextArg => { + if (isBodyParserError(nextArg)) { + return HttpErrorHandler.handleErrorByStatusCode( + req, + res, + nextArg, + nextArg.statusCode + ) + } + next(nextArg) + }) + } +} + +module.exports = { + urlencoded: wrapBodyParser('urlencoded'), + json: wrapBodyParser('json'), +} diff --git a/services/web/app/src/infrastructure/CSP.js b/services/web/app/src/infrastructure/CSP.js new file mode 100644 index 0000000000..284a782f90 --- /dev/null +++ b/services/web/app/src/infrastructure/CSP.js @@ -0,0 +1,66 @@ +const crypto = require('crypto') +const path = require('path') + +module.exports = function ({ + reportUri, + reportPercentage, + reportOnly = false, + exclude = [], + percentage, +}) { + return function (req, res, next) { + const originalRender = res.render + + res.render = (...args) => { + const view = relativeViewPath(args[0]) + + // enable the CSP header for a percentage of requests + const belowCutoff = Math.random() * 100 <= percentage + + if (belowCutoff && !exclude.includes(view)) { + res.locals.cspEnabled = true + + const scriptNonce = crypto.randomBytes(16).toString('base64') + + res.locals.scriptNonce = scriptNonce + + const directives = [ + `script-src 'nonce-${scriptNonce}' 'unsafe-inline' 'strict-dynamic' https: 'report-sample'`, + `object-src 'none'`, + `base-uri 'none'`, + ] + + // enable the report URI for a percentage of CSP-enabled requests + const belowReportCutoff = Math.random() * 100 <= reportPercentage + + if (reportUri && belowReportCutoff) { + directives.push(`report-uri ${reportUri}`) + // NOTE: implement report-to once it's more widely supported + } + + const policy = directives.join('; ') + + // Note: https://csp-evaluator.withgoogle.com/ is useful for checking the policy + + const header = reportOnly + ? 'Content-Security-Policy-Report-Only' + : 'Content-Security-Policy' + + res.set(header, policy) + } + + originalRender.apply(res, args) + } + + next() + } +} + +const webRoot = path.resolve(__dirname, '..', '..', '..') + +// build the view path relative to the web root +function relativeViewPath(view) { + return path.isAbsolute(view) + ? path.relative(webRoot, view) + : path.join('app', 'views', view) +} diff --git a/services/web/app/src/infrastructure/Csrf.js b/services/web/app/src/infrastructure/Csrf.js new file mode 100644 index 0000000000..8d83a58328 --- /dev/null +++ b/services/web/app/src/infrastructure/Csrf.js @@ -0,0 +1,99 @@ +/* eslint-disable + max-len, + no-return-assign, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ + +const csurf = require('csurf') +const csrf = csurf() +const { promisify } = require('util') + +// Wrapper for `csurf` middleware that provides a list of routes that can be excluded from csrf checks. +// +// Include with `Csrf = require('./Csrf')` +// +// Add the middleware to the router with: +// myRouter.csrf = new Csrf() +// myRouter.use webRouter.csrf.middleware +// When building routes, specify a route to exclude from csrf checks with: +// myRouter.csrf.disableDefaultCsrfProtection "/path" "METHOD" +// +// To validate the csrf token in a request to ensure that it's valid, you can use `validateRequest`, which takes a +// request object and calls a callback with an error if invalid. + +class Csrf { + constructor() { + this.middleware = this.middleware.bind(this) + this.excluded_routes = {} + } + + disableDefaultCsrfProtection(route, method) { + if (!this.excluded_routes[route]) { + this.excluded_routes[route] = {} + } + return (this.excluded_routes[route][method] = 1) + } + + middleware(req, res, next) { + // We want to call the middleware for all routes, even if excluded, because csurf sets up a csrfToken() method on + // the request, to get a new csrf token for any rendered forms. For excluded routes we'll then ignore a 'bad csrf + // token' error from csurf and continue on... + + // check whether the request method is excluded for the specified route + if ( + (this.excluded_routes[req.path] != null + ? this.excluded_routes[req.path][req.method] + : undefined) === 1 + ) { + // ignore the error if it's due to a bad csrf token, and continue + return csrf(req, res, err => { + if (err && err.code !== 'EBADCSRFTOKEN') { + return next(err) + } else { + return next() + } + }) + } else { + return csrf(req, res, next) + } + } + + static validateRequest(req, cb) { + // run a dummy csrf check to see if it returns an error + if (cb == null) { + cb = function (valid) {} + } + return csrf(req, null, err => cb(err)) + } + + static validateToken(token, session, cb) { + if (token == null) { + return cb(new Error('missing token')) + } + // run a dummy csrf check to see if it returns an error + // use this to simulate a csrf check regardless of req method, headers &c. + const req = { + body: { + _csrf: token, + }, + headers: {}, + method: 'POST', + session, + } + return Csrf.validateRequest(req, cb) + } +} + +Csrf.promises = { + validateRequest: promisify(Csrf.validateRequest), +} + +module.exports = Csrf diff --git a/services/web/app/src/infrastructure/ExpressLocals.js b/services/web/app/src/infrastructure/ExpressLocals.js new file mode 100644 index 0000000000..96225241b7 --- /dev/null +++ b/services/web/app/src/infrastructure/ExpressLocals.js @@ -0,0 +1,371 @@ +const logger = require('logger-sharelatex') +const Settings = require('@overleaf/settings') +const querystring = require('querystring') +const _ = require('lodash') +const Url = require('url') +const Path = require('path') +const moment = require('moment') +const pug = require('pug-runtime') + +const IS_DEV_ENV = ['development', 'test'].includes(process.env.NODE_ENV) + +const Features = require('./Features') +const SessionManager = require('../Features/Authentication/SessionManager') +const PackageVersions = require('./PackageVersions') +const Modules = require('./Modules') +const SafeHTMLSubstitute = require('../Features/Helpers/SafeHTMLSubstitution') + +let webpackManifest +if (!IS_DEV_ENV) { + // Only load webpack manifest file in production. In dev, the web and webpack + // containers can't coordinate, so there no guarantee that the manifest file + // exists when the web server boots. We therefore ignore the manifest file in + // dev reload + webpackManifest = require(`../../../public/manifest.json`) +} + +const I18N_HTML_INJECTIONS = new Set() + +module.exports = function (webRouter, privateApiRouter, publicApiRouter) { + webRouter.use(function (req, res, next) { + res.locals.session = req.session + next() + }) + + function addSetContentDisposition(req, res, next) { + res.setContentDisposition = function (type, opts) { + const directives = _.map( + opts, + (v, k) => `${k}="${encodeURIComponent(v)}"` + ) + const contentDispositionValue = `${type}; ${directives.join('; ')}` + res.setHeader('Content-Disposition', contentDispositionValue) + } + next() + } + webRouter.use(addSetContentDisposition) + privateApiRouter.use(addSetContentDisposition) + publicApiRouter.use(addSetContentDisposition) + + webRouter.use(function (req, res, next) { + req.externalAuthenticationSystemUsed = + Features.externalAuthenticationSystemUsed + res.locals.externalAuthenticationSystemUsed = + Features.externalAuthenticationSystemUsed + req.hasFeature = res.locals.hasFeature = Features.hasFeature + next() + }) + + webRouter.use(function (req, res, next) { + let staticFilesBase + + const cdnAvailable = + Settings.cdn && Settings.cdn.web && !!Settings.cdn.web.host + const cdnBlocked = req.query.nocdn === 'true' || req.session.cdnBlocked + const userId = SessionManager.getLoggedInUserId(req.session) + if (cdnBlocked && req.session.cdnBlocked == null) { + logger.log( + { user_id: userId, ip: req != null ? req.ip : undefined }, + 'cdnBlocked for user, not using it and turning it off for future requets' + ) + req.session.cdnBlocked = true + } + const host = req.headers && req.headers.host + const isSmoke = host.slice(0, 5).toLowerCase() === 'smoke' + if (cdnAvailable && !isSmoke && !cdnBlocked) { + staticFilesBase = Settings.cdn.web.host + } else { + staticFilesBase = '' + } + + res.locals.buildBaseAssetPath = function () { + // Return the base asset path (including the CDN url) so that webpack can + // use this to dynamically fetch scripts (e.g. PDFjs worker) + return Url.resolve(staticFilesBase, '/') + } + + res.locals.buildJsPath = function (jsFile) { + let path + if (IS_DEV_ENV) { + // In dev: resolve path within JS asset directory + // We are *not* guaranteed to have a manifest file when the server + // starts up + path = Path.join('/js', jsFile) + } else { + // In production: resolve path from webpack manifest file + // We are guaranteed to have a manifest file since webpack compiles in + // the build + path = `/${webpackManifest[jsFile]}` + } + + return Url.resolve(staticFilesBase, path) + } + + // Temporary hack while jQuery/Angular dependencies are *not* bundled, + // instead copied into output directory + res.locals.buildCopiedJsAssetPath = function (jsFile) { + let path + if (IS_DEV_ENV) { + // In dev: resolve path to root directory + // We are *not* guaranteed to have a manifest file when the server + // starts up + path = Path.join('/', jsFile) + } else { + // In production: resolve path from webpack manifest file + // We are guaranteed to have a manifest file since webpack compiles in + // the build + path = `/${webpackManifest[jsFile]}` + } + + return Url.resolve(staticFilesBase, path) + } + + res.locals.mathJaxPath = `/js/libs/mathjax/MathJax.js?${querystring.stringify( + { + config: 'TeX-AMS_HTML,Safe', + v: require('mathjax/package.json').version, + } + )}` + + res.locals.lib = PackageVersions.lib + + res.locals.moment = moment + + const IEEE_BRAND_ID = 15 + res.locals.isIEEE = brandVariation => + (brandVariation != null ? brandVariation.brand_id : undefined) === + IEEE_BRAND_ID + + res.locals.getCssThemeModifier = function (userSettings, brandVariation) { + // Themes only exist in OL v2 + if (Settings.overleaf != null) { + // The IEEE theme takes precedence over the user personal setting, i.e. a user with + // a theme setting of "light" will still get the IEE theme in IEEE branded projects. + if (res.locals.isIEEE(brandVariation)) { + return 'ieee-' + } else if (userSettings && userSettings.overallTheme != null) { + return userSettings.overallTheme + } + } + } + + res.locals.buildStylesheetPath = function (cssFileName) { + let path + if (IS_DEV_ENV) { + // In dev: resolve path within CSS asset directory + // We are *not* guaranteed to have a manifest file when the server + // starts up + path = Path.join('/stylesheets/', cssFileName) + } else { + // In production: resolve path from webpack manifest file + // We are guaranteed to have a manifest file since webpack compiles in + // the build + path = `/${webpackManifest[cssFileName]}` + } + + return Url.resolve(staticFilesBase, path) + } + + res.locals.buildCssPath = function (themeModifier = '') { + return res.locals.buildStylesheetPath(`${themeModifier}style.css`) + } + + res.locals.buildImgPath = function (imgFile) { + const path = Path.join('/img/', imgFile) + return Url.resolve(staticFilesBase, path) + } + + next() + }) + + webRouter.use(function (req, res, next) { + res.locals.translate = function (key, vars, components) { + vars = vars || {} + + if (Settings.i18n.checkForHTMLInVars) { + Object.entries(vars).forEach(([field, value]) => { + if (pug.escape(value) !== value) { + const violationsKey = key + field + // do not flood the logs, log one sample per pod + key + field + if (!I18N_HTML_INJECTIONS.has(violationsKey)) { + logger.warn( + { key, field, value }, + 'html content in translations context vars' + ) + I18N_HTML_INJECTIONS.add(violationsKey) + } + } + }) + } + + vars.appName = Settings.appName + const locale = req.i18n.translate(key, vars) + if (components) { + return SafeHTMLSubstitute.render(locale, components) + } else { + return locale + } + } + // Don't include the query string parameters, otherwise Google + // treats ?nocdn=true as the canonical version + const parsedOriginalUrl = Url.parse(req.originalUrl) + res.locals.currentUrl = parsedOriginalUrl.pathname + res.locals.currentUrlWithQueryParams = parsedOriginalUrl.path + res.locals.capitalize = function (string) { + if (string.length === 0) { + return '' + } + return string.charAt(0).toUpperCase() + string.slice(1) + } + next() + }) + + webRouter.use(function (req, res, next) { + res.locals.getUserEmail = function () { + const user = SessionManager.getSessionUser(req.session) + const email = (user != null ? user.email : undefined) || '' + return email + } + next() + }) + + webRouter.use(function (req, res, next) { + res.locals.StringHelper = require('../Features/Helpers/StringHelper') + next() + }) + + webRouter.use(function (req, res, next) { + res.locals.buildReferalUrl = function (referalMedium) { + let url = Settings.siteUrl + const currentUser = SessionManager.getSessionUser(req.session) + if ( + currentUser != null && + (currentUser != null ? currentUser.referal_id : undefined) != null + ) { + url += `?r=${currentUser.referal_id}&rm=${referalMedium}&rs=b` // Referal source = bonus + } + return url + } + res.locals.getReferalId = function () { + const currentUser = SessionManager.getSessionUser(req.session) + if ( + currentUser != null && + (currentUser != null ? currentUser.referal_id : undefined) != null + ) { + return currentUser.referal_id + } + } + next() + }) + + webRouter.use(function (req, res, next) { + res.locals.csrfToken = req != null ? req.csrfToken() : undefined + next() + }) + + webRouter.use(function (req, res, next) { + res.locals.gaToken = + Settings.analytics && Settings.analytics.ga && Settings.analytics.ga.token + res.locals.gaOptimizeId = _.get(Settings, ['analytics', 'gaOptimize', 'id']) + next() + }) + + webRouter.use(function (req, res, next) { + res.locals.getReqQueryParam = field => + req.query != null ? req.query[field] : undefined + next() + }) + + webRouter.use(function (req, res, next) { + const currentUser = SessionManager.getSessionUser(req.session) + if (currentUser != null) { + res.locals.user = { + email: currentUser.email, + first_name: currentUser.first_name, + last_name: currentUser.last_name, + } + } + next() + }) + + webRouter.use(function (req, res, next) { + res.locals.getLoggedInUserId = () => + SessionManager.getLoggedInUserId(req.session) + res.locals.getSessionUser = () => SessionManager.getSessionUser(req.session) + next() + }) + + webRouter.use(function (req, res, next) { + // Clone the nav settings so they can be modified for each request + res.locals.nav = {} + for (const key in Settings.nav) { + res.locals.nav[key] = _.clone(Settings.nav[key]) + } + res.locals.templates = Settings.templateLinks + next() + }) + + webRouter.use(function (req, res, next) { + if (Settings.reloadModuleViewsOnEachRequest) { + Modules.loadViewIncludes() + } + res.locals.moduleIncludes = Modules.moduleIncludes + res.locals.moduleIncludesAvailable = Modules.moduleIncludesAvailable + next() + }) + + webRouter.use(function (req, res, next) { + // TODO + if (Settings.overleaf != null) { + res.locals.overallThemes = [ + { + name: 'Default', + val: '', + path: res.locals.buildCssPath(), + }, + { + name: 'Light', + val: 'light-', + path: res.locals.buildCssPath('light-'), + }, + ] + } + next() + }) + + webRouter.use(function (req, res, next) { + res.locals.settings = Settings + next() + }) + + webRouter.use(function (req, res, next) { + res.locals.ExposedSettings = { + isOverleaf: Settings.overleaf != null, + appName: Settings.appName, + hasSamlBeta: req.session.samlBeta, + hasSamlFeature: Features.hasFeature('saml'), + samlInitPath: _.get(Settings, ['saml', 'ukamf', 'initPath']), + hasLinkUrlFeature: Features.hasFeature('link-url'), + hasLinkedProjectFileFeature: Features.hasFeature('linked-project-file'), + hasLinkedProjectOutputFileFeature: Features.hasFeature( + 'linked-project-output-file' + ), + siteUrl: Settings.siteUrl, + emailConfirmationDisabled: Settings.emailConfirmationDisabled, + maxEntitiesPerProject: Settings.maxEntitiesPerProject, + maxUploadSize: Settings.maxUploadSize, + recaptchaSiteKeyV3: + Settings.recaptcha != null ? Settings.recaptcha.siteKeyV3 : undefined, + recaptchaDisabled: + Settings.recaptcha != null ? Settings.recaptcha.disabled : undefined, + textExtensions: Settings.textExtensions, + validRootDocExtensions: Settings.validRootDocExtensions, + sentryAllowedOriginRegex: Settings.sentry.allowedOriginRegex, + sentryDsn: Settings.sentry.publicDSN, + sentryEnvironment: Settings.sentry.environment, + sentryRelease: Settings.sentry.release, + enableSubscriptions: Settings.enableSubscriptions, + } + next() + }) +} diff --git a/services/web/app/src/infrastructure/Features.js b/services/web/app/src/infrastructure/Features.js new file mode 100644 index 0000000000..72c10472fe --- /dev/null +++ b/services/web/app/src/infrastructure/Features.js @@ -0,0 +1,111 @@ +const _ = require('lodash') +const Settings = require('@overleaf/settings') + +const publicRegistrationModuleAvailable = Settings.moduleImportSequence.includes( + 'public-registration' +) + +const supportModuleAvailable = Settings.moduleImportSequence.includes('support') + +const historyV1ModuleAvailable = Settings.moduleImportSequence.includes( + 'history-v1' +) + +const trackChangesModuleAvailable = Settings.moduleImportSequence.includes( + 'track-changes' +) + +/** + * @typedef {Object} Settings + * @property {Object | undefined} apis + * @property {Object | undefined} apis.linkedUrlProxy + * @property {string | undefined} apis.linkedUrlProxy.url + * @property {Object | undefined} apis.references + * @property {string | undefined} apis.references.url + * @property {boolean | undefined} enableGithubSync + * @property {boolean | undefined} enableGitBridge + * @property {boolean | undefined} enableHomepage + * @property {boolean | undefined} enableSaml + * @property {boolean | undefined} ldap + * @property {boolean | undefined} oauth + * @property {Object | undefined} overleaf + * @property {Object | undefined} overleaf.oauth + * @property {boolean | undefined} saml + */ + +const Features = { + /** + * @returns {boolean} + */ + externalAuthenticationSystemUsed() { + return ( + (Boolean(Settings.ldap) && Boolean(Settings.ldap.enable)) || + (Boolean(Settings.saml) && Boolean(Settings.saml.enable)) || + Boolean(_.get(Settings, ['overleaf', 'oauth'])) + ) + }, + + /** + * Whether a feature is enabled in the appliation's configuration + * + * @param {string} feature + * @returns {boolean} + */ + hasFeature(feature) { + switch (feature) { + case 'saas': + return Boolean(Settings.overleaf) + case 'homepage': + return Boolean(Settings.enableHomepage) + case 'registration-page': + return ( + !Features.externalAuthenticationSystemUsed() || + Boolean(Settings.overleaf) + ) + case 'registration': + return publicRegistrationModuleAvailable || Boolean(Settings.overleaf) + case 'github-sync': + return Boolean(Settings.enableGithubSync) + case 'git-bridge': + return Boolean(Settings.enableGitBridge) + case 'custom-togglers': + return Boolean(Settings.overleaf) + case 'oauth': + return Boolean(Settings.oauth) + case 'templates-server-pro': + return !Settings.overleaf + case 'history-v1': + return historyV1ModuleAvailable + case 'affiliations': + case 'analytics': + return Boolean(_.get(Settings, ['apis', 'v1', 'url'])) + case 'overleaf-integration': + return Boolean(Settings.overleaf) + case 'references': + return Boolean(_.get(Settings, ['apis', 'references', 'url'])) + case 'saml': + return Boolean(Settings.enableSaml) + case 'linked-project-file': + return Boolean(Settings.enabledLinkedFileTypes.includes('project_file')) + case 'linked-project-output-file': + return Boolean( + Settings.enabledLinkedFileTypes.includes('project_output_file') + ) + case 'link-url': + return Boolean( + _.get(Settings, ['apis', 'linkedUrlProxy', 'url']) && + Settings.enabledLinkedFileTypes.includes('url') + ) + case 'public-registration': + return publicRegistrationModuleAvailable + case 'support': + return supportModuleAvailable + case 'track-changes': + return trackChangesModuleAvailable + default: + throw new Error(`unknown feature: ${feature}`) + } + }, +} + +module.exports = Features diff --git a/services/web/app/src/infrastructure/FileWriter.js b/services/web/app/src/infrastructure/FileWriter.js new file mode 100644 index 0000000000..55e5faba8f --- /dev/null +++ b/services/web/app/src/infrastructure/FileWriter.js @@ -0,0 +1,198 @@ +/* eslint-disable + node/handle-callback-err, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const fs = require('fs') +const OError = require('@overleaf/o-error') +const logger = require('logger-sharelatex') +const uuid = require('uuid') +const _ = require('underscore') +const Settings = require('@overleaf/settings') +const request = require('request') +const { Transform, pipeline } = require('stream') +const { FileTooLargeError } = require('../Features/Errors/Errors') +const { promisifyAll } = require('../util/promises') + +class SizeLimitedStream extends Transform { + constructor(options) { + options.autoDestroy = true + super(options) + + this.bytes = 0 + this.maxSizeBytes = options.maxSizeBytes + this.drain = false + this.on('error', () => { + this.drain = true + this.resume() + }) + } + + _transform(chunk, encoding, done) { + if (this.drain) { + // mechanism to drain the source stream on error, to avoid leaks + // we consume the rest of the incoming stream and don't push it anywhere + return done() + } + + this.bytes += chunk.length + if (this.maxSizeBytes && this.bytes > this.maxSizeBytes) { + return done( + new FileTooLargeError({ + message: 'stream size limit reached', + info: { size: this.bytes }, + }) + ) + } + this.push(chunk) + done() + } +} + +const FileWriter = { + ensureDumpFolderExists(callback) { + if (callback == null) { + callback = function (error) {} + } + return fs.mkdir(Settings.path.dumpFolder, function (error) { + if (error != null && error.code !== 'EEXIST') { + // Ignore error about already existing + return callback(error) + } + return callback(null) + }) + }, + + writeLinesToDisk(identifier, lines, callback) { + if (callback == null) { + callback = function (error, fsPath) {} + } + return FileWriter.writeContentToDisk(identifier, lines.join('\n'), callback) + }, + + writeContentToDisk(identifier, content, callback) { + if (callback == null) { + callback = function (error, fsPath) {} + } + callback = _.once(callback) + const fsPath = `${Settings.path.dumpFolder}/${identifier}_${uuid.v4()}` + return FileWriter.ensureDumpFolderExists(function (error) { + if (error != null) { + return callback(error) + } + return fs.writeFile(fsPath, content, function (error) { + if (error != null) { + return callback(error) + } + return callback(null, fsPath) + }) + }) + }, + + writeStreamToDisk(identifier, stream, options, callback) { + if (typeof options === 'function') { + callback = options + options = {} + } + if (callback == null) { + callback = function (error, fsPath) {} + } + options = options || {} + + const fsPath = `${Settings.path.dumpFolder}/${identifier}_${uuid.v4()}` + + stream.pause() + + FileWriter.ensureDumpFolderExists(function (error) { + const writeStream = fs.createWriteStream(fsPath) + + if (error != null) { + return callback(error) + } + stream.resume() + + const passThrough = new SizeLimitedStream({ + maxSizeBytes: options.maxSizeBytes, + }) + + // if writing fails, we want to consume the bytes from the source, to avoid leaks + for (const evt of ['error', 'close']) { + writeStream.on(evt, function () { + passThrough.unpipe(writeStream) + passThrough.resume() + }) + } + + pipeline(stream, passThrough, writeStream, function (err) { + if ( + options.maxSizeBytes && + passThrough.bytes >= options.maxSizeBytes && + !(err instanceof FileTooLargeError) + ) { + err = new FileTooLargeError({ + message: 'stream size limit reached', + info: { size: passThrough.bytes }, + }).withCause(err || {}) + } + if (err) { + OError.tag( + err, + '[writeStreamToDisk] something went wrong writing the stream to disk', + { + identifier, + fsPath, + } + ) + return callback(err) + } + + logger.log( + { identifier, fsPath }, + '[writeStreamToDisk] write stream finished' + ) + callback(null, fsPath) + }) + }) + }, + + writeUrlToDisk(identifier, url, options, callback) { + if (typeof options === 'function') { + callback = options + options = {} + } + if (callback == null) { + callback = function (error, fsPath) {} + } + options = options || {} + callback = _.once(callback) + + const stream = request.get(url) + stream.on('error', function (err) { + logger.warn( + { err, identifier, url }, + '[writeUrlToDisk] something went wrong with writing to disk' + ) + callback(err) + }) + stream.on('response', function (response) { + if (response.statusCode >= 200 && response.statusCode < 300) { + FileWriter.writeStreamToDisk(identifier, stream, options, callback) + } else { + const err = new Error(`bad response from url: ${response.statusCode}`) + logger.warn({ err, identifier, url }, `[writeUrlToDisk] ${err.message}`) + return callback(err) + } + }) + }, +} + +module.exports = FileWriter +module.exports.promises = promisifyAll(FileWriter) +module.exports.SizeLimitedStream = SizeLimitedStream diff --git a/services/web/app/src/infrastructure/GeoIpLookup.js b/services/web/app/src/infrastructure/GeoIpLookup.js new file mode 100644 index 0000000000..94bb5a0d66 --- /dev/null +++ b/services/web/app/src/infrastructure/GeoIpLookup.js @@ -0,0 +1,99 @@ +const request = require('request') +const settings = require('@overleaf/settings') +const _ = require('underscore') +const logger = require('logger-sharelatex') +const URL = require('url') +const { promisify, promisifyMultiResult } = require('../util/promises') + +const currencyMappings = { + GB: 'GBP', + US: 'USD', + CH: 'CHF', + NZ: 'NZD', + AU: 'AUD', + DK: 'DKK', + NO: 'NOK', + CA: 'CAD', + SE: 'SEK', +} + +// Countries which would likely prefer Euro's +const EuroCountries = [ + 'AT', + 'BE', + 'BG', + 'HR', + 'CY', + 'CZ', + 'EE', + 'FI', + 'FR', + 'DE', + 'EL', + 'HU', + 'IE', + 'IT', + 'LV', + 'LT', + 'LU', + 'MT', + 'NL', + 'PL', + 'PT', + 'RO', + 'SK', + 'SI', + 'ES', +] + +_.each(EuroCountries, country => (currencyMappings[country] = 'EUR')) + +function getDetails(ip, callback) { + if (!ip) { + return callback(new Error('no ip passed')) + } + ip = ip.trim().split(' ')[0] + const opts = { + url: URL.resolve(settings.apis.geoIpLookup.url, ip), + timeout: 1000, + json: true, + } + logger.log({ ip, opts }, 'getting geo ip details') + request.get(opts, function (err, res, ipDetails) { + if (err) { + logger.warn({ err, ip }, 'error getting ip details') + } + callback(err, ipDetails) + }) +} + +function getCurrencyCode(ip, callback) { + getDetails(ip, function (err, ipDetails) { + if (err || !ipDetails) { + logger.err( + { err, ip }, + 'problem getting currencyCode for ip, defaulting to USD' + ) + return callback(null, 'USD') + } + const countryCode = + ipDetails && ipDetails.country_code + ? ipDetails.country_code.toUpperCase() + : undefined + const currencyCode = currencyMappings[countryCode] || 'USD' + logger.log({ ip, currencyCode, ipDetails }, 'got currencyCode for ip') + callback(err, currencyCode, countryCode) + }) +} + +module.exports = { + getDetails, + getCurrencyCode, + promises: { + getDetails: promisify(getDetails), + getCurrencyCode: promisifyMultiResult(getCurrencyCode, [ + 'currencyCode', + 'countryCode', + ]), + }, +} diff --git a/services/web/app/src/infrastructure/JsonWebToken.js b/services/web/app/src/infrastructure/JsonWebToken.js new file mode 100644 index 0000000000..0a49e97062 --- /dev/null +++ b/services/web/app/src/infrastructure/JsonWebToken.js @@ -0,0 +1,22 @@ +const { callbackify, promisify } = require('util') +const JWT = require('jsonwebtoken') +const Settings = require('@overleaf/settings') + +const jwtSign = promisify(JWT.sign) + +async function sign(payload, options = {}) { + const key = Settings.jwt.key + const algorithm = Settings.jwt.algorithm + if (!key || !algorithm) { + throw new Error('missing JWT configuration') + } + const token = await jwtSign(payload, key, { ...options, algorithm }) + return token +} + +module.exports = { + sign: callbackify(sign), + promises: { + sign, + }, +} diff --git a/services/web/app/src/infrastructure/Keys.js b/services/web/app/src/infrastructure/Keys.js new file mode 100644 index 0000000000..cd177b90bc --- /dev/null +++ b/services/web/app/src/infrastructure/Keys.js @@ -0,0 +1,8 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +module.exports = { + queue: { + web_to_tpds_http_requests: 'web_to_tpds_http_requests', + tpds_to_web_http_requests: 'tpds_to_web_http_requests', + }, +} diff --git a/services/web/app/src/infrastructure/LockManager.js b/services/web/app/src/infrastructure/LockManager.js new file mode 100644 index 0000000000..4e8adfc98f --- /dev/null +++ b/services/web/app/src/infrastructure/LockManager.js @@ -0,0 +1,204 @@ +const { callbackify, promisify } = require('util') +const metrics = require('@overleaf/metrics') +const RedisWrapper = require('./RedisWrapper') +const rclient = RedisWrapper.client('lock') +const logger = require('logger-sharelatex') +const os = require('os') +const crypto = require('crypto') +const async = require('async') +const settings = require('@overleaf/settings') + +const HOST = os.hostname() +const PID = process.pid +const RND = crypto.randomBytes(4).toString('hex') +let COUNT = 0 + +const LOCK_QUEUES = new Map() // queue lock requests for each name/id so they get the lock on a first-come first-served basis + +logger.log( + { lockManagerSettings: settings.lockManager }, + 'LockManager initialising' +) + +const LockManager = { + // ms between each test of the lock + LOCK_TEST_INTERVAL: settings.lockManager.lockTestInterval || 50, + // back off to ms between each test of the lock + MAX_TEST_INTERVAL: settings.lockManager.maxTestInterval || 1000, + // ms maximum time to spend trying to get the lock + MAX_LOCK_WAIT_TIME: settings.lockManager.maxLockWaitTime || 10000, + // seconds. Time until lock auto expires in redis + REDIS_LOCK_EXPIRY: settings.lockManager.redisLockExpiry || 30, + // ms, if execution takes longer than this then log + SLOW_EXECUTION_THRESHOLD: settings.lockManager.slowExecutionThreshold || 5000, + + // Use a signed lock value as described in + // http://redis.io/topics/distlock#correct-implementation-with-a-single-instance + // to prevent accidental unlocking by multiple processes + randomLock() { + const time = Date.now() + return `locked:host=${HOST}:pid=${PID}:random=${RND}:time=${time}:count=${COUNT++}` + }, + + unlockScript: + 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end', + + runWithLock(namespace, id, runner, callback) { + // runner must be a function accepting a callback, e.g. runner = (cb) -> + + // This error is defined here so we get a useful stacktrace + const slowExecutionError = new Error('slow execution during lock') + + const timer = new metrics.Timer(`lock.${namespace}`) + const key = `lock:web:${namespace}:${id}` + LockManager._getLock(key, namespace, (error, lockValue) => { + if (error != null) { + return callback(error) + } + + // The lock can expire in redis but the process carry on. This setTimeout call + // is designed to log if this happens. + function countIfExceededLockTimeout() { + metrics.inc(`lock.${namespace}.exceeded_lock_timeout`) + logger.log('exceeded lock timeout', { + namespace, + id, + slowExecutionError, + }) + } + const exceededLockTimeout = setTimeout( + countIfExceededLockTimeout, + LockManager.REDIS_LOCK_EXPIRY * 1000 + ) + + runner((error1, ...values) => + LockManager._releaseLock(key, lockValue, error2 => { + clearTimeout(exceededLockTimeout) + + const timeTaken = new Date() - timer.start + if (timeTaken > LockManager.SLOW_EXECUTION_THRESHOLD) { + logger.log('slow execution during lock', { + namespace, + id, + timeTaken, + slowExecutionError, + }) + } + + timer.done() + error = error1 || error2 + if (error != null) { + return callback(error) + } + callback(null, ...values) + }) + ) + }) + }, + + _tryLock(key, namespace, callback) { + const lockValue = LockManager.randomLock() + rclient.set( + key, + lockValue, + 'EX', + LockManager.REDIS_LOCK_EXPIRY, + 'NX', + (err, gotLock) => { + if (err != null) { + return callback(err) + } + if (gotLock === 'OK') { + metrics.inc(`lock.${namespace}.try.success`) + callback(err, true, lockValue) + } else { + metrics.inc(`lock.${namespace}.try.failed`) + logger.log({ key, redis_response: gotLock }, 'lock is locked') + callback(err, false) + } + } + ) + }, + + // it's sufficient to serialize within a process because that is where the parallel operations occur + _getLock(key, namespace, callback) { + // this is what we need to do for each lock we want to request + const task = next => + LockManager._getLockByPolling(key, namespace, (error, lockValue) => { + // tell the queue to start trying to get the next lock (if any) + next() + // we have got a lock result, so we can continue with our own execution + callback(error, lockValue) + }) + // create a queue for this key if needed + const queueName = `${key}:${namespace}` + let queue = LOCK_QUEUES.get(queueName) + if (queue == null) { + const handler = (fn, cb) => fn(cb) + // set up a new queue for this key + queue = async.queue(handler, 1) + queue.push(task) + // remove the queue object when queue is empty + queue.drain = () => LOCK_QUEUES.delete(queueName) + // store the queue in our global map + LOCK_QUEUES.set(queueName, queue) + } else { + // queue the request to get the lock + queue.push(task) + } + }, + + _getLockByPolling(key, namespace, callback) { + const startTime = Date.now() + const testInterval = LockManager.LOCK_TEST_INTERVAL + let attempts = 0 + function attempt() { + if (Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME) { + metrics.inc(`lock.${namespace}.get.failed`) + return callback(new Error('Timeout')) + } + + attempts += 1 + LockManager._tryLock(key, namespace, (error, gotLock, lockValue) => { + if (error != null) { + return callback(error) + } + if (gotLock) { + metrics.gauge(`lock.${namespace}.get.success.tries`, attempts) + callback(null, lockValue) + } else { + setTimeout(attempt, testInterval) + } + }) + } + attempt() + }, + + _releaseLock(key, lockValue, callback) { + rclient.eval(LockManager.unlockScript, 1, key, lockValue, (err, result) => { + if (err != null) { + callback(err) + } else if (result != null && result !== 1) { + // successful unlock should release exactly one key + logger.warn( + { key, lockValue, redis_err: err, redis_result: result }, + 'unlocking error' + ) + metrics.inc('unlock-error') + callback(new Error('tried to release timed out lock')) + } else { + callback(null, result) + } + }) + }, +} + +module.exports = LockManager + +const promisifiedRunWithLock = promisify(LockManager.runWithLock) +LockManager.promises = { + runWithLock(namespace, id, runner) { + const cbRunner = callbackify(runner) + return promisifiedRunWithLock(namespace, id, cbRunner) + }, +} diff --git a/services/web/app/src/infrastructure/LoggerSerializers.js b/services/web/app/src/infrastructure/LoggerSerializers.js new file mode 100644 index 0000000000..09d274aa82 --- /dev/null +++ b/services/web/app/src/infrastructure/LoggerSerializers.js @@ -0,0 +1,57 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +module.exports = { + user(user) { + if (user == null) { + return null + } + if (user._id == null) { + user = { _id: user } + } + return { + id: user._id, + email: user.email, + first_name: user.name, + last_name: user.name, + } + }, + + project(project) { + if (project == null) { + return null + } + if (project._id == null) { + project = { _id: project } + } + return { + id: project._id, + name: project.name, + } + }, + + docs(docs) { + if ((docs != null ? docs.map : undefined) == null) { + return + } + return docs.map(doc => ({ + path: doc.path, + id: doc.doc, + })) + }, + + files(files) { + if ((files != null ? files.map : undefined) == null) { + return + } + return files.map(file => ({ + path: file.path, + id: file.file, + })) + }, +} diff --git a/services/web/app/src/infrastructure/Metrics.js b/services/web/app/src/infrastructure/Metrics.js new file mode 100644 index 0000000000..2185c3c6dd --- /dev/null +++ b/services/web/app/src/infrastructure/Metrics.js @@ -0,0 +1,7 @@ +const Metrics = require('@overleaf/metrics') + +exports.analyticsQueue = new Metrics.prom.Counter({ + name: 'analytics_queue', + help: 'Number of events sent to the analytics queue', + labelNames: ['status', 'event_type'], +}) diff --git a/services/web/app/src/infrastructure/Modules.js b/services/web/app/src/infrastructure/Modules.js new file mode 100644 index 0000000000..a010fd39f9 --- /dev/null +++ b/services/web/app/src/infrastructure/Modules.js @@ -0,0 +1,158 @@ +const fs = require('fs') +const Path = require('path') +const pug = require('pug') +const async = require('async') +const { promisify } = require('util') +const Settings = require('@overleaf/settings') + +const MODULE_BASE_PATH = Path.resolve(__dirname + '/../../../modules') + +const _modules = [] +const _hooks = {} +let _viewIncludes = {} + +function loadModules() { + const settingsCheckModule = Path.join( + MODULE_BASE_PATH, + 'settings-check', + 'index.js' + ) + if (fs.existsSync(settingsCheckModule)) { + require(settingsCheckModule) + } + + for (const moduleName of Settings.moduleImportSequence) { + const loadedModule = require(Path.join( + MODULE_BASE_PATH, + moduleName, + 'index.js' + )) + loadedModule.name = moduleName + _modules.push(loadedModule) + } + attachHooks() +} + +function applyRouter(webRouter, privateApiRouter, publicApiRouter) { + for (const module of _modules) { + if (module.router && module.router.apply) { + module.router.apply(webRouter, privateApiRouter, publicApiRouter) + } + } +} + +function applyNonCsrfRouter(webRouter, privateApiRouter, publicApiRouter) { + for (const module of _modules) { + if (module.nonCsrfRouter != null) { + module.nonCsrfRouter.apply(webRouter, privateApiRouter, publicApiRouter) + } + if (module.router && module.router.applyNonCsrfRouter) { + module.router.applyNonCsrfRouter( + webRouter, + privateApiRouter, + publicApiRouter + ) + } + } +} + +function loadViewIncludes(app) { + _viewIncludes = {} + for (const module of _modules) { + const object = module.viewIncludes || {} + for (const view in object) { + const partial = object[view] + if (!_viewIncludes[view]) { + _viewIncludes[view] = [] + } + const filePath = Path.join( + MODULE_BASE_PATH, + module.name, + 'app/views', + partial + '.pug' + ) + _viewIncludes[view].push( + pug.compileFile(filePath, { + doctype: 'html', + compileDebug: Settings.debugPugTemplates, + }) + ) + } + } +} + +function moduleIncludes(view, locals) { + const compiledPartials = _viewIncludes[view] || [] + let html = '' + for (const compiledPartial of compiledPartials) { + html += compiledPartial(locals) + } + return html +} + +function moduleIncludesAvailable(view) { + return (_viewIncludes[view] || []).length > 0 +} + +function linkedFileAgentsIncludes() { + const agents = {} + for (const module of _modules) { + for (const name in module.linkedFileAgents) { + const agentFunction = module.linkedFileAgents[name] + agents[name] = agentFunction() + } + } + return agents +} + +function attachHooks() { + for (var module of _modules) { + if (module.hooks != null) { + for (const hook in module.hooks) { + const method = module.hooks[hook] + attachHook(hook, method) + } + } + } +} + +function attachHook(name, method) { + if (_hooks[name] == null) { + _hooks[name] = [] + } + _hooks[name].push(method) +} + +function fireHook(name, ...rest) { + const adjustedLength = Math.max(rest.length, 1) + const args = rest.slice(0, adjustedLength - 1) + const callback = rest[adjustedLength - 1] + const methods = _hooks[name] || [] + const callMethods = methods.map(method => cb => method(...args, cb)) + async.series(callMethods, function (error, results) { + if (error) { + return callback(error) + } + callback(null, results) + }) +} + +module.exports = { + applyNonCsrfRouter, + applyRouter, + linkedFileAgentsIncludes, + loadViewIncludes, + moduleIncludes, + moduleIncludesAvailable, + hooks: { + attach: attachHook, + fire: fireHook, + }, + promises: { + hooks: { + fire: promisify(fireHook), + }, + }, +} + +loadModules() diff --git a/services/web/app/src/infrastructure/Mongoose.js b/services/web/app/src/infrastructure/Mongoose.js new file mode 100644 index 0000000000..f680abe94c --- /dev/null +++ b/services/web/app/src/infrastructure/Mongoose.js @@ -0,0 +1,61 @@ +const mongoose = require('mongoose') +const Settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') + +if ( + typeof global.beforeEach === 'function' && + process.argv.join(' ').match(/unit/) +) { + throw new Error( + 'It looks like unit tests are running, but you are connecting to Mongo. Missing a stub?' + ) +} + +const connectionPromise = mongoose.connect( + Settings.mongo.url, + Object.assign( + { + // mongoose specific config + config: { autoIndex: false }, + // mongoose defaults to false, native driver defaults to true + useNewUrlParser: true, + // use the equivalent `findOneAndUpdate` methods of the native driver + useFindAndModify: false, + }, + Settings.mongo.options + ) +) + +mongoose.connection.on('connected', () => + logger.log('mongoose default connection open') +) + +mongoose.connection.on('error', err => + logger.err({ err }, 'mongoose error on default connection') +) + +mongoose.connection.on('disconnected', () => + logger.log('mongoose default connection disconnected') +) + +if (process.env.MONGOOSE_DEBUG) { + mongoose.set('debug', (collectionName, method, query, doc) => + logger.debug({ collectionName, method, query, doc }, 'mongoose debug') + ) +} + +mongoose.plugin(schema => { + schema.options.usePushEach = true +}) + +mongoose.Promise = global.Promise + +async function getNativeDb() { + const mongooseInstance = await connectionPromise + return mongooseInstance.connection.db +} + +mongoose.getNativeDb = getNativeDb +mongoose.connectionPromise = connectionPromise + +module.exports = mongoose diff --git a/services/web/app/src/infrastructure/PackageVersions.js b/services/web/app/src/infrastructure/PackageVersions.js new file mode 100644 index 0000000000..3509d0b22d --- /dev/null +++ b/services/web/app/src/infrastructure/PackageVersions.js @@ -0,0 +1,25 @@ +// TODO: This file was created by bulk-decaffeinate. +// Sanity-check the conversion and remove this comment. +/* + * decaffeinate suggestions: + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const ACE_VERSION = require('ace-builds/version') +const version = { + // Upgrade instructions: https://github.com/overleaf/write_latex/wiki/Upgrading-Ace + ace: ACE_VERSION, + fineuploader: '5.15.4', +} + +module.exports = { + version, + + lib(name) { + if (version[name] != null) { + return `${name}-${version[name]}` + } else { + return `${name}` + } + }, +} diff --git a/services/web/app/src/infrastructure/ProxyManager.js b/services/web/app/src/infrastructure/ProxyManager.js new file mode 100644 index 0000000000..a5afff292b --- /dev/null +++ b/services/web/app/src/infrastructure/ProxyManager.js @@ -0,0 +1,90 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let ProxyManager +const settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const request = require('request') +const URL = require('url') + +module.exports = ProxyManager = { + apply(publicApiRouter) { + return (() => { + const result = [] + for (var proxyUrl in settings.proxyUrls) { + const target = settings.proxyUrls[proxyUrl] + result.push( + (function (target) { + const method = + (target.options != null ? target.options.method : undefined) || + 'get' + return publicApiRouter[method]( + proxyUrl, + ProxyManager.createProxy(target) + ) + })(target) + ) + } + return result + })() + }, + + createProxy(target) { + return function (req, res, next) { + const targetUrl = makeTargetUrl(target, req) + logger.log({ targetUrl, reqUrl: req.url }, 'proxying url') + + const options = { url: targetUrl } + if (req.headers != null ? req.headers.cookie : undefined) { + options.headers = { Cookie: req.headers.cookie } + } + if ((target != null ? target.options : undefined) != null) { + Object.assign(options, target.options) + } + if (['post', 'put'].includes(options.method)) { + options.form = req.body + } + const upstream = request(options) + upstream.on('error', error => + logger.error({ err: error }, 'error in ProxyManager') + ) + + // TODO: better handling of status code + // see https://github.com/overleaf/write_latex/wiki/Streams-and-pipes-in-Node.js + return upstream.pipe(res) + } + }, +} + +// make a URL from a proxy target. +// if the query is specified, set/replace the target's query with the given query +var makeTargetUrl = function (target, req) { + const targetUrl = URL.parse(parseSettingUrl(target, req)) + if (req.query != null && Object.keys(req.query).length > 0) { + targetUrl.query = req.query + targetUrl.search = null // clear `search` as it takes precedence over `query` + } + return targetUrl.format() +} + +var parseSettingUrl = function (target, { params }) { + let path + if (typeof target === 'string') { + return target + } + if (typeof target.path === 'function') { + path = target.path(params) + } else { + ;({ path } = target) + } + return `${target.baseUrl}${path || ''}` +} diff --git a/services/web/app/src/infrastructure/Queues.js b/services/web/app/src/infrastructure/Queues.js new file mode 100644 index 0000000000..d22d0ac61a --- /dev/null +++ b/services/web/app/src/infrastructure/Queues.js @@ -0,0 +1,62 @@ +const Queue = require('bull') +const Settings = require('@overleaf/settings') + +// Bull will keep a fixed number of the most recently completed jobs. This is +// useful to inspect recently completed jobs. The bull prometheus exporter also +// uses the completed job records to report on job duration. +const MAX_COMPLETED_JOBS_RETAINED = 10000 +const MAX_FAILED_JOBS_RETAINED = 50000 + +const queues = {} + +function getAnalyticsEventsQueue() { + if (Settings.analytics.enabled) { + return getOrCreateQueue('analytics-events') + } +} + +function getAnalyticsEditingSessionsQueue() { + if (Settings.analytics.enabled) { + return getOrCreateQueue('analytics-editing-sessions') + } +} + +function getAnalyticsUserPropertiesQueue() { + if (Settings.analytics.enabled) { + return getOrCreateQueue('analytics-user-properties') + } +} + +function getOnboardingEmailsQueue() { + return getOrCreateQueue('emails-onboarding') +} + +function getPostRegistrationAnalyticsQueue() { + return getOrCreateQueue('post-registration-analytics') +} + +function getOrCreateQueue(queueName, defaultJobOptions) { + if (!queues[queueName]) { + queues[queueName] = new Queue(queueName, { + redis: Settings.redis.queues, + defaultJobOptions: { + removeOnComplete: MAX_COMPLETED_JOBS_RETAINED, + removeOnFail: MAX_FAILED_JOBS_RETAINED, + attempts: 11, + backoff: { + type: 'exponential', + delay: 3000, + }, + }, + }) + } + return queues[queueName] +} + +module.exports = { + getAnalyticsEventsQueue, + getAnalyticsEditingSessionsQueue, + getAnalyticsUserPropertiesQueue, + getOnboardingEmailsQueue, + getPostRegistrationAnalyticsQueue, +} diff --git a/services/web/app/src/infrastructure/RandomLogging.js b/services/web/app/src/infrastructure/RandomLogging.js new file mode 100644 index 0000000000..a7967ccd04 --- /dev/null +++ b/services/web/app/src/infrastructure/RandomLogging.js @@ -0,0 +1,21 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let trackOpenSockets +const _ = require('underscore') +const metrics = require('@overleaf/metrics') +;(trackOpenSockets = function () { + metrics.gauge( + 'http.open-sockets', + _.size(require('http').globalAgent.sockets.length), + 0.5 + ) + return setTimeout(trackOpenSockets, 1000) +})() diff --git a/services/web/app/src/infrastructure/RateLimiter.js b/services/web/app/src/infrastructure/RateLimiter.js new file mode 100644 index 0000000000..6691edb820 --- /dev/null +++ b/services/web/app/src/infrastructure/RateLimiter.js @@ -0,0 +1,42 @@ +const settings = require('@overleaf/settings') +const Metrics = require('@overleaf/metrics') +const RedisWrapper = require('./RedisWrapper') +const rclient = RedisWrapper.client('ratelimiter') +const { RedisRateLimiter } = require('rolling-rate-limiter') +const { callbackify } = require('util') + +async function addCount(opts) { + if (settings.disableRateLimits) { + return true + } + const namespace = `RateLimit:${opts.endpointName}:` + const k = `{${opts.subjectName}}` + const limiter = new RedisRateLimiter({ + client: rclient, + namespace, + interval: opts.timeInterval * 1000, + maxInInterval: opts.throttle, + }) + const rateLimited = await limiter.limit(k) + if (rateLimited) { + Metrics.inc('rate-limit-hit', 1, { + path: opts.endpointName, + }) + } + return !rateLimited +} + +async function clearRateLimit(endpointName, subject) { + // same as the key which will be built by RollingRateLimiter (namespace+k) + const keyName = `RateLimit:${endpointName}:{${subject}}` + await rclient.del(keyName) +} + +module.exports = { + addCount: callbackify(addCount), + clearRateLimit: callbackify(clearRateLimit), + promises: { + addCount, + clearRateLimit, + }, +} diff --git a/services/web/app/src/infrastructure/RedirectManager.js b/services/web/app/src/infrastructure/RedirectManager.js new file mode 100644 index 0000000000..6f0d3c3ec2 --- /dev/null +++ b/services/web/app/src/infrastructure/RedirectManager.js @@ -0,0 +1,85 @@ +/* eslint-disable + max-len, + no-unused-vars, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +let RedirectManager +const settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const URL = require('url') +const querystring = require('querystring') + +module.exports = RedirectManager = { + apply(webRouter) { + return (() => { + const result = [] + for (var redirectUrl in settings.redirects) { + var target = settings.redirects[redirectUrl] + result.push( + Array.from(target.methods || ['get']).map(method => + webRouter[method]( + redirectUrl, + RedirectManager.createRedirect(target) + ) + ) + ) + } + return result + })() + }, + + createRedirect(target) { + return function (req, res, next) { + let url + if ( + (req.headers != null ? req.headers['x-skip-redirects'] : undefined) != + null + ) { + return next() + } + let code = 302 + if (typeof target === 'string') { + url = target + } else { + if (req.method !== 'GET') { + code = 307 + } + + if (typeof target.url === 'function') { + url = target.url(req.params) + if (!url) { + return next() + } + } else { + ;({ url } = target) + } + + if (target.baseUrl != null) { + url = `${target.baseUrl}${url}` + } + } + return res.redirect(code, url + getQueryString(req)) + } + }, +} + +// Naively get the query params string. Stringifying the req.query object may +// have differences between Express and Rails, so safer to just pass the raw +// string +var getQueryString = function (req) { + const { search } = URL.parse(req.url) + if (search) { + return search + } else { + return '' + } +} diff --git a/services/web/app/src/infrastructure/RedisWrapper.js b/services/web/app/src/infrastructure/RedisWrapper.js new file mode 100644 index 0000000000..9f6df9a5a2 --- /dev/null +++ b/services/web/app/src/infrastructure/RedisWrapper.js @@ -0,0 +1,24 @@ +const Settings = require('@overleaf/settings') +const redis = require('@overleaf/redis-wrapper') + +if ( + typeof global.beforeEach === 'function' && + process.argv.join(' ').match(/unit/) +) { + throw new Error( + 'It looks like unit tests are running, but you are connecting to Redis. Missing a stub?' + ) +} + +// A per-feature interface to Redis, +// looks up the feature in `settings.redis` +// and returns an appropriate client. +// Necessary because we don't want to migrate web over +// to redis-cluster all at once. +module.exports = { + // feature = 'websessions' | 'ratelimiter' | ... + client(feature) { + const redisFeatureSettings = Settings.redis[feature] || Settings.redis.web + return redis.createClient(redisFeatureSettings) + }, +} diff --git a/services/web/app/src/infrastructure/RequestContentTypeDetection.js b/services/web/app/src/infrastructure/RequestContentTypeDetection.js new file mode 100644 index 0000000000..6c8c587de6 --- /dev/null +++ b/services/web/app/src/infrastructure/RequestContentTypeDetection.js @@ -0,0 +1,5 @@ +module.exports = { + acceptsJson(req) { + return req.accepts(['html', 'json']) === 'json' + }, +} diff --git a/services/web/app/src/infrastructure/Server.js b/services/web/app/src/infrastructure/Server.js new file mode 100644 index 0000000000..7e30f77237 --- /dev/null +++ b/services/web/app/src/infrastructure/Server.js @@ -0,0 +1,278 @@ +const Path = require('path') +const express = require('express') +const Settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const metrics = require('@overleaf/metrics') +const expressLocals = require('./ExpressLocals') +const Validation = require('./Validation') +const csp = require('./CSP') +const Router = require('../router') +const helmet = require('helmet') +const UserSessionsRedis = require('../Features/User/UserSessionsRedis') +const Csrf = require('./Csrf') + +const sessionsRedisClient = UserSessionsRedis.client() + +const SessionAutostartMiddleware = require('./SessionAutostartMiddleware') +const SessionStoreManager = require('./SessionStoreManager') +const session = require('express-session') +const RedisStore = require('connect-redis')(session) +const bodyParser = require('./BodyParserWrapper') +const methodOverride = require('method-override') +const cookieParser = require('cookie-parser') +const bearerToken = require('express-bearer-token') + +const passport = require('passport') +const LocalStrategy = require('passport-local').Strategy + +const oneDayInMilliseconds = 86400000 +const ReferalConnect = require('../Features/Referal/ReferalConnect') +const RedirectManager = require('./RedirectManager') +const ProxyManager = require('./ProxyManager') +const translations = require('./Translations') +const Modules = require('./Modules') +const Views = require('./Views') + +const ErrorController = require('../Features/Errors/ErrorController') +const HttpErrorHandler = require('../Features/Errors/HttpErrorHandler') +const UserSessionsManager = require('../Features/User/UserSessionsManager') +const AuthenticationController = require('../Features/Authentication/AuthenticationController') +const SessionManager = require('../Features/Authentication/SessionManager') + +const STATIC_CACHE_AGE = Settings.cacheStaticAssets + ? oneDayInMilliseconds * 365 + : 0 + +// Init the session store +const sessionStore = new RedisStore({ client: sessionsRedisClient }) + +const app = express() + +const webRouter = express.Router() +const privateApiRouter = express.Router() +const publicApiRouter = express.Router() + +if (Settings.behindProxy) { + app.set('trust proxy', Settings.trustedProxyIps || true) + /** + * Handle the X-Original-Forwarded-For header. + * + * The nginx ingress sends us the contents of X-Forwarded-For it received in + * X-Original-Forwarded-For. Express expects all proxy IPs to be in a comma + * separated list in X-Forwarded-For. + */ + app.use((req, res, next) => { + if ( + req.headers['x-original-forwarded-for'] && + req.headers['x-forwarded-for'] + ) { + req.headers['x-forwarded-for'] = + req.headers['x-original-forwarded-for'] + + ', ' + + req.headers['x-forwarded-for'] + } + next() + }) +} +if (Settings.exposeHostname) { + const HOSTNAME = require('os').hostname() + app.use((req, res, next) => { + res.setHeader('X-Served-By', HOSTNAME) + next() + }) +} + +webRouter.get( + '/serviceWorker.js', + express.static(Path.join(__dirname, '/../../../public'), { + maxAge: oneDayInMilliseconds, + }) +) +webRouter.use( + express.static(Path.join(__dirname, '/../../../public'), { + maxAge: STATIC_CACHE_AGE, + }) +) +app.set('views', Path.join(__dirname, '/../../views')) +app.set('view engine', 'pug') +Modules.loadViewIncludes(app) + +app.use(bodyParser.urlencoded({ extended: true, limit: '2mb' })) +app.use(bodyParser.json({ limit: Settings.max_json_request_size })) +app.use(methodOverride()) +app.use(bearerToken()) + +app.use(metrics.http.monitor(logger)) +RedirectManager.apply(webRouter) +ProxyManager.apply(publicApiRouter) + +webRouter.use(cookieParser(Settings.security.sessionSecret)) +SessionAutostartMiddleware.applyInitialMiddleware(webRouter) +webRouter.use( + session({ + resave: false, + saveUninitialized: false, + secret: Settings.security.sessionSecret, + proxy: Settings.behindProxy, + cookie: { + domain: Settings.cookieDomain, + maxAge: Settings.cookieSessionLength, // in milliseconds, see https://github.com/expressjs/session#cookiemaxage + secure: Settings.secureCookie, + sameSite: Settings.sameSiteCookie, + }, + store: sessionStore, + key: Settings.cookieName, + rolling: true, + }) +) + +// patch the session store to generate a validation token for every new session +SessionStoreManager.enableValidationToken(sessionStore) +// use middleware to reject all requests with invalid tokens +webRouter.use(SessionStoreManager.validationMiddleware) + +// passport +webRouter.use(passport.initialize()) +webRouter.use(passport.session()) + +passport.use( + new LocalStrategy( + { + passReqToCallback: true, + usernameField: 'email', + passwordField: 'password', + }, + AuthenticationController.doPassportLogin + ) +) +passport.serializeUser(AuthenticationController.serializeUser) +passport.deserializeUser(AuthenticationController.deserializeUser) + +Modules.hooks.fire('passportSetup', passport, function (err) { + if (err != null) { + logger.err({ err }, 'error setting up passport in modules') + } +}) + +Modules.applyNonCsrfRouter(webRouter, privateApiRouter, publicApiRouter) + +webRouter.csrf = new Csrf() +webRouter.use(webRouter.csrf.middleware) +webRouter.use(translations.i18nMiddleware) +webRouter.use(translations.setLangBasedOnDomainMiddleware) + +// Measure expiry from last request, not last login +webRouter.use(function (req, res, next) { + if (!req.session.noSessionCallback) { + req.session.touch() + if (SessionManager.isUserLoggedIn(req.session)) { + UserSessionsManager.touch( + SessionManager.getSessionUser(req.session), + err => { + if (err) { + logger.err({ err }, 'error extending user session') + } + } + ) + } + } + next() +}) + +webRouter.use(ReferalConnect.use) +expressLocals(webRouter, privateApiRouter, publicApiRouter) + +webRouter.use(SessionAutostartMiddleware.invokeCallbackMiddleware) + +webRouter.use(function (req, res, next) { + if (Settings.siteIsOpen) { + next() + } else if ( + SessionManager.getSessionUser(req.session) && + SessionManager.getSessionUser(req.session).isAdmin + ) { + next() + } else { + HttpErrorHandler.maintenance(req, res) + } +}) + +webRouter.use(function (req, res, next) { + if (Settings.editorIsOpen) { + next() + } else if (req.url.indexOf('/admin') === 0) { + next() + } else { + HttpErrorHandler.maintenance(req, res) + } +}) + +webRouter.use(AuthenticationController.validateAdmin) + +// add security headers using Helmet +const noCacheMiddleware = require('nocache')() +webRouter.use(function (req, res, next) { + const isLoggedIn = SessionManager.isUserLoggedIn(req.session) + const isProjectPage = !!req.path.match('^/project/[a-f0-9]{24}$') + if (isLoggedIn || isProjectPage) { + noCacheMiddleware(req, res, next) + } else { + next() + } +}) +webRouter.use( + helmet({ + // note that more headers are added by default + dnsPrefetchControl: false, + referrerPolicy: { policy: 'origin-when-cross-origin' }, + hsts: false, + }) +) + +// add CSP header to HTML-rendering routes, if enabled +if (Settings.csp && Settings.csp.enabled) { + logger.info('adding CSP header to rendered routes', Settings.csp) + webRouter.use(csp(Settings.csp)) +} + +logger.info('creating HTTP server'.yellow) +const server = require('http').createServer(app) + +// provide settings for separate web and api processes +if (Settings.enabledServices.includes('api')) { + logger.info('providing api router') + app.use(privateApiRouter) + app.use(Validation.errorMiddleware) + app.use(ErrorController.handleApiError) +} + +if (Settings.enabledServices.includes('web')) { + logger.info('providing web router') + + if (Settings.precompilePugTemplatesAtBootTime) { + logger.info('precompiling views for web in production environment') + Views.precompileViews(app) + } + if (app.get('env') === 'test') { + logger.info('enabling view cache for acceptance tests') + app.enable('view cache') + } + + app.use(publicApiRouter) // public API goes with web router for public access + app.use(Validation.errorMiddleware) + app.use(ErrorController.handleApiError) + + app.use(webRouter) + app.use(Validation.errorMiddleware) + app.use(ErrorController.handleError) +} + +metrics.injectMetricsRoute(webRouter) +metrics.injectMetricsRoute(privateApiRouter) + +Router.initialize(webRouter, privateApiRouter, publicApiRouter) + +module.exports = { + app, + server, +} diff --git a/services/web/app/src/infrastructure/SessionAutostartMiddleware.js b/services/web/app/src/infrastructure/SessionAutostartMiddleware.js new file mode 100644 index 0000000000..ca883db483 --- /dev/null +++ b/services/web/app/src/infrastructure/SessionAutostartMiddleware.js @@ -0,0 +1,122 @@ +const Settings = require('@overleaf/settings') +const OError = require('@overleaf/o-error') + +const botUserAgents = [ + 'kube-probe', + 'GoogleStackdriverMonitoring', + 'GoogleHC', + 'Googlebot', + 'bingbot', + 'facebookexternal', +].map(agent => { + return agent.toLowerCase() +}) +// SessionAutostartMiddleware provides a mechanism to force certain routes not +// to get an automatic session where they don't have one already. This allows us +// to work around issues where we might overwrite a user's login cookie with one +// that is hidden by a `SameSite` setting. +// +// When registering a route with disableSessionAutostartForRoute, a callback +// should be provided that handles the case that a session is not available. +// This will be called as a standard middleware with (req, res, next) - calling +// next will continue and sett up a session as normal, otherwise the app can +// perform a different operation as usual + +class SessionAutostartMiddleware { + constructor() { + this.middleware = this.middleware.bind(this) + this._cookieName = Settings.cookieName + this._noAutostartCallbacks = new Map() + } + + static applyInitialMiddleware(router) { + const middleware = new SessionAutostartMiddleware() + router.sessionAutostartMiddleware = middleware + router.use(middleware.middleware) + } + + disableSessionAutostartForRoute(route, method, callback) { + if (typeof callback !== 'function') { + throw new Error('callback not provided when disabling session autostart') + } + + if (!this._noAutostartCallbacks[route]) { + this._noAutostartCallbacks[route] = new Map() + } + + this._noAutostartCallbacks[route][method] = callback + } + + applyDefaultPostGatewayForRoute(route) { + this.disableSessionAutostartForRoute( + route, + 'POST', + SessionAutostartMiddleware.genericPostGatewayMiddleware + ) + } + + autostartCallbackForRequest(req) { + return ( + this._noAutostartCallbacks[req.path] && + this._noAutostartCallbacks[req.path][req.method] + ) + } + + reqIsBot(req) { + const agent = (req.headers['user-agent'] || '').toLowerCase() + + const foundMatch = botUserAgents.find(botAgent => { + return agent.includes(botAgent) + }) + + if (foundMatch) { + return true + } else { + return false + } + } + + middleware(req, _res, next) { + if (!req.signedCookies[this._cookieName]) { + const callback = this.autostartCallbackForRequest(req) + if (callback) { + req.session = { + noSessionCallback: callback, + } + } else if (this.reqIsBot(req)) { + req.session = { + noSessionCallback: (_req, _res, next) => { + next() + }, + } + } + } + next() + } + + static invokeCallbackMiddleware(req, res, next) { + if (req.session.noSessionCallback) { + return req.session.noSessionCallback(req, res, next) + } + next() + } + + static genericPostGatewayMiddleware(req, res, next) { + if (req.method !== 'POST') { + return next( + new OError('post gateway invoked for non-POST request', { + path: req.path, + method: req.method, + }) + ) + } + + if (req.body.viaGateway) { + return next() + } + + res.render('general/post-gateway', { form_data: req.body }) + } +} + +module.exports = SessionAutostartMiddleware diff --git a/services/web/app/src/infrastructure/SessionStoreManager.js b/services/web/app/src/infrastructure/SessionStoreManager.js new file mode 100644 index 0000000000..a9ef6bdd21 --- /dev/null +++ b/services/web/app/src/infrastructure/SessionStoreManager.js @@ -0,0 +1,74 @@ +const Metrics = require('@overleaf/metrics') +const logger = require('logger-sharelatex') + +function computeValidationToken(req) { + // this should be a deterministic function of the client-side sessionID, + // prepended with a version number in case we want to change it later + return 'v1:' + req.sessionID.slice(-4) +} + +function checkValidationToken(req) { + if (req.session) { + const sessionToken = req.session.validationToken + if (sessionToken) { + const clientToken = computeValidationToken(req) + // Reject invalid sessions. If you change the method for computing the + // token (above) then you need to either check or ignore previous + // versions of the token. + if (sessionToken === clientToken) { + Metrics.inc('security.session', 1, { status: 'ok' }) + return true + } else { + logger.error( + { + sessionToken: sessionToken, + clientToken: clientToken, + }, + 'session token validation failed' + ) + Metrics.inc('security.session', 1, { status: 'error' }) + return false + } + } else { + Metrics.inc('security.session', 1, { status: 'missing' }) + } + } + return true // fallback to allowing session +} + +module.exports = { + enableValidationToken(sessionStore) { + // generate an identifier from the sessionID for every new session + const originalGenerate = sessionStore.generate + sessionStore.generate = function (req) { + originalGenerate(req) + // add the validation token as a property that cannot be overwritten + Object.defineProperty(req.session, 'validationToken', { + value: computeValidationToken(req), + enumerable: true, + writable: false, + }) + Metrics.inc('security.session', 1, { status: 'new' }) + } + }, + + validationMiddleware(req, res, next) { + if (!req.session.noSessionCallback) { + if (!checkValidationToken(req)) { + // the session must exist for it to fail validation + return req.session.destroy(() => { + return next(new Error('invalid session')) + }) + } + } + next() + }, + + hasValidationToken(req) { + if (req && req.session && req.session.validationToken) { + return true + } else { + return false + } + }, +} diff --git a/services/web/app/src/infrastructure/Translations.js b/services/web/app/src/infrastructure/Translations.js new file mode 100644 index 0000000000..af92ca5f08 --- /dev/null +++ b/services/web/app/src/infrastructure/Translations.js @@ -0,0 +1,99 @@ +const i18n = require('i18next') +const fsBackend = require('i18next-fs-backend') +const middleware = require('i18next-http-middleware') +const path = require('path') +const Settings = require('@overleaf/settings') +const { URL } = require('url') + +const fallbackLanguageCode = Settings.i18n.defaultLng || 'en' +const availableLanguageCodes = [] +const availableHosts = new Map() +const subdomainConfigs = new Map() +Object.values(Settings.i18n.subdomainLang || {}).forEach(function (spec) { + availableLanguageCodes.push(spec.lngCode) + // prebuild a host->lngCode mapping for the usage at runtime in the + // middleware + availableHosts.set(new URL(spec.url).host, spec.lngCode) + + // prebuild a lngCode -> language config mapping; some subdomains should + // not appear in the language picker + if (!spec.hide) { + subdomainConfigs.set(spec.lngCode, spec) + } +}) +if (!availableLanguageCodes.includes(fallbackLanguageCode)) { + // always load the fallback locale + availableLanguageCodes.push(fallbackLanguageCode) +} + +i18n + .use(fsBackend) + .use(middleware.LanguageDetector) + .init({ + backend: { + loadPath: path.join(__dirname, '../../../locales/__lng__.json'), + }, + + // Load translation files synchronously: https://www.i18next.com/overview/configuration-options#initimmediate + initImmediate: false, + + // We use the legacy v1 JSON format, so configure interpolator to use + // underscores instead of curly braces + interpolation: { + prefix: '__', + suffix: '__', + unescapeSuffix: 'HTML', + // Disable escaping of interpolated values for backwards compatibility. + // We escape the value after it's translated in web, so there's no + // security risk + escapeValue: Settings.i18n.escapeHTMLInVars, + // Disable nesting in interpolated values, preventing user input + // injection via another nested value + skipOnVariables: true, + }, + + preload: availableLanguageCodes, + supportedLngs: availableLanguageCodes, + fallbackLng: fallbackLanguageCode, + }) + +// Make custom language detector for Accept-Language header +const headerLangDetector = new middleware.LanguageDetector(i18n.services, { + order: ['header'], +}) + +function setLangBasedOnDomainMiddleware(req, res, next) { + // Determine language from subdomain + const lang = availableHosts.get(req.headers.host) + if (lang) { + req.i18n.changeLanguage(lang) + } + + // expose the language code to pug + res.locals.currentLngCode = req.language + + // If the set language is different from the language detection (based on + // the Accept-Language header), then set flag which will show a banner + // offering to switch to the appropriate library + const detectedLanguageCode = headerLangDetector.detect(req, res) + if (req.language !== detectedLanguageCode) { + res.locals.suggestedLanguageSubdomainConfig = subdomainConfigs.get( + detectedLanguageCode + ) + } + + // Decorate req.i18n with translate function alias for backwards + // compatibility usage in requests + req.i18n.translate = req.i18n.t + next() +} + +// Decorate i18n with translate function alias for backwards compatibility +// in direct usage +i18n.translate = i18n.t + +module.exports = { + i18nMiddleware: middleware.handle(i18n), + setLangBasedOnDomainMiddleware, + i18n, +} diff --git a/services/web/app/src/infrastructure/UnsupportedBrowserMiddleware.js b/services/web/app/src/infrastructure/UnsupportedBrowserMiddleware.js new file mode 100644 index 0000000000..d7cf9db39e --- /dev/null +++ b/services/web/app/src/infrastructure/UnsupportedBrowserMiddleware.js @@ -0,0 +1,45 @@ +const Bowser = require('bowser') +const Settings = require('@overleaf/settings') +const Url = require('url') +const { getSafeRedirectPath } = require('../Features/Helpers/UrlHelper') + +function unsupportedBrowserMiddleware(req, res, next) { + if (!Settings.unsupportedBrowsers) return next() + + const userAgent = req.headers['user-agent'] + + if (!userAgent) return next() + + const parser = Bowser.getParser(userAgent) + + // Allow bots through by only ignoring bots or unrecognised UA strings + const isBot = parser.isPlatform('bot') || !parser.getBrowserName() + if (isBot) return next() + + const isUnsupported = parser.satisfies(Settings.unsupportedBrowsers) + if (isUnsupported) { + return res.redirect( + Url.format({ + pathname: '/unsupported-browser', + query: { fromURL: req.originalUrl }, + }) + ) + } + + next() +} + +function renderUnsupportedBrowserPage(req, res) { + let fromURL + if (typeof req.query.fromURL === 'string') { + try { + fromURL = Settings.siteUrl + getSafeRedirectPath(req.query.fromURL) + } catch (e) {} + } + res.render('general/unsupported-browser', { fromURL }) +} + +module.exports = { + renderUnsupportedBrowserPage, + unsupportedBrowserMiddleware, +} diff --git a/services/web/app/src/infrastructure/Validation.js b/services/web/app/src/infrastructure/Validation.js new file mode 100644 index 0000000000..49f4c6d8a4 --- /dev/null +++ b/services/web/app/src/infrastructure/Validation.js @@ -0,0 +1,32 @@ +const { Joi: CelebrateJoi, celebrate, errors } = require('celebrate') +const { ObjectId } = require('mongodb') + +const objectIdValidator = { + name: 'objectId', + language: { + invalid: 'needs to be a valid ObjectId', + }, + pre(value, state, options) { + if (!ObjectId.isValid(value)) { + return this.createError('objectId.invalid', { value }, state, options) + } + + if (options.convert) { + return new ObjectId(value) + } + + return value + }, +} + +const Joi = CelebrateJoi.extend(objectIdValidator) +const errorMiddleware = errors() + +module.exports = { Joi, validate, errorMiddleware } + +/** + * Validation middleware + */ +function validate(schema) { + return celebrate(schema, { allowUnknown: true }) +} diff --git a/services/web/app/src/infrastructure/Views.js b/services/web/app/src/infrastructure/Views.js new file mode 100644 index 0000000000..e1323a823d --- /dev/null +++ b/services/web/app/src/infrastructure/Views.js @@ -0,0 +1,53 @@ +const logger = require('logger-sharelatex') +const pug = require('pug') +const globby = require('globby') +const Settings = require('@overleaf/settings') +const path = require('path') + +// Generate list of view names from app/views + +const viewList = globby + .sync('app/views/**/*.pug', { + onlyFiles: true, + concurrency: 1, + ignore: '**/_*.pug', + }) + .concat( + globby.sync('modules/*/app/views/**/*.pug', { + onlyFiles: true, + concurrency: 1, + ignore: '**/_*.pug', + }) + ) + .map(x => { + return x.replace(/\.pug$/, '') // strip trailing .pug extension + }) + .filter(x => { + return !/^_/.test(x) + }) + +module.exports = { + precompileViews(app) { + const startTime = Date.now() + let success = 0 + let failures = 0 + viewList.forEach(view => { + const filename = path.resolve(view + '.pug') // express views are cached using the absolute path + try { + pug.compileFile(filename, { + cache: true, + compileDebug: Settings.debugPugTemplates, + }) + logger.log({ filename }, 'compiled') + success++ + } catch (err) { + logger.error({ filename, err: err.message }, 'error compiling') + failures++ + } + }) + logger.log( + { timeTaken: Date.now() - startTime, failures, success }, + 'compiled templates' + ) + }, +} diff --git a/services/web/app/src/infrastructure/mongodb.js b/services/web/app/src/infrastructure/mongodb.js new file mode 100644 index 0000000000..408f174906 --- /dev/null +++ b/services/web/app/src/infrastructure/mongodb.js @@ -0,0 +1,93 @@ +const Settings = require('@overleaf/settings') +const { MongoClient, ObjectId } = require('mongodb') + +if ( + typeof global.beforeEach === 'function' && + process.argv.join(' ').match(/unit/) +) { + throw new Error( + 'It looks like unit tests are running, but you are connecting to Mongo. Missing a stub?' + ) +} + +const clientPromise = MongoClient.connect( + Settings.mongo.url, + Settings.mongo.options +) + +let setupDbPromise +async function waitForDb() { + if (!setupDbPromise) { + setupDbPromise = setupDb() + } + await setupDbPromise +} + +const db = {} +async function setupDb() { + const internalDb = (await clientPromise).db() + + db.contacts = internalDb.collection('contacts') + db.deletedFiles = internalDb.collection('deletedFiles') + db.deletedProjects = internalDb.collection('deletedProjects') + db.deletedSubscriptions = internalDb.collection('deletedSubscriptions') + db.deletedUsers = internalDb.collection('deletedUsers') + db.docHistory = internalDb.collection('docHistory') + db.docHistoryIndex = internalDb.collection('docHistoryIndex') + db.docOps = internalDb.collection('docOps') + db.docSnapshots = internalDb.collection('docSnapshots') + db.docs = internalDb.collection('docs') + db.githubSyncEntityVersions = internalDb.collection( + 'githubSyncEntityVersions' + ) + db.githubSyncProjectStates = internalDb.collection('githubSyncProjectStates') + db.githubSyncUserCredentials = internalDb.collection( + 'githubSyncUserCredentials' + ) + db.institutions = internalDb.collection('institutions') + db.messages = internalDb.collection('messages') + db.migrations = internalDb.collection('migrations') + db.notifications = internalDb.collection('notifications') + db.oauthAccessTokens = internalDb.collection('oauthAccessTokens') + db.oauthApplications = internalDb.collection('oauthApplications') + db.oauthAuthorizationCodes = internalDb.collection('oauthAuthorizationCodes') + db.projectHistoryFailures = internalDb.collection('projectHistoryFailures') + db.projectHistoryLabels = internalDb.collection('projectHistoryLabels') + db.projectHistoryMetaData = internalDb.collection('projectHistoryMetaData') + db.projectHistorySyncState = internalDb.collection('projectHistorySyncState') + db.projectInvites = internalDb.collection('projectInvites') + db.projects = internalDb.collection('projects') + db.publishers = internalDb.collection('publishers') + db.rooms = internalDb.collection('rooms') + db.samlCache = internalDb.collection('samlCache') + db.samlLogs = internalDb.collection('samlLogs') + db.spellingPreferences = internalDb.collection('spellingPreferences') + db.subscriptions = internalDb.collection('subscriptions') + db.systemmessages = internalDb.collection('systemmessages') + db.tags = internalDb.collection('tags') + db.teamInvites = internalDb.collection('teamInvites') + db.templates = internalDb.collection('templates') + db.tokens = internalDb.collection('tokens') + db.users = internalDb.collection('users') + db.userstubs = internalDb.collection('userstubs') +} +async function addCollection(name) { + await waitForDb() + const internalDb = (await clientPromise).db() + + db[name] = internalDb.collection(name) +} +async function getCollectionNames() { + const internalDb = (await clientPromise).db() + + const collections = await internalDb.collections() + return collections.map(collection => collection.collectionName) +} + +module.exports = { + db, + ObjectId, + addCollection, + getCollectionNames, + waitForDb, +} diff --git a/services/web/app/src/models/DeletedFile.js b/services/web/app/src/models/DeletedFile.js new file mode 100644 index 0000000000..b7f56bee05 --- /dev/null +++ b/services/web/app/src/models/DeletedFile.js @@ -0,0 +1,21 @@ +const mongoose = require('../infrastructure/Mongoose') +const { Schema } = mongoose + +const DeletedFileSchema = new Schema( + { + name: String, + projectId: Schema.ObjectId, + created: { + type: Date, + }, + linkedFileData: { type: Schema.Types.Mixed }, + hash: { + type: String, + }, + deletedAt: { type: Date }, + }, + { collection: 'deletedFiles' } +) + +exports.DeletedFile = mongoose.model('DeletedFile', DeletedFileSchema) +exports.DeletedFileSchema = DeletedFileSchema diff --git a/services/web/app/src/models/DeletedProject.js b/services/web/app/src/models/DeletedProject.js new file mode 100644 index 0000000000..8f1070a06b --- /dev/null +++ b/services/web/app/src/models/DeletedProject.js @@ -0,0 +1,33 @@ +const mongoose = require('../infrastructure/Mongoose') +const { ProjectSchema } = require('./Project') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const DeleterDataSchema = new Schema({ + deleterId: { type: ObjectId, ref: 'User' }, + deleterIpAddress: { type: String }, + deletedAt: { type: Date }, + deletedProjectId: { type: ObjectId }, + deletedProjectOwnerId: { type: ObjectId, ref: 'User' }, + deletedProjectCollaboratorIds: [{ type: ObjectId, ref: 'User' }], + deletedProjectReadOnlyIds: [{ type: ObjectId, ref: 'User' }], + deletedProjectReadWriteTokenAccessIds: [{ type: ObjectId, ref: 'User' }], + deletedProjectReadOnlyTokenAccessIds: [{ type: ObjectId, ref: 'User' }], + deletedProjectReadWriteToken: { type: String }, + deletedProjectReadOnlyToken: { type: String }, + deletedProjectLastUpdatedAt: { type: Date }, + deletedProjectOverleafId: { type: Number }, + deletedProjectOverleafHistoryId: { type: Number }, +}) + +const DeletedProjectSchema = new Schema( + { + deleterData: DeleterDataSchema, + project: ProjectSchema, + }, + { collection: 'deletedProjects' } +) + +exports.DeletedProject = mongoose.model('DeletedProject', DeletedProjectSchema) +exports.DeletedProjectSchema = DeletedProjectSchema diff --git a/services/web/app/src/models/DeletedSubscription.js b/services/web/app/src/models/DeletedSubscription.js new file mode 100644 index 0000000000..0aa387255b --- /dev/null +++ b/services/web/app/src/models/DeletedSubscription.js @@ -0,0 +1,34 @@ +const mongoose = require('../infrastructure/Mongoose') +const { SubscriptionSchema } = require('./Subscription') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const DeleterDataSchema = new Schema( + { + deleterId: { type: ObjectId, ref: 'User' }, + deleterIpAddress: { type: String }, + deletedAt: { + type: Date, + default() { + return new Date() + }, + }, + }, + { _id: false } +) + +const DeletedSubscriptionSchema = new Schema( + { + deleterData: DeleterDataSchema, + subscription: SubscriptionSchema, + }, + { collection: 'deletedSubscriptions' } +) + +exports.DeletedSubscription = mongoose.model( + 'DeletedSubscription', + DeletedSubscriptionSchema +) + +exports.DeletedSubscriptionSchema = DeletedSubscriptionSchema diff --git a/services/web/app/src/models/DeletedUser.js b/services/web/app/src/models/DeletedUser.js new file mode 100644 index 0000000000..579862819f --- /dev/null +++ b/services/web/app/src/models/DeletedUser.js @@ -0,0 +1,31 @@ +const mongoose = require('../infrastructure/Mongoose') +const { UserSchema } = require('./User') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const DeleterDataSchema = new Schema({ + deleterId: { type: ObjectId, ref: 'User' }, + deleterIpAddress: { type: String }, + deletedAt: { type: Date }, + deletedUserId: { type: ObjectId }, + deletedUserLastLoggedIn: { type: Date }, + deletedUserSignUpDate: { type: Date }, + deletedUserLoginCount: { type: Number }, + deletedUserReferralId: { type: String }, + deletedUserReferredUsers: [{ type: ObjectId, ref: 'User' }], + deletedUserReferredUserCount: { type: Number }, + deletedUserOverleafId: { type: Number }, +}) + +const DeletedUserSchema = new Schema( + { + deleterData: DeleterDataSchema, + user: UserSchema, + }, + { collection: 'deletedUsers' } +) + +exports.DeletedUser = mongoose.model('DeletedUser', DeletedUserSchema) + +exports.DeletedUserSchema = DeletedUserSchema diff --git a/services/web/app/src/models/Doc.js b/services/web/app/src/models/Doc.js new file mode 100644 index 0000000000..0653d410a8 --- /dev/null +++ b/services/web/app/src/models/Doc.js @@ -0,0 +1,11 @@ +const mongoose = require('../infrastructure/Mongoose') + +const { Schema } = mongoose + +const DocSchema = new Schema({ + name: { type: String, default: 'new doc' }, +}) + +exports.Doc = mongoose.model('Doc', DocSchema) + +exports.DocSchema = DocSchema diff --git a/services/web/app/src/models/DocSnapshot.js b/services/web/app/src/models/DocSnapshot.js new file mode 100644 index 0000000000..0c25a367df --- /dev/null +++ b/services/web/app/src/models/DocSnapshot.js @@ -0,0 +1,18 @@ +const mongoose = require('../infrastructure/Mongoose') + +const { Schema } = mongoose + +const DocSnapshotSchema = new Schema( + { + project_id: Schema.Types.ObjectId, + doc_id: Schema.Types.ObjectId, + version: Number, + lines: [String], + pathname: String, + ranges: Schema.Types.Mixed, + ts: Date, + }, + { collection: 'docSnapshots' } +) + +exports.DocSnapshot = mongoose.model('DocSnapshot', DocSnapshotSchema) diff --git a/services/web/app/src/models/File.js b/services/web/app/src/models/File.js new file mode 100644 index 0000000000..5bc5d523fe --- /dev/null +++ b/services/web/app/src/models/File.js @@ -0,0 +1,24 @@ +const mongoose = require('../infrastructure/Mongoose') + +const { Schema } = mongoose + +const FileSchema = new Schema({ + name: { + type: String, + default: '', + }, + created: { + type: Date, + default() { + return new Date() + }, + }, + rev: { type: Number, default: 0 }, + linkedFileData: { type: Schema.Types.Mixed }, + hash: { + type: String, + }, +}) + +exports.File = mongoose.model('File', FileSchema) +exports.FileSchema = FileSchema diff --git a/services/web/app/src/models/Folder.js b/services/web/app/src/models/Folder.js new file mode 100644 index 0000000000..989f3fde5e --- /dev/null +++ b/services/web/app/src/models/Folder.js @@ -0,0 +1,18 @@ +const mongoose = require('../infrastructure/Mongoose') +const { DocSchema } = require('./Doc') +const { FileSchema } = require('./File') + +const { Schema } = mongoose + +const FolderSchema = new Schema({ + name: { type: String, default: 'new folder' }, +}) + +FolderSchema.add({ + docs: [DocSchema], + fileRefs: [FileSchema], + folders: [FolderSchema], +}) + +exports.Folder = mongoose.model('Folder', FolderSchema) +exports.FolderSchema = FolderSchema diff --git a/services/web/app/src/models/Institution.js b/services/web/app/src/models/Institution.js new file mode 100644 index 0000000000..7b44012405 --- /dev/null +++ b/services/web/app/src/models/Institution.js @@ -0,0 +1,41 @@ +const mongoose = require('../infrastructure/Mongoose') +const { Schema } = mongoose +const { ObjectId } = Schema +const settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const request = require('request') + +const InstitutionSchema = new Schema({ + v1Id: { type: Number, required: true }, + managerIds: [{ type: ObjectId, ref: 'User' }], + metricsEmail: { + optedOutUserIds: [{ type: ObjectId, ref: 'User' }], + lastSent: { type: Date }, + }, +}) + +// fetch institution's data from v1 API. Errors are ignored +InstitutionSchema.method('fetchV1Data', function (callback) { + const url = `${settings.apis.v1.url}/universities/list/${this.v1Id}` + request.get(url, (error, response, body) => { + let parsedBody + try { + parsedBody = JSON.parse(body) + } catch (error1) { + // log error and carry on without v1 data + error = error1 + logger.err( + { model: 'Institution', v1Id: this.v1Id, error }, + '[fetchV1DataError]' + ) + } + this.name = parsedBody != null ? parsedBody.name : undefined + this.countryCode = parsedBody != null ? parsedBody.country_code : undefined + this.departments = parsedBody != null ? parsedBody.departments : undefined + this.portalSlug = parsedBody != null ? parsedBody.portal_slug : undefined + callback(null, this) + }) +}) + +exports.Institution = mongoose.model('Institution', InstitutionSchema) +exports.InstitutionSchema = InstitutionSchema diff --git a/services/web/app/src/models/OauthAccessToken.js b/services/web/app/src/models/OauthAccessToken.js new file mode 100644 index 0000000000..6caa4d1c17 --- /dev/null +++ b/services/web/app/src/models/OauthAccessToken.js @@ -0,0 +1,26 @@ +const mongoose = require('../infrastructure/Mongoose') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const OauthAccessTokenSchema = new Schema( + { + accessToken: String, + accessTokenExpiresAt: Date, + oauthApplication_id: { type: ObjectId, ref: 'OauthApplication' }, + refreshToken: String, + refreshTokenExpiresAt: Date, + scope: String, + user_id: { type: ObjectId, ref: 'User' }, + }, + { + collection: 'oauthAccessTokens', + } +) + +exports.OauthAccessToken = mongoose.model( + 'OauthAccessToken', + OauthAccessTokenSchema +) + +exports.OauthAccessTokenSchema = OauthAccessTokenSchema diff --git a/services/web/app/src/models/OauthApplication.js b/services/web/app/src/models/OauthApplication.js new file mode 100644 index 0000000000..cd7e4dccdb --- /dev/null +++ b/services/web/app/src/models/OauthApplication.js @@ -0,0 +1,24 @@ +const mongoose = require('../infrastructure/Mongoose') + +const { Schema } = mongoose + +const OauthApplicationSchema = new Schema( + { + id: String, + clientSecret: String, + grants: [String], + name: String, + redirectUris: [String], + scopes: [String], + }, + { + collection: 'oauthApplications', + } +) + +exports.OauthApplication = mongoose.model( + 'OauthApplication', + OauthApplicationSchema +) + +exports.OauthApplicationSchema = OauthApplicationSchema diff --git a/services/web/app/src/models/OauthAuthorizationCode.js b/services/web/app/src/models/OauthAuthorizationCode.js new file mode 100644 index 0000000000..d67b920f2c --- /dev/null +++ b/services/web/app/src/models/OauthAuthorizationCode.js @@ -0,0 +1,25 @@ +const mongoose = require('../infrastructure/Mongoose') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const OauthAuthorizationCodeSchema = new Schema( + { + authorizationCode: String, + expiresAt: Date, + oauthApplication_id: { type: ObjectId, ref: 'OauthApplication' }, + redirectUri: String, + scope: String, + user_id: { type: ObjectId, ref: 'User' }, + }, + { + collection: 'oauthAuthorizationCodes', + } +) + +exports.OauthAuthorizationCode = mongoose.model( + 'OauthAuthorizationCode', + OauthAuthorizationCodeSchema +) + +exports.OauthAuthorizationCodeSchema = OauthAuthorizationCodeSchema diff --git a/services/web/app/src/models/Project.js b/services/web/app/src/models/Project.js new file mode 100644 index 0000000000..c27801c9f0 --- /dev/null +++ b/services/web/app/src/models/Project.js @@ -0,0 +1,141 @@ +const mongoose = require('../infrastructure/Mongoose') +const _ = require('underscore') +const { FolderSchema } = require('./Folder') +const Errors = require('../Features/Errors/Errors') + +const concreteObjectId = mongoose.Types.ObjectId +const { Schema } = mongoose +const { ObjectId } = Schema + +const DeletedDocSchema = new Schema({ + name: String, + deletedAt: { type: Date }, +}) + +const DeletedFileSchema = new Schema({ + name: String, + created: { + type: Date, + }, + linkedFileData: { type: Schema.Types.Mixed }, + hash: { + type: String, + }, + deletedAt: { type: Date }, +}) + +const AuditLogEntrySchema = new Schema({ + _id: false, + operation: { type: String }, + initiatorId: { type: Schema.Types.ObjectId }, + timestamp: { type: Date }, + info: { type: Object }, +}) + +const ProjectSchema = new Schema({ + name: { type: String, default: 'new project' }, + lastUpdated: { + type: Date, + default() { + return new Date() + }, + }, + lastUpdatedBy: { type: ObjectId, ref: 'User' }, + lastOpened: { type: Date }, + active: { type: Boolean, default: true }, + owner_ref: { type: ObjectId, ref: 'User' }, + collaberator_refs: [{ type: ObjectId, ref: 'User' }], + readOnly_refs: [{ type: ObjectId, ref: 'User' }], + rootDoc_id: { type: ObjectId }, + rootFolder: [FolderSchema], + version: { type: Number }, // incremented for every change in the project structure (folders and filenames) + publicAccesLevel: { type: String, default: 'private' }, + compiler: { type: String, default: 'pdflatex' }, + spellCheckLanguage: { type: String, default: 'en' }, + deletedByExternalDataSource: { type: Boolean, default: false }, + description: { type: String, default: '' }, + archived: { type: Schema.Types.Mixed }, + trashed: [{ type: ObjectId, ref: 'User' }], + deletedDocs: [DeletedDocSchema], + deletedFiles: [DeletedFileSchema], + imageName: { type: String }, + brandVariationId: { type: String }, + track_changes: { type: Object }, + tokens: { + readOnly: { + type: String, + index: { + unique: true, + partialFilterExpression: { 'tokens.readOnly': { $exists: true } }, + }, + }, + readAndWrite: { + type: String, + index: { + unique: true, + partialFilterExpression: { 'tokens.readAndWrite': { $exists: true } }, + }, + }, + readAndWritePrefix: { + type: String, + index: { + unique: true, + partialFilterExpression: { + 'tokens.readAndWritePrefix': { $exists: true }, + }, + }, + }, + }, + tokenAccessReadOnly_refs: [{ type: ObjectId, ref: 'User' }], + tokenAccessReadAndWrite_refs: [{ type: ObjectId, ref: 'User' }], + fromV1TemplateId: { type: Number }, + fromV1TemplateVersionId: { type: Number }, + overleaf: { + id: { type: Number }, + imported_at_ver_id: { type: Number }, + token: { type: String }, + read_token: { type: String }, + history: { + id: { type: Number }, + display: { type: Boolean }, + upgradedAt: { type: Date }, + }, + }, + collabratecUsers: [ + { + user_id: { type: ObjectId, ref: 'User' }, + collabratec_document_id: { type: String }, + collabratec_privategroup_id: { type: String }, + added_at: { + type: Date, + default() { + return new Date() + }, + }, + }, + ], + auditLog: [AuditLogEntrySchema], + deferredTpdsFlushCounter: { type: Number }, +}) + +ProjectSchema.statics.getProject = function (projectOrId, fields, callback) { + if (projectOrId._id != null) { + callback(null, projectOrId) + } else { + try { + concreteObjectId(projectOrId.toString()) + } catch (e) { + return callback(new Errors.NotFoundError(e.message)) + } + this.findById(projectOrId, fields, callback) + } +} + +function applyToAllFilesRecursivly(folder, fun) { + _.each(folder.fileRefs, file => fun(file)) + _.each(folder.folders, folder => applyToAllFilesRecursivly(folder, fun)) +} +ProjectSchema.statics.applyToAllFilesRecursivly = applyToAllFilesRecursivly + +exports.Project = mongoose.model('Project', ProjectSchema) +exports.ProjectSchema = ProjectSchema diff --git a/services/web/app/src/models/ProjectHistoryFailure.js b/services/web/app/src/models/ProjectHistoryFailure.js new file mode 100644 index 0000000000..b28fb07c87 --- /dev/null +++ b/services/web/app/src/models/ProjectHistoryFailure.js @@ -0,0 +1,24 @@ +const mongoose = require('../infrastructure/Mongoose') + +const { Schema } = mongoose + +const ProjectHistoryFailureSchema = new Schema( + { + project_id: Schema.Types.ObjectId, + ts: Date, + queueSize: Number, + error: String, + stack: String, + attempts: Number, + history: Schema.Types.Mixed, + resyncStartedAt: Date, + resyncAttempts: Number, + requestCount: Number, + }, + { collection: 'projectHistoryFailures' } +) + +exports.ProjectHistoryFailure = mongoose.model( + 'ProjectHistoryFailure', + ProjectHistoryFailureSchema +) diff --git a/services/web/app/src/models/ProjectInvite.js b/services/web/app/src/models/ProjectInvite.js new file mode 100644 index 0000000000..3dc792848c --- /dev/null +++ b/services/web/app/src/models/ProjectInvite.js @@ -0,0 +1,35 @@ +const mongoose = require('../infrastructure/Mongoose') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const EXPIRY_IN_SECONDS = 60 * 60 * 24 * 30 + +const ExpiryDate = function () { + const timestamp = new Date() + timestamp.setSeconds(timestamp.getSeconds() + EXPIRY_IN_SECONDS) + return timestamp +} + +const ProjectInviteSchema = new Schema( + { + email: String, + token: String, + sendingUserId: ObjectId, + projectId: ObjectId, + privileges: String, + createdAt: { type: Date, default: Date.now }, + expires: { + type: Date, + default: ExpiryDate, + index: { expireAfterSeconds: 10 }, + }, + }, + { + collection: 'projectInvites', + } +) + +exports.ProjectInvite = mongoose.model('ProjectInvite', ProjectInviteSchema) +exports.ProjectInviteSchema = ProjectInviteSchema +exports.EXPIRY_IN_SECONDS = EXPIRY_IN_SECONDS diff --git a/services/web/app/src/models/Publisher.js b/services/web/app/src/models/Publisher.js new file mode 100644 index 0000000000..b19a0b4cac --- /dev/null +++ b/services/web/app/src/models/Publisher.js @@ -0,0 +1,46 @@ +const mongoose = require('../infrastructure/Mongoose') +const { Schema } = mongoose +const { ObjectId } = Schema +const settings = require('@overleaf/settings') +const logger = require('logger-sharelatex') +const request = require('request') + +const PublisherSchema = new Schema({ + slug: { type: String, required: true }, + managerIds: [{ type: ObjectId, ref: 'User' }], +}) + +// fetch publisher's (brand on v1) data from v1 API. Errors are ignored +PublisherSchema.method('fetchV1Data', function (callback) { + request( + { + baseUrl: settings.apis.v1.url, + url: `/api/v2/brands/${this.slug}`, + method: 'GET', + auth: { + user: settings.apis.v1.user, + pass: settings.apis.v1.pass, + sendImmediately: true, + }, + }, + (error, response, body) => { + let parsedBody + try { + parsedBody = JSON.parse(body) + } catch (error1) { + // log error and carry on without v1 data + error = error1 + logger.err( + { model: 'Publisher', slug: this.slug, error }, + '[fetchV1DataError]' + ) + } + this.name = parsedBody != null ? parsedBody.name : undefined + this.partner = parsedBody != null ? parsedBody.partner : undefined + callback(null, this) + } + ) +}) + +exports.Publisher = mongoose.model('Publisher', PublisherSchema) +exports.PublisherSchema = PublisherSchema diff --git a/services/web/app/src/models/SamlCache.js b/services/web/app/src/models/SamlCache.js new file mode 100644 index 0000000000..9c3bf642c7 --- /dev/null +++ b/services/web/app/src/models/SamlCache.js @@ -0,0 +1,15 @@ +const mongoose = require('../infrastructure/Mongoose') +const { Schema } = mongoose + +const SamlCacheSchema = new Schema( + { + createdAt: { type: Date }, + requestId: { type: String }, + }, + { + collection: 'samlCache', + } +) + +exports.SamlCache = mongoose.model('SamlCache', SamlCacheSchema) +exports.SamlCacheSchema = SamlCacheSchema diff --git a/services/web/app/src/models/SamlLog.js b/services/web/app/src/models/SamlLog.js new file mode 100644 index 0000000000..234a25da8b --- /dev/null +++ b/services/web/app/src/models/SamlLog.js @@ -0,0 +1,18 @@ +const mongoose = require('../infrastructure/Mongoose') +const { Schema } = mongoose + +const SamlLogSchema = new Schema( + { + createdAt: { type: Date, default: () => new Date() }, + data: { type: Object }, + jsonData: { type: String }, + providerId: { type: String, default: '' }, + sessionId: { type: String, default: '' }, + }, + { + collection: 'samlLogs', + } +) + +exports.SamlLog = mongoose.model('SamlLog', SamlLogSchema) +exports.SamlLogSchema = SamlLogSchema diff --git a/services/web/app/src/models/SplitTest.js b/services/web/app/src/models/SplitTest.js new file mode 100644 index 0000000000..dc998f6d4d --- /dev/null +++ b/services/web/app/src/models/SplitTest.js @@ -0,0 +1,111 @@ +const mongoose = require('../infrastructure/Mongoose') +const { Schema } = mongoose +const _ = require('lodash') + +const MIN_NAME_LENGTH = 3 +const MAX_NAME_LENGTH = 200 +const MIN_VARIANT_NAME_LENGTH = 3 +const MAX_VARIANT_NAME_LENGTH = 255 +const NAME_REGEX = /^[a-zA-Z0-9\-_]+$/ + +const RolloutPercentType = { + type: Number, + default: 0, + min: [0, 'Rollout percentage must be between 0 and 100, got {VALUE}'], + max: [100, 'Rollout percentage must be between 0 and 100, got {VALUE}'], + required: true, +} + +const VariantSchema = new Schema( + { + name: { + type: String, + minLength: MIN_VARIANT_NAME_LENGTH, + maxLength: MAX_VARIANT_NAME_LENGTH, + required: true, + validate: { + validator: function (input) { + return input !== null && input !== 'default' && NAME_REGEX.test(input) + }, + message: `invalid, cannot be 'default' and must match: ${NAME_REGEX}, got {VALUE}`, + }, + }, + active: { + type: Boolean, + default: true, + required: true, + }, + rolloutPercent: RolloutPercentType, + rolloutStripes: [ + { + start: RolloutPercentType, + end: RolloutPercentType, + }, + ], + }, + { _id: false } +) + +const VersionSchema = new Schema( + { + versionNumber: { + type: Number, + default: 1, + min: [1, 'must be 1 or higher, got {VALUE}'], + required: true, + }, + phase: { + type: String, + default: 'alpha', + enum: ['alpha', 'beta', 'release'], + required: true, + }, + active: { + type: Boolean, + default: true, + required: true, + }, + variants: [VariantSchema], + }, + { _id: false } +) + +const SplitTestSchema = new Schema({ + name: { + type: String, + minLength: MIN_NAME_LENGTH, + maxlength: MAX_NAME_LENGTH, + required: true, + unique: true, + validate: { + validator: function (input) { + return input !== null && NAME_REGEX.test(input) + }, + message: `invalid, must match: ${NAME_REGEX}`, + }, + }, + versions: [VersionSchema], + forbidReleasePhase: { + type: Boolean, + required: false, + }, +}) + +SplitTestSchema.methods.getCurrentVersion = function () { + if (this.versions && this.versions.length > 0) { + return _.maxBy(this.versions, 'versionNumber') + } else { + return undefined + } +} + +SplitTestSchema.methods.getVersion = function (versionNumber) { + return _.find(this.versions || [], { + versionNumber, + }) +} + +module.exports = { + SplitTest: mongoose.model('SplitTest', SplitTestSchema), + SplitTestSchema, +} diff --git a/services/web/app/src/models/Subscription.js b/services/web/app/src/models/Subscription.js new file mode 100644 index 0000000000..f8f6459484 --- /dev/null +++ b/services/web/app/src/models/Subscription.js @@ -0,0 +1,49 @@ +const mongoose = require('../infrastructure/Mongoose') +const { TeamInviteSchema } = require('./TeamInvite') + +const { Schema } = mongoose +const { ObjectId } = Schema + +const SubscriptionSchema = new Schema({ + admin_id: { + type: ObjectId, + ref: 'User', + index: { unique: true, dropDups: true }, + }, + manager_ids: { + type: [ObjectId], + ref: 'User', + required: true, + validate: function (managers) { + // require at least one manager + return !!managers.length + }, + }, + member_ids: [{ type: ObjectId, ref: 'User' }], + invited_emails: [String], + teamInvites: [TeamInviteSchema], + recurlySubscription_id: String, + teamName: { type: String }, + teamNotice: { type: String }, + planCode: { type: String }, + groupPlan: { type: Boolean, default: false }, + membersLimit: { type: Number, default: 0 }, + customAccount: Boolean, + overleaf: { + id: { + type: Number, + index: { + unique: true, + partialFilterExpression: { 'overleaf.id': { $exists: true } }, + }, + }, + }, +}) + +// Subscriptions have no v1 data to fetch +SubscriptionSchema.method('fetchV1Data', function (callback) { + callback(null, this) +}) + +exports.Subscription = mongoose.model('Subscription', SubscriptionSchema) +exports.SubscriptionSchema = SubscriptionSchema diff --git a/services/web/app/src/models/SystemMessage.js b/services/web/app/src/models/SystemMessage.js new file mode 100644 index 0000000000..af263fafc8 --- /dev/null +++ b/services/web/app/src/models/SystemMessage.js @@ -0,0 +1,9 @@ +const mongoose = require('../infrastructure/Mongoose') + +const { Schema } = mongoose + +const SystemMessageSchema = new Schema({ + content: { type: String, default: '' }, +}) + +exports.SystemMessage = mongoose.model('SystemMessage', SystemMessageSchema) diff --git a/services/web/app/src/models/Tag.js b/services/web/app/src/models/Tag.js new file mode 100644 index 0000000000..0f4b23db30 --- /dev/null +++ b/services/web/app/src/models/Tag.js @@ -0,0 +1,14 @@ +const mongoose = require('../infrastructure/Mongoose') +const { Schema } = mongoose + +// Note that for legacy reasons, user_id and project_ids are plain strings, +// not ObjectIds. + +const TagSchema = new Schema({ + user_id: { type: String, required: true }, + name: { type: String, required: true }, + project_ids: [String], +}) + +exports.Tag = mongoose.model('Tag', TagSchema) +exports.TagSchema = TagSchema diff --git a/services/web/app/src/models/TeamInvite.js b/services/web/app/src/models/TeamInvite.js new file mode 100644 index 0000000000..9600e2054f --- /dev/null +++ b/services/web/app/src/models/TeamInvite.js @@ -0,0 +1,13 @@ +const mongoose = require('../infrastructure/Mongoose') + +const { Schema } = mongoose + +const TeamInviteSchema = new Schema({ + email: { type: String, required: true }, + token: { type: String }, + inviterName: { type: String }, + sentAt: { type: Date }, +}) + +exports.TeamInvite = mongoose.model('TeamInvite', TeamInviteSchema) +exports.TeamInviteSchema = TeamInviteSchema diff --git a/services/web/app/src/models/User.js b/services/web/app/src/models/User.js new file mode 100644 index 0000000000..4ba8a810f2 --- /dev/null +++ b/services/web/app/src/models/User.js @@ -0,0 +1,172 @@ +const Settings = require('@overleaf/settings') +const mongoose = require('../infrastructure/Mongoose') +const TokenGenerator = require('../Features/TokenGenerator/TokenGenerator') +const { Schema } = mongoose +const { ObjectId } = Schema + +// See https://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address/574698#574698 +const MAX_EMAIL_LENGTH = 254 + +const AuditLogEntrySchema = new Schema({ + _id: false, + info: { type: Object }, + initiatorId: { type: Schema.Types.ObjectId }, + ipAddress: { type: String }, + operation: { type: String }, + userId: { type: Schema.Types.ObjectId }, + timestamp: { type: Date }, +}) + +const UserSchema = new Schema({ + email: { type: String, default: '', maxlength: MAX_EMAIL_LENGTH }, + emails: [ + { + email: { type: String, default: '', maxlength: MAX_EMAIL_LENGTH }, + reversedHostname: { type: String, default: '' }, + createdAt: { + type: Date, + default() { + return new Date() + }, + }, + confirmedAt: { type: Date }, + samlProviderId: { type: String }, + affiliationUnchecked: { type: Boolean }, + reconfirmedAt: { type: Date }, + }, + ], + first_name: { type: String, default: '' }, + last_name: { type: String, default: '' }, + role: { type: String, default: '' }, + institution: { type: String, default: '' }, + hashedPassword: String, + isAdmin: { type: Boolean, default: false }, + staffAccess: { + publisherMetrics: { type: Boolean, default: false }, + publisherManagement: { type: Boolean, default: false }, + institutionMetrics: { type: Boolean, default: false }, + institutionManagement: { type: Boolean, default: false }, + groupMetrics: { type: Boolean, default: false }, + groupManagement: { type: Boolean, default: false }, + adminMetrics: { type: Boolean, default: false }, + }, + signUpDate: { + type: Date, + default() { + return new Date() + }, + }, + lastLoggedIn: { type: Date }, + lastLoginIp: { type: String, default: '' }, + loginCount: { type: Number, default: 0 }, + holdingAccount: { type: Boolean, default: false }, + ace: { + mode: { type: String, default: 'none' }, + theme: { type: String, default: 'textmate' }, + overallTheme: { type: String, default: '' }, + fontSize: { type: Number, default: '12' }, + autoComplete: { type: Boolean, default: true }, + autoPairDelimiters: { type: Boolean, default: true }, + spellCheckLanguage: { type: String, default: 'en' }, + pdfViewer: { type: String, default: 'pdfjs' }, + syntaxValidation: { type: Boolean }, + fontFamily: { type: String }, + lineHeight: { type: String }, + }, + features: { + collaborators: { + type: Number, + default: Settings.defaultFeatures.collaborators, + }, + versioning: { type: Boolean, default: Settings.defaultFeatures.versioning }, + dropbox: { type: Boolean, default: Settings.defaultFeatures.dropbox }, + github: { type: Boolean, default: Settings.defaultFeatures.github }, + gitBridge: { type: Boolean, default: Settings.defaultFeatures.gitBridge }, + compileTimeout: { + type: Number, + default: Settings.defaultFeatures.compileTimeout, + }, + compileGroup: { + type: String, + default: Settings.defaultFeatures.compileGroup, + }, + templates: { type: Boolean, default: Settings.defaultFeatures.templates }, + references: { type: Boolean, default: Settings.defaultFeatures.references }, + trackChanges: { + type: Boolean, + default: Settings.defaultFeatures.trackChanges, + }, + mendeley: { type: Boolean, default: Settings.defaultFeatures.mendeley }, + zotero: { type: Boolean, default: Settings.defaultFeatures.zotero }, + referencesSearch: { + type: Boolean, + default: Settings.defaultFeatures.referencesSearch, + }, + }, + featuresOverrides: [ + { + createdAt: { + type: Date, + default() { + return new Date() + }, + }, + expiresAt: { type: Date }, + note: { type: String }, + features: { + collaborators: { type: Number }, + versioning: { type: Boolean }, + dropbox: { type: Boolean }, + github: { type: Boolean }, + gitBridge: { type: Boolean }, + compileTimeout: { type: Number }, + compileGroup: { type: String }, + templates: { type: Boolean }, + trackChanges: { type: Boolean }, + mendeley: { type: Boolean }, + zotero: { type: Boolean }, + referencesSearch: { type: Boolean }, + }, + }, + ], + featuresUpdatedAt: { type: Date }, + // when auto-merged from SL and must-reconfirm is set, we may end up using + // `sharelatexHashedPassword` to recover accounts... + sharelatexHashedPassword: String, + must_reconfirm: { type: Boolean, default: false }, + referal_id: { + type: String, + default() { + return TokenGenerator.generateReferralId() + }, + }, + refered_users: [{ type: ObjectId, ref: 'User' }], + refered_user_count: { type: Number, default: 0 }, + refProviders: { + // The actual values are managed by third-party-references. + mendeley: Schema.Types.Mixed, + zotero: Schema.Types.Mixed, + }, + alphaProgram: { type: Boolean, default: false }, // experimental features + betaProgram: { type: Boolean, default: false }, + overleaf: { + id: { type: Number }, + accessToken: { type: String }, + refreshToken: { type: String }, + }, + awareOfV2: { type: Boolean, default: false }, + samlIdentifiers: { type: Array, default: [] }, + thirdPartyIdentifiers: { type: Array, default: [] }, + migratedAt: { type: Date }, + twoFactorAuthentication: { + createdAt: { type: Date }, + enrolledAt: { type: Date }, + secret: { type: String }, + }, + onboardingEmailSentAt: { type: Date }, + auditLog: [AuditLogEntrySchema], + splitTests: Schema.Types.Mixed, +}) + +exports.User = mongoose.model('User', UserSchema) +exports.UserSchema = UserSchema diff --git a/services/web/app/src/router.js b/services/web/app/src/router.js new file mode 100644 index 0000000000..88c4f423a9 --- /dev/null +++ b/services/web/app/src/router.js @@ -0,0 +1,1179 @@ +const AdminController = require('./Features/ServerAdmin/AdminController') +const ErrorController = require('./Features/Errors/ErrorController') +const ProjectController = require('./Features/Project/ProjectController') +const ProjectApiController = require('./Features/Project/ProjectApiController') +const SpellingController = require('./Features/Spelling/SpellingController') +const EditorRouter = require('./Features/Editor/EditorRouter') +const Settings = require('@overleaf/settings') +const TpdsController = require('./Features/ThirdPartyDataStore/TpdsController') +const SubscriptionRouter = require('./Features/Subscription/SubscriptionRouter') +const UploadsRouter = require('./Features/Uploads/UploadsRouter') +const metrics = require('@overleaf/metrics') +const ReferalController = require('./Features/Referal/ReferalController') +const AuthenticationController = require('./Features/Authentication/AuthenticationController') +const SessionManager = require('./Features/Authentication/SessionManager') +const TagsController = require('./Features/Tags/TagsController') +const NotificationsController = require('./Features/Notifications/NotificationsController') +const CollaboratorsRouter = require('./Features/Collaborators/CollaboratorsRouter') +const UserInfoController = require('./Features/User/UserInfoController') +const UserController = require('./Features/User/UserController') +const UserEmailsController = require('./Features/User/UserEmailsController') +const UserPagesController = require('./Features/User/UserPagesController') +const DocumentController = require('./Features/Documents/DocumentController') +const CompileManager = require('./Features/Compile/CompileManager') +const CompileController = require('./Features/Compile/CompileController') +const ClsiCookieManager = require('./Features/Compile/ClsiCookieManager')( + Settings.apis.clsi != null ? Settings.apis.clsi.backendGroupName : undefined +) +const HealthCheckController = require('./Features/HealthCheck/HealthCheckController') +const ProjectDownloadsController = require('./Features/Downloads/ProjectDownloadsController') +const FileStoreController = require('./Features/FileStore/FileStoreController') +const HistoryController = require('./Features/History/HistoryController') +const ExportsController = require('./Features/Exports/ExportsController') +const PasswordResetRouter = require('./Features/PasswordReset/PasswordResetRouter') +const StaticPagesRouter = require('./Features/StaticPages/StaticPagesRouter') +const ChatController = require('./Features/Chat/ChatController') +const Modules = require('./infrastructure/Modules') +const RateLimiterMiddleware = require('./Features/Security/RateLimiterMiddleware') +const InactiveProjectController = require('./Features/InactiveData/InactiveProjectController') +const ContactRouter = require('./Features/Contacts/ContactRouter') +const ReferencesController = require('./Features/References/ReferencesController') +const AuthorizationMiddleware = require('./Features/Authorization/AuthorizationMiddleware') +const BetaProgramController = require('./Features/BetaProgram/BetaProgramController') +const AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter') +const MetaController = require('./Features/Metadata/MetaController') +const TokenAccessController = require('./Features/TokenAccess/TokenAccessController') +const Features = require('./infrastructure/Features') +const LinkedFilesRouter = require('./Features/LinkedFiles/LinkedFilesRouter') +const TemplatesRouter = require('./Features/Templates/TemplatesRouter') +const InstitutionsController = require('./Features/Institutions/InstitutionsController') +const UserMembershipRouter = require('./Features/UserMembership/UserMembershipRouter') +const SystemMessageController = require('./Features/SystemMessages/SystemMessageController') +const AnalyticsRegistrationSourceMiddleware = require('./Features/Analytics/AnalyticsRegistrationSourceMiddleware') +const { Joi, validate } = require('./infrastructure/Validation') +const { + renderUnsupportedBrowserPage, + unsupportedBrowserMiddleware, +} = require('./infrastructure/UnsupportedBrowserMiddleware') + +const logger = require('logger-sharelatex') +const _ = require('underscore') + +module.exports = { initialize } + +function initialize(webRouter, privateApiRouter, publicApiRouter) { + if (!Settings.allowPublicAccess) { + webRouter.all('*', AuthenticationController.requireGlobalLogin) + } + + webRouter.get('*', AnalyticsRegistrationSourceMiddleware.setInbound()) + + webRouter.get('/login', UserPagesController.loginPage) + AuthenticationController.addEndpointToLoginWhitelist('/login') + + webRouter.post('/login', AuthenticationController.passportLogin) + + if (Settings.enableLegacyLogin) { + AuthenticationController.addEndpointToLoginWhitelist('/login/legacy') + webRouter.get('/login/legacy', UserPagesController.loginPage) + webRouter.post('/login/legacy', AuthenticationController.passportLogin) + } + + webRouter.get( + '/read-only/one-time-login', + UserPagesController.oneTimeLoginPage + ) + AuthenticationController.addEndpointToLoginWhitelist( + '/read-only/one-time-login' + ) + + webRouter.get('/logout', UserPagesController.logoutPage) + webRouter.post('/logout', UserController.logout) + + webRouter.get('/restricted', AuthorizationMiddleware.restricted) + + if (Features.hasFeature('registration-page')) { + webRouter.get('/register', UserPagesController.registerPage) + AuthenticationController.addEndpointToLoginWhitelist('/register') + } + + EditorRouter.apply(webRouter, privateApiRouter) + CollaboratorsRouter.apply(webRouter, privateApiRouter) + SubscriptionRouter.apply(webRouter, privateApiRouter, publicApiRouter) + UploadsRouter.apply(webRouter, privateApiRouter) + PasswordResetRouter.apply(webRouter, privateApiRouter) + StaticPagesRouter.apply(webRouter, privateApiRouter) + ContactRouter.apply(webRouter, privateApiRouter) + AnalyticsRouter.apply(webRouter, privateApiRouter, publicApiRouter) + LinkedFilesRouter.apply(webRouter, privateApiRouter, publicApiRouter) + TemplatesRouter.apply(webRouter) + UserMembershipRouter.apply(webRouter) + + Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter) + + if (Settings.enableSubscriptions) { + webRouter.get( + '/user/bonus', + AuthenticationController.requireLogin(), + ReferalController.bonus + ) + } + + // .getMessages will generate an empty response for anonymous users. + webRouter.get('/system/messages', SystemMessageController.getMessages) + + webRouter.get( + '/user/settings', + AuthenticationController.requireLogin(), + UserPagesController.settingsPage + ) + webRouter.post( + '/user/settings', + AuthenticationController.requireLogin(), + UserController.updateUserSettings + ) + webRouter.post( + '/user/password/update', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'change-password', + maxRequests: 10, + timeInterval: 60, + }), + UserController.changePassword + ) + webRouter.get( + '/user/emails', + AuthenticationController.requireLogin(), + UserController.promises.ensureAffiliationMiddleware, + UserEmailsController.list + ) + webRouter.get('/user/emails/confirm', UserEmailsController.showConfirm) + webRouter.post( + '/user/emails/confirm', + RateLimiterMiddleware.rateLimit({ + endpointName: 'confirm-email', + maxRequests: 10, + timeInterval: 60, + }), + UserEmailsController.confirm + ) + webRouter.post( + '/user/emails/resend_confirmation', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'resend-confirmation', + maxRequests: 10, + timeInterval: 60, + }), + UserEmailsController.resendConfirmation + ) + + if (Features.hasFeature('affiliations')) { + webRouter.post( + '/user/emails', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'add-email', + maxRequests: 10, + timeInterval: 60, + }), + UserEmailsController.add + ) + webRouter.post( + '/user/emails/delete', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'delete-email', + maxRequests: 10, + timeInterval: 60, + }), + UserEmailsController.remove + ) + webRouter.post( + '/user/emails/default', + AuthenticationController.requireLogin(), + UserEmailsController.setDefault + ) + webRouter.post( + '/user/emails/endorse', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'endorse-email', + maxRequests: 30, + timeInterval: 60, + }), + UserEmailsController.endorse + ) + } + + webRouter.get( + '/user/sessions', + AuthenticationController.requireLogin(), + UserPagesController.sessionsPage + ) + webRouter.post( + '/user/sessions/clear', + AuthenticationController.requireLogin(), + UserController.clearSessions + ) + + webRouter.delete( + '/user/newsletter/unsubscribe', + AuthenticationController.requireLogin(), + UserController.unsubscribe + ) + webRouter.post( + '/user/delete', + RateLimiterMiddleware.rateLimit({ + endpointName: 'delete-user', + maxRequests: 10, + timeInterval: 60, + }), + AuthenticationController.requireLogin(), + UserController.tryDeleteUser + ) + + webRouter.get( + '/user/personal_info', + AuthenticationController.requireLogin(), + UserInfoController.getLoggedInUsersPersonalInfo + ) + privateApiRouter.get( + '/user/:user_id/personal_info', + AuthenticationController.requirePrivateApiAuth(), + UserInfoController.getPersonalInfo + ) + + webRouter.get( + '/user/reconfirm', + UserPagesController.renderReconfirmAccountPage + ) + // for /user/reconfirm POST, see password router + + webRouter.get( + '/user/tpds/queues', + AuthenticationController.requireLogin(), + TpdsController.getQueues + ) + + webRouter.get( + '/user/projects', + AuthenticationController.requireLogin(), + ProjectController.userProjectsJson + ) + webRouter.get( + '/project/:Project_id/entities', + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanReadProject, + ProjectController.projectEntitiesJson + ) + + webRouter.get( + '/project', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'open-dashboard', + maxRequests: 30, + timeInterval: 60, + }), + unsupportedBrowserMiddleware, + ProjectController.projectListPage + ) + webRouter.post( + '/project/new', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'create-project', + maxRequests: 20, + timeInterval: 60, + }), + ProjectController.newProject + ) + + webRouter.get( + '/Project/:Project_id', + RateLimiterMiddleware.rateLimit({ + endpointName: 'open-project', + params: ['Project_id'], + maxRequests: 15, + timeInterval: 60, + }), + unsupportedBrowserMiddleware, + AuthenticationController.validateUserSession(), + AuthorizationMiddleware.ensureUserCanReadProject, + ProjectController.loadEditor + ) + webRouter.head( + '/Project/:Project_id/file/:File_id', + AuthorizationMiddleware.ensureUserCanReadProject, + FileStoreController.getFileHead + ) + webRouter.get( + '/Project/:Project_id/file/:File_id', + AuthorizationMiddleware.ensureUserCanReadProject, + FileStoreController.getFile + ) + webRouter.post( + '/project/:Project_id/settings', + AuthorizationMiddleware.ensureUserCanWriteProjectSettings, + ProjectController.updateProjectSettings + ) + webRouter.post( + '/project/:Project_id/settings/admin', + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanAdminProject, + ProjectController.updateProjectAdminSettings + ) + + webRouter.post( + '/project/:Project_id/compile', + RateLimiterMiddleware.rateLimit({ + endpointName: 'compile-project-http', + params: ['Project_id'], + maxRequests: 800, + timeInterval: 60 * 60, + }), + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.compile + ) + + webRouter.post( + '/project/:Project_id/compile/stop', + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.stopCompile + ) + + // LEGACY: Used by the web download buttons, adds filename header, TODO: remove at some future date + webRouter.get( + '/project/:Project_id/output/output.pdf', + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.downloadPdf + ) + + // PDF Download button + webRouter.get( + /^\/download\/project\/([^/]*)\/output\/output\.pdf$/, + function (req, res, next) { + const params = { Project_id: req.params[0] } + req.params = params + next() + }, + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.downloadPdf + ) + + // PDF Download button for specific build + webRouter.get( + /^\/download\/project\/([^/]*)\/build\/([0-9a-f-]+)\/output\/output\.pdf$/, + function (req, res, next) { + const params = { + Project_id: req.params[0], + build_id: req.params[1], + } + req.params = params + next() + }, + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.downloadPdf + ) + + // Align with limits defined in CompileController.downloadPdf + const rateLimiterMiddlewareOutputFiles = RateLimiterMiddleware.rateLimit({ + endpointName: 'misc-output-download', + params: ['Project_id'], + maxRequests: 1000, + timeInterval: 60 * 60, + }) + + // Used by the pdf viewers + webRouter.get( + /^\/project\/([^/]*)\/output\/(.*)$/, + function (req, res, next) { + const params = { + Project_id: req.params[0], + file: req.params[1], + } + req.params = params + next() + }, + rateLimiterMiddlewareOutputFiles, + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.getFileFromClsi + ) + // direct url access to output files for a specific build (query string not required) + webRouter.get( + /^\/project\/([^/]*)\/build\/([0-9a-f-]+)\/output\/(.*)$/, + function (req, res, next) { + const params = { + Project_id: req.params[0], + build_id: req.params[1], + file: req.params[2], + } + req.params = params + next() + }, + rateLimiterMiddlewareOutputFiles, + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.getFileFromClsi + ) + + // direct url access to output files for user but no build, to retrieve files when build fails + webRouter.get( + /^\/project\/([^/]*)\/user\/([0-9a-f-]+)\/output\/(.*)$/, + function (req, res, next) { + const params = { + Project_id: req.params[0], + user_id: req.params[1], + file: req.params[2], + } + req.params = params + next() + }, + rateLimiterMiddlewareOutputFiles, + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.getFileFromClsi + ) + + // direct url access to output files for a specific user and build (query string not required) + webRouter.get( + /^\/project\/([^/]*)\/user\/([0-9a-f]+)\/build\/([0-9a-f-]+)\/output\/(.*)$/, + function (req, res, next) { + const params = { + Project_id: req.params[0], + user_id: req.params[1], + build_id: req.params[2], + file: req.params[3], + } + req.params = params + next() + }, + rateLimiterMiddlewareOutputFiles, + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.getFileFromClsi + ) + + webRouter.delete( + '/project/:Project_id/output', + validate({ query: { clsiserverid: Joi.string() } }), + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.deleteAuxFiles + ) + webRouter.get( + '/project/:Project_id/sync/code', + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.proxySyncCode + ) + webRouter.get( + '/project/:Project_id/sync/pdf', + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.proxySyncPdf + ) + webRouter.get( + '/project/:Project_id/wordcount', + validate({ query: { clsiserverid: Joi.string() } }), + AuthorizationMiddleware.ensureUserCanReadProject, + CompileController.wordCount + ) + + webRouter.post( + '/Project/:Project_id/archive', + AuthorizationMiddleware.ensureUserCanReadProject, + ProjectController.archiveProject + ) + webRouter.delete( + '/Project/:Project_id/archive', + AuthorizationMiddleware.ensureUserCanReadProject, + ProjectController.unarchiveProject + ) + webRouter.post( + '/project/:project_id/trash', + AuthorizationMiddleware.ensureUserCanReadProject, + ProjectController.trashProject + ) + webRouter.delete( + '/project/:project_id/trash', + AuthorizationMiddleware.ensureUserCanReadProject, + ProjectController.untrashProject + ) + + webRouter.delete( + '/Project/:Project_id', + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanAdminProject, + ProjectController.deleteProject + ) + + webRouter.post( + '/Project/:Project_id/restore', + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanAdminProject, + ProjectController.restoreProject + ) + webRouter.post( + '/Project/:Project_id/clone', + AuthorizationMiddleware.ensureUserCanReadProject, + ProjectController.cloneProject + ) + + webRouter.post( + '/project/:Project_id/rename', + AuthenticationController.requireLogin(), + AuthorizationMiddleware.ensureUserCanAdminProject, + ProjectController.renameProject + ) + webRouter.get( + '/project/:Project_id/updates', + AuthorizationMiddleware.ensureUserCanReadProject, + HistoryController.selectHistoryApi, + HistoryController.proxyToHistoryApiAndInjectUserDetails + ) + webRouter.get( + '/project/:Project_id/doc/:doc_id/diff', + AuthorizationMiddleware.ensureUserCanReadProject, + HistoryController.selectHistoryApi, + HistoryController.proxyToHistoryApi + ) + webRouter.get( + '/project/:Project_id/diff', + AuthorizationMiddleware.ensureUserCanReadProject, + HistoryController.selectHistoryApi, + HistoryController.proxyToHistoryApiAndInjectUserDetails + ) + webRouter.get( + '/project/:Project_id/filetree/diff', + AuthorizationMiddleware.ensureUserCanReadProject, + HistoryController.selectHistoryApi, + HistoryController.proxyToHistoryApi + ) + webRouter.post( + '/project/:Project_id/doc/:doc_id/version/:version_id/restore', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + HistoryController.selectHistoryApi, + HistoryController.proxyToHistoryApi + ) + webRouter.post( + '/project/:project_id/doc/:doc_id/restore', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + HistoryController.restoreDocFromDeletedDoc + ) + webRouter.post( + '/project/:project_id/restore_file', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + HistoryController.restoreFileFromV2 + ) + webRouter.get( + '/project/:project_id/version/:version/zip', + RateLimiterMiddleware.rateLimit({ + endpointName: 'download-project-revision', + maxRequests: 30, + timeInterval: 60 * 60, + }), + AuthorizationMiddleware.ensureUserCanReadProject, + HistoryController.downloadZipOfVersion + ) + privateApiRouter.post( + '/project/:Project_id/history/resync', + AuthenticationController.requirePrivateApiAuth(), + HistoryController.resyncProjectHistory + ) + + webRouter.get( + '/project/:Project_id/labels', + AuthorizationMiddleware.ensureUserCanReadProject, + HistoryController.selectHistoryApi, + HistoryController.ensureProjectHistoryEnabled, + HistoryController.getLabels + ) + webRouter.post( + '/project/:Project_id/labels', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + HistoryController.selectHistoryApi, + HistoryController.ensureProjectHistoryEnabled, + HistoryController.createLabel + ) + webRouter.delete( + '/project/:Project_id/labels/:label_id', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + HistoryController.selectHistoryApi, + HistoryController.ensureProjectHistoryEnabled, + HistoryController.deleteLabel + ) + + webRouter.post( + '/project/:project_id/export/:brand_variation_id', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + ExportsController.exportProject + ) + webRouter.get( + '/project/:project_id/export/:export_id', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + ExportsController.exportStatus + ) + webRouter.get( + '/project/:project_id/export/:export_id/:type', + AuthorizationMiddleware.ensureUserCanWriteProjectContent, + ExportsController.exportDownload + ) + + webRouter.get( + '/Project/:Project_id/download/zip', + AuthorizationMiddleware.ensureUserCanReadProject, + ProjectDownloadsController.downloadProject + ) + webRouter.get( + '/project/download/zip', + AuthorizationMiddleware.ensureUserCanReadMultipleProjects, + ProjectDownloadsController.downloadMultipleProjects + ) + + webRouter.get( + '/project/:project_id/metadata', + AuthorizationMiddleware.ensureUserCanReadProject, + Settings.allowAnonymousReadAndWriteSharing + ? (req, res, next) => { + next() + } + : AuthenticationController.requireLogin(), + MetaController.getMetadata + ) + webRouter.post( + '/project/:project_id/doc/:doc_id/metadata', + AuthorizationMiddleware.ensureUserCanReadProject, + Settings.allowAnonymousReadAndWriteSharing + ? (req, res, next) => { + next() + } + : AuthenticationController.requireLogin(), + MetaController.broadcastMetadataForDoc + ) + privateApiRouter.post( + '/internal/expire-deleted-projects-after-duration', + AuthenticationController.requirePrivateApiAuth(), + ProjectController.expireDeletedProjectsAfterDuration + ) + privateApiRouter.post( + '/internal/expire-deleted-users-after-duration', + AuthenticationController.requirePrivateApiAuth(), + UserController.expireDeletedUsersAfterDuration + ) + privateApiRouter.post( + '/internal/project/:projectId/expire-deleted-project', + AuthenticationController.requirePrivateApiAuth(), + ProjectController.expireDeletedProject + ) + privateApiRouter.post( + '/internal/users/:userId/expire', + AuthenticationController.requirePrivateApiAuth(), + UserController.expireDeletedUser + ) + + privateApiRouter.get( + '/user/:userId/tag', + AuthenticationController.requirePrivateApiAuth(), + TagsController.apiGetAllTags + ) + webRouter.get( + '/tag', + AuthenticationController.requireLogin(), + TagsController.getAllTags + ) + webRouter.post( + '/tag', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'create-tag', + maxRequests: 30, + timeInterval: 60, + }), + TagsController.createTag + ) + webRouter.post( + '/tag/:tagId/rename', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'rename-tag', + maxRequests: 30, + timeInterval: 60, + }), + TagsController.renameTag + ) + webRouter.delete( + '/tag/:tagId', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'delete-tag', + maxRequests: 30, + timeInterval: 60, + }), + TagsController.deleteTag + ) + webRouter.post( + '/tag/:tagId/project/:projectId', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'add-project-to-tag', + maxRequests: 30, + timeInterval: 60, + }), + TagsController.addProjectToTag + ) + webRouter.delete( + '/tag/:tagId/project/:projectId', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit({ + endpointName: 'remove-project-from-tag', + maxRequests: 30, + timeInterval: 60, + }), + TagsController.removeProjectFromTag + ) + + webRouter.get( + '/notifications', + AuthenticationController.requireLogin(), + NotificationsController.getAllUnreadNotifications + ) + webRouter.delete( + '/notifications/:notificationId', + AuthenticationController.requireLogin(), + NotificationsController.markNotificationAsRead + ) + + // Deprecated in favour of /internal/project/:project_id but still used by versioning + privateApiRouter.get( + '/project/:project_id/details', + AuthenticationController.requirePrivateApiAuth(), + ProjectApiController.getProjectDetails + ) + + // New 'stable' /internal API end points + privateApiRouter.get( + '/internal/project/:project_id', + AuthenticationController.requirePrivateApiAuth(), + ProjectApiController.getProjectDetails + ) + privateApiRouter.get( + '/internal/project/:Project_id/zip', + AuthenticationController.requirePrivateApiAuth(), + ProjectDownloadsController.downloadProject + ) + privateApiRouter.get( + '/internal/project/:project_id/compile/pdf', + AuthenticationController.requirePrivateApiAuth(), + CompileController.compileAndDownloadPdf + ) + + privateApiRouter.post( + '/internal/deactivateOldProjects', + AuthenticationController.requirePrivateApiAuth(), + InactiveProjectController.deactivateOldProjects + ) + privateApiRouter.post( + '/internal/project/:project_id/deactivate', + AuthenticationController.requirePrivateApiAuth(), + InactiveProjectController.deactivateProject + ) + + privateApiRouter.get( + /^\/internal\/project\/([^/]*)\/output\/(.*)$/, + function (req, res, next) { + const params = { + Project_id: req.params[0], + file: req.params[1], + } + req.params = params + next() + }, + AuthenticationController.requirePrivateApiAuth(), + CompileController.getFileFromClsi + ) + + privateApiRouter.get( + '/project/:Project_id/doc/:doc_id', + AuthenticationController.requirePrivateApiAuth(), + DocumentController.getDocument + ) + privateApiRouter.post( + '/project/:Project_id/doc/:doc_id', + AuthenticationController.requirePrivateApiAuth(), + DocumentController.setDocument + ) + + privateApiRouter.post( + '/user/:user_id/update/*', + AuthenticationController.requirePrivateApiAuth(), + TpdsController.mergeUpdate + ) + privateApiRouter.delete( + '/user/:user_id/update/*', + AuthenticationController.requirePrivateApiAuth(), + TpdsController.deleteUpdate + ) + + privateApiRouter.post( + '/project/:project_id/contents/*', + AuthenticationController.requirePrivateApiAuth(), + TpdsController.updateProjectContents + ) + privateApiRouter.delete( + '/project/:project_id/contents/*', + AuthenticationController.requirePrivateApiAuth(), + TpdsController.deleteProjectContents + ) + + webRouter.post( + '/spelling/check', + AuthenticationController.requireLogin(), + SpellingController.proxyRequestToSpellingApi + ) + webRouter.post( + '/spelling/learn', + AuthenticationController.requireLogin(), + SpellingController.proxyRequestToSpellingApi + ) + + webRouter.get( + '/project/:project_id/messages', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + ChatController.getMessages + ) + webRouter.post( + '/project/:project_id/messages', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + RateLimiterMiddleware.rateLimit({ + endpointName: 'send-chat-message', + maxRequests: 100, + timeInterval: 60, + }), + ChatController.sendMessage + ) + + webRouter.post( + '/project/:Project_id/references/index', + AuthorizationMiddleware.ensureUserCanReadProject, + RateLimiterMiddleware.rateLimit({ + endpointName: 'index-project-references', + maxRequests: 30, + timeInterval: 60, + }), + ReferencesController.index + ) + webRouter.post( + '/project/:Project_id/references/indexAll', + AuthorizationMiddleware.ensureUserCanReadProject, + RateLimiterMiddleware.rateLimit({ + endpointName: 'index-all-project-references', + maxRequests: 30, + timeInterval: 60, + }), + ReferencesController.indexAll + ) + + // disable beta program while v2 is in beta + webRouter.get( + '/beta/participate', + AuthenticationController.requireLogin(), + BetaProgramController.optInPage + ) + webRouter.post( + '/beta/opt-in', + AuthenticationController.requireLogin(), + BetaProgramController.optIn + ) + webRouter.post( + '/beta/opt-out', + AuthenticationController.requireLogin(), + BetaProgramController.optOut + ) + + // New "api" endpoints. Started as a way for v1 to call over to v2 (for + // long-term features, as opposed to the nominally temporary ones in the + // overleaf-integration module), but may expand beyond that role. + publicApiRouter.post( + '/api/clsi/compile/:submission_id', + AuthenticationController.requirePrivateApiAuth(), + CompileController.compileSubmission + ) + publicApiRouter.get( + /^\/api\/clsi\/compile\/([^/]*)\/build\/([0-9a-f-]+)\/output\/(.*)$/, + function (req, res, next) { + const params = { + submission_id: req.params[0], + build_id: req.params[1], + file: req.params[2], + } + req.params = params + next() + }, + AuthenticationController.requirePrivateApiAuth(), + CompileController.getFileFromClsiWithoutUser + ) + publicApiRouter.post( + '/api/institutions/confirm_university_domain', + RateLimiterMiddleware.rateLimit({ + endpointName: 'confirm-university-domain', + maxRequests: 1, + timeInterval: 60, + }), + AuthenticationController.requirePrivateApiAuth(), + InstitutionsController.confirmDomain + ) + + webRouter.get('/chrome', function (req, res, next) { + // Match v1 behaviour - this is used for a Chrome web app + if (SessionManager.isUserLoggedIn(req.session)) { + res.redirect('/project') + } else { + res.redirect('/register') + } + }) + + // Admin Stuff + webRouter.get( + '/admin', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.index + ) + webRouter.get( + '/admin/user', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + (req, res) => res.redirect('/admin/register') + ) // this gets removed by admin-panel addon + webRouter.get( + '/admin/register', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.registerNewUser + ) + webRouter.post( + '/admin/register', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + UserController.register + ) + if (!Features.hasFeature('saas')) { + webRouter.post( + '/admin/openEditor', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.openEditor + ) + webRouter.post( + '/admin/closeEditor', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.closeEditor + ) + webRouter.post( + '/admin/disconnectAllUsers', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.disconnectAllUsers + ) + } + webRouter.post( + '/admin/flushProjectToTpds', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.flushProjectToTpds + ) + webRouter.post( + '/admin/pollDropboxForUser', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.pollDropboxForUser + ) + webRouter.post( + '/admin/messages', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.createMessage + ) + webRouter.post( + '/admin/messages/clear', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.clearMessages + ) + webRouter.post( + '/admin/unregisterServiceWorker', + AuthorizationMiddleware.ensureUserIsSiteAdmin, + AdminController.unregisterServiceWorker + ) + + privateApiRouter.post( + '/disconnectAllUsers', + AdminController.disconnectAllUsers + ) + + privateApiRouter.get('/perfTest', (req, res) => res.send('hello')) + + publicApiRouter.get('/status', (req, res) => { + if (!Settings.siteIsOpen) { + res.send('web site is closed (web)') + } else if (!Settings.editorIsOpen) { + res.send('web editor is closed (web)') + } else { + res.send('web sharelatex is alive (web)') + } + }) + privateApiRouter.get('/status', (req, res) => + res.send('web sharelatex is alive (api)') + ) + + // used by kubernetes health-check and acceptance tests + webRouter.get('/dev/csrf', (req, res) => res.send(res.locals.csrfToken)) + + publicApiRouter.get( + '/health_check', + HealthCheckController.checkActiveHandles, + HealthCheckController.check + ) + privateApiRouter.get( + '/health_check', + HealthCheckController.checkActiveHandles, + HealthCheckController.checkApi + ) + publicApiRouter.get( + '/health_check/api', + HealthCheckController.checkActiveHandles, + HealthCheckController.checkApi + ) + privateApiRouter.get( + '/health_check/api', + HealthCheckController.checkActiveHandles, + HealthCheckController.checkApi + ) + publicApiRouter.get( + '/health_check/full', + HealthCheckController.checkActiveHandles, + HealthCheckController.check + ) + privateApiRouter.get( + '/health_check/full', + HealthCheckController.checkActiveHandles, + HealthCheckController.check + ) + + publicApiRouter.get('/health_check/redis', HealthCheckController.checkRedis) + privateApiRouter.get('/health_check/redis', HealthCheckController.checkRedis) + + publicApiRouter.get('/health_check/mongo', HealthCheckController.checkMongo) + privateApiRouter.get('/health_check/mongo', HealthCheckController.checkMongo) + + webRouter.get( + '/status/compiler/:Project_id', + RateLimiterMiddleware.rateLimit({ + endpointName: 'status-compiler', + maxRequests: 10, + timeInterval: 60, + }), + AuthorizationMiddleware.ensureUserCanReadProject, + function (req, res) { + const projectId = req.params.Project_id + const sendRes = _.once(function (statusCode, message) { + res.status(statusCode) + res.send(message) + ClsiCookieManager.clearServerId(projectId) + }) // force every compile to a new server + // set a timeout + var handler = setTimeout(function () { + sendRes(500, 'Compiler timed out') + handler = null + }, 10000) + // use a valid user id for testing + const testUserId = '123456789012345678901234' + // run the compile + CompileManager.compile( + projectId, + testUserId, + {}, + function (error, status) { + if (handler) { + clearTimeout(handler) + } + if (error) { + sendRes(500, `Compiler returned error ${error.message}`) + } else if (status === 'success') { + sendRes(200, 'Compiler returned in less than 10 seconds') + } else { + sendRes(500, `Compiler returned failure ${status}`) + } + } + ) + } + ) + + webRouter.get('/no-cache', function (req, res, next) { + res.header('Cache-Control', 'max-age=0') + res.sendStatus(404) + }) + + webRouter.get('/oops-express', (req, res, next) => + next(new Error('Test error')) + ) + webRouter.get('/oops-internal', function (req, res, next) { + throw new Error('Test error') + }) + webRouter.get('/oops-mongo', (req, res, next) => + require('./models/Project').Project.findOne({}, function () { + throw new Error('Test error') + }) + ) + + privateApiRouter.get('/opps-small', function (req, res, next) { + logger.err('test error occured') + res.sendStatus(200) + }) + + webRouter.post('/error/client', function (req, res, next) { + logger.warn( + { err: req.body.error, meta: req.body.meta }, + 'client side error' + ) + metrics.inc('client-side-error') + res.sendStatus(204) + }) + + webRouter.get( + `/read/:token(${TokenAccessController.READ_ONLY_TOKEN_PATTERN})`, + RateLimiterMiddleware.rateLimit({ + endpointName: 'read-only-token', + maxRequests: 15, + timeInterval: 60, + }), + AnalyticsRegistrationSourceMiddleware.setSource('link-sharing'), + TokenAccessController.tokenAccessPage, + AnalyticsRegistrationSourceMiddleware.clearSource() + ) + + webRouter.get( + `/:token(${TokenAccessController.READ_AND_WRITE_TOKEN_PATTERN})`, + RateLimiterMiddleware.rateLimit({ + endpointName: 'read-and-write-token', + maxRequests: 15, + timeInterval: 60, + }), + AnalyticsRegistrationSourceMiddleware.setSource('link-sharing'), + TokenAccessController.tokenAccessPage, + AnalyticsRegistrationSourceMiddleware.clearSource() + ) + + webRouter.post( + `/:token(${TokenAccessController.READ_AND_WRITE_TOKEN_PATTERN})/grant`, + RateLimiterMiddleware.rateLimit({ + endpointName: 'grant-token-access-read-write', + maxRequests: 10, + timeInterval: 60, + }), + TokenAccessController.grantTokenAccessReadAndWrite + ) + + webRouter.post( + `/read/:token(${TokenAccessController.READ_ONLY_TOKEN_PATTERN})/grant`, + RateLimiterMiddleware.rateLimit({ + endpointName: 'grant-token-access-read-only', + maxRequests: 10, + timeInterval: 60, + }), + TokenAccessController.grantTokenAccessReadOnly + ) + + webRouter.get('/unsupported-browser', renderUnsupportedBrowserPage) + + webRouter.get('*', ErrorController.notFound) +} diff --git a/services/web/app/src/util/promises.js b/services/web/app/src/util/promises.js new file mode 100644 index 0000000000..c1a68784ea --- /dev/null +++ b/services/web/app/src/util/promises.js @@ -0,0 +1,139 @@ +const { promisify, callbackify } = require('util') +const pLimit = require('p-limit') + +module.exports = { + promisify, + promisifyAll, + promisifyMultiResult, + callbackify, + callbackifyMultiResult, + expressify, + promiseMapWithLimit, +} + +/** + * Promisify all functions in a module. + * + * This is meant to be used only when all functions in the module are async + * callback-style functions. + * + * It's very much tailored to our current module structure. In particular, it + * binds `this` to the module when calling the function in order not to break + * modules that call sibling functions using `this`. + * + * This will not magically fix all modules. Special cases should be promisified + * manually. + * + * The second argument is a bag of options: + * + * - without: an array of function names that shouldn't be promisified + * + * - multiResult: an object whose keys are function names and values are lists + * of parameter names. This is meant for functions that invoke their callbacks + * with more than one result in separate parameters. The promisifed function + * will return these results as a single object, with each result keyed under + * the corresponding parameter name. + */ +function promisifyAll(module, opts = {}) { + const { without = [], multiResult = {} } = opts + const promises = {} + for (const propName of Object.getOwnPropertyNames(module)) { + if (without.includes(propName)) { + continue + } + const propValue = module[propName] + if (typeof propValue !== 'function') { + continue + } + if (multiResult[propName] != null) { + promises[propName] = promisifyMultiResult( + propValue, + multiResult[propName] + ).bind(module) + } else { + promises[propName] = promisify(propValue).bind(module) + } + } + return promises +} + +/** + * Promisify a function that returns multiple results via additional callback + * parameters. + * + * The promisified function returns the results in a single object whose keys + * are the names given in the array `resultNames`. + * + * Example: + * + * function f(callback) { + * return callback(null, 1, 2, 3) + * } + * + * const g = promisifyMultiResult(f, ['a', 'b', 'c']) + * + * const result = await g() // returns {a: 1, b: 2, c: 3} + */ +function promisifyMultiResult(fn, resultNames) { + function promisified(...args) { + return new Promise((resolve, reject) => { + try { + fn(...args, (err, ...results) => { + if (err != null) { + return reject(err) + } + const promiseResult = {} + for (let i = 0; i < resultNames.length; i++) { + promiseResult[resultNames[i]] = results[i] + } + resolve(promiseResult) + }) + } catch (err) { + reject(err) + } + }) + } + return promisified +} + +/** + * Reverse the effect of `promisifyMultiResult`. + * + * This is meant for providing a temporary backward compatible callback + * interface while we migrate to promises. + */ +function callbackifyMultiResult(fn, resultNames) { + function callbackified(...args) { + const [callback] = args.splice(-1) + fn(...args) + .then(result => { + const cbResults = resultNames.map(resultName => result[resultName]) + callback(null, ...cbResults) + }) + .catch(err => { + callback(err) + }) + } + return callbackified +} + +/** + * Transform an async function into an Express middleware + * + * Any error will be passed to the error middlewares via `next()` + */ +function expressify(fn) { + return (req, res, next) => { + fn(req, res, next).catch(next) + } +} + +/** + * Map values in `array` with the async function `fn` + * + * Limit the number of unresolved promises to `concurrency`. + */ +function promiseMapWithLimit(concurrency, array, fn) { + const limit = pLimit(concurrency) + return Promise.all(array.map(x => limit(() => fn(x)))) +} diff --git a/services/web/app/templates/plans/groups.json b/services/web/app/templates/plans/groups.json new file mode 100644 index 0000000000..f7556afd0e --- /dev/null +++ b/services/web/app/templates/plans/groups.json @@ -0,0 +1,122 @@ +{ + "enterprise": { + "collaborator": { + "USD": { + "2": 252, + "3": 376, + "4": 495, + "5": 615, + "10": 1170, + "20": 2160, + "50": 4950 + }, + "EUR": { + "2": 235, + "3": 352, + "4": 468, + "5": 584, + "10": 1090, + "20": 2015, + "50": 4620 + }, + "GBP": { + "2": 198, + "3": 296, + "4": 394, + "5": 492, + "10": 935, + "20": 1730, + "50": 3960 + } + }, + "professional": { + "USD": { + "2": 504, + "3": 752, + "4": 990, + "5": 1230, + "10": 2340, + "20": 4320, + "50": 9900 + }, + "EUR": { + "2": 470, + "3": 704, + "4": 936, + "5": 1168, + "10": 2185, + "20": 4030, + "50": 9240 + }, + "GBP": { + "2": 396, + "3": 592, + "4": 788, + "5": 984, + "10": 1870, + "20": 3455, + "50": 7920 + } + } + }, + "educational": { + "collaborator": { + "USD": { + "2": 252, + "3": 376, + "4": 495, + "5": 615, + "10": 695, + "20": 1295, + "50": 2970 + }, + "EUR": { + "2": 235, + "3": 352, + "4": 468, + "5": 584, + "10": 655, + "20": 1210, + "50": 2770 + }, + "GBP": { + "2": 198, + "3": 296, + "4": 394, + "5": 492, + "10": 560, + "20": 1035, + "50": 2375 + } + }, + "professional": { + "USD": { + "2": 504, + "3": 752, + "4": 990, + "5": 1230, + "10": 1390, + "20": 2590, + "50": 5940 + }, + "EUR": { + "2": 470, + "3": 704, + "4": 936, + "5": 1168, + "10": 1310, + "20": 2420, + "50": 5545 + }, + "GBP": { + "2": 396, + "3": 592, + "4": 788, + "5": 984, + "10": 1125, + "20": 2075, + "50": 4750 + } + } + } +} \ No newline at end of file diff --git a/services/web/app/templates/project_files/main.tex b/services/web/app/templates/project_files/main.tex new file mode 100644 index 0000000000..a0e2bff21d --- /dev/null +++ b/services/web/app/templates/project_files/main.tex @@ -0,0 +1,31 @@ +\documentclass{article} +\usepackage[utf8]{inputenc} + +\title{<%= project_name %>} +\author{<%= user.first_name %> <%= user.last_name %>} +\date{<%= month %> <%= year %>} + +\usepackage{natbib} +\usepackage{graphicx} + +\begin{document} + +\maketitle + +\section{Introduction} +There is a theory which states that if ever anyone discovers exactly what the Universe is for and why it is here, it will instantly disappear and be replaced by something even more bizarre and inexplicable. +There is another theory which states that this has already happened. + +\begin{figure}[h!] +\centering +\includegraphics[scale=1.7]{universe} +\caption{The Universe} +\label{fig:universe} +\end{figure} + +\section{Conclusion} +``I always thought something was fundamentally wrong with the universe'' \citep{adams1995hitchhiker} + +\bibliographystyle{plain} +\bibliography{references} +\end{document} diff --git a/services/web/app/templates/project_files/mainbasic.tex b/services/web/app/templates/project_files/mainbasic.tex new file mode 100644 index 0000000000..12198ef625 --- /dev/null +++ b/services/web/app/templates/project_files/mainbasic.tex @@ -0,0 +1,14 @@ +\documentclass{article} +\usepackage[utf8]{inputenc} + +\title{<%= project_name %>} +\author{<%= user.first_name %> <%= user.last_name %>} +\date{<%= month %> <%= year %>} + +\begin{document} + +\maketitle + +\section{Introduction} + +\end{document} diff --git a/services/web/app/templates/project_files/references.bib b/services/web/app/templates/project_files/references.bib new file mode 100644 index 0000000000..1758b10f6f --- /dev/null +++ b/services/web/app/templates/project_files/references.bib @@ -0,0 +1,8 @@ +@book{adams1995hitchhiker, + title={The Hitchhiker's Guide to the Galaxy}, + author={Adams, D.}, + isbn={9781417642595}, + url={http://books.google.com/books?id=W-xMPgAACAAJ}, + year={1995}, + publisher={San Val} +} diff --git a/services/web/app/templates/project_files/test-example-project/frog.jpg b/services/web/app/templates/project_files/test-example-project/frog.jpg new file mode 100644 index 0000000000..5b889ef3cf Binary files /dev/null and b/services/web/app/templates/project_files/test-example-project/frog.jpg differ diff --git a/services/web/app/templates/project_files/test-example-project/main.tex b/services/web/app/templates/project_files/test-example-project/main.tex new file mode 100644 index 0000000000..617005bfbf --- /dev/null +++ b/services/web/app/templates/project_files/test-example-project/main.tex @@ -0,0 +1,119 @@ +\documentclass{article} + +% Language setting +% Replace `english' with e.g. `spanish' to change the document language +\usepackage[english]{babel} + +% Set page size and margins +% Replace `letterpaper' with`a4paper' for UK/EU standard size +\usepackage[letterpaper,top=2cm,bottom=2cm,left=3cm,right=3cm,marginparwidth=1.75cm]{geometry} + +% Useful packages +\usepackage{amsmath} +\usepackage{graphicx} +\usepackage[colorlinks=true, allcolors=blue]{hyperref} + +\title{Your Paper} +\author{You} + +\begin{document} +\maketitle + +\begin{abstract} +Your abstract. +\end{abstract} + +\section{Introduction} + +Your introduction goes here! Simply start writing your document and use the Recompile button to view the updated PDF preview. Examples of commonly used commands and features are listed below, to help you get started. + +Once you're familiar with the editor, you can find various project setting in the Overleaf menu, accessed via the button in the very top left of the editor. To view tutorials, user guides, and further documentation, please visit our \href{https://www.overleaf.com/learn}{help library}, or head to our plans page to \href{https://www.overleaf.com/user/subscription/plans}{choose your plan}. + +\section{Some examples to get started} + +\subsection{How to create Sections and Subsections} + +Simply use the section and subsection commands, as in this example document! With Overleaf, all the formatting and numbering is handled automatically according to the template you've chosen. If you're using Rich Text mode, you can also create new section and subsections via the buttons in the editor toolbar. + +\subsection{How to include Figures} + +First you have to upload the image file from your computer using the upload link in the file-tree menu. Then use the includegraphics command to include it in your document. Use the figure environment and the caption command to add a number and a caption to your figure. See the code for Figure \ref{fig:frog} in this section for an example. + +Note that your figure will automatically be placed in the most appropriate place for it, given the surrounding text and taking into account other figures or tables that may be close by. You can find out more about adding images to your documents in this help article on \href{https://www.overleaf.com/learn/how-to/Including_images_on_Overleaf}{including images on Overleaf}. + +\begin{figure} +\centering +\includegraphics[width=0.3\textwidth]{frog.jpg} +\caption{\label{fig:frog}This frog was uploaded via the file-tree menu.} +\end{figure} + +\subsection{How to add Tables} + +Use the table and tabular environments for basic tables --- see Table~\ref{tab:widgets}, for example. For more information, please see this help article on \href{https://www.overleaf.com/learn/latex/tables}{tables}. + +\begin{table} +\centering +\begin{tabular}{l|r} +Item & Quantity \\\hline +Widgets & 42 \\ +Gadgets & 13 +\end{tabular} +\caption{\label{tab:widgets}An example table.} +\end{table} + +\subsection{How to add Comments and Track Changes} + +Comments can be added to your project by highlighting some text and clicking ``Add comment'' in the top right of the editor pane. To view existing comments, click on the Review menu in the toolbar above. To reply to a comment, click on the Reply button in the lower right corner of the comment. You can close the Review pane by clicking its name on the toolbar when you're done reviewing for the time being. + +Track changes are available on all our \href{https://www.overleaf.com/user/subscription/plans}{premium plans}, and can be toggled on or off using the option at the top of the Review pane. Track changes allow you to keep track of every change made to the document, along with the person making the change. + +\subsection{How to add Lists} + +You can make lists with automatic numbering \dots + +\begin{enumerate} +\item Like this, +\item and like this. +\end{enumerate} +\dots or bullet points \dots +\begin{itemize} +\item Like this, +\item and like this. +\end{itemize} + +\subsection{How to write Mathematics} + +\LaTeX{} is great at typesetting mathematics. Let $X_1, X_2, \ldots, X_n$ be a sequence of independent and identically distributed random variables with $\text{E}[X_i] = \mu$ and $\text{Var}[X_i] = \sigma^2 < \infty$, and let +\[S_n = \frac{X_1 + X_2 + \cdots + X_n}{n} + = \frac{1}{n}\sum_{i}^{n} X_i\] +denote their mean. Then as $n$ approaches infinity, the random variables $\sqrt{n}(S_n - \mu)$ converge in distribution to a normal $\mathcal{N}(0, \sigma^2)$. + + +\subsection{How to change the margins and paper size} + +Usually the template you're using will have the page margins and paper size set correctly for that use-case. For example, if you're using a journal article template provided by the journal publisher, that template will be formatted according to their requirements. In these cases, it's best not to alter the margins directly. + +If however you're using a more general template, such as this one, and would like to alter the margins, a common way to do so is via the geometry package. You can find the geometry package loaded in the preamble at the top of this example file, and if you'd like to learn more about how to adjust the settings, please visit this help article on \href{https://www.overleaf.com/learn/latex/page_size_and_margins}{page size and margins}. + +\subsection{How to change the document language and spell check settings} + +Overleaf supports many different languages, including multiple different languages within one document. + +To configure the document language, simply edit the option provided to the babel package in the preamble at the top of this example project. To learn more about the different options, please visit this help article on \href{https://www.overleaf.com/learn/latex/International_language_support}{international language support}. + +To change the spell check language, simply open the Overleaf menu at the top left of the editor window, scroll down to the spell check setting, and adjust accordingly. + +\subsection{How to add Citations and a References List} + +You can simply upload a \verb|.bib| file containing your BibTeX entries, created with a tool such as JabRef. You can then cite entries from it, like this: \cite{greenwade93}. Just remember to specify a bibliography style, as well as the filename of the \verb|.bib|. You can find a \href{https://www.overleaf.com/help/97-how-to-include-a-bibliography-using-bibtex}{video tutorial here} to learn more about BibTeX. + +If you have an \href{https://www.overleaf.com/user/subscription/plans}{upgraded account}, you can also import your Mendeley or Zotero library directly as a \verb|.bib| file, via the upload menu in the file-tree. + +\subsection{Good luck!} + +We hope you find Overleaf useful, and do take a look at our \href{https://www.overleaf.com/learn}{help library} for more tutorials and user guides! Please also let us know if you have any feedback using the Contact Us link at the bottom of the Overleaf menu --- or use the contact form at \url{https://www.overleaf.com/contact}. + +\bibliographystyle{alpha} +\bibliography{sample} + +\end{document} \ No newline at end of file diff --git a/services/web/app/templates/project_files/test-example-project/sample.bib b/services/web/app/templates/project_files/test-example-project/sample.bib new file mode 100644 index 0000000000..a0e21c740c --- /dev/null +++ b/services/web/app/templates/project_files/test-example-project/sample.bib @@ -0,0 +1,9 @@ +@article{greenwade93, + author = "George D. Greenwade", + title = "The {C}omprehensive {T}ex {A}rchive {N}etwork ({CTAN})", + year = "1993", + journal = "TUGBoat", + volume = "14", + number = "3", + pages = "342--351" +} diff --git a/services/web/app/templates/project_files/universe.jpg b/services/web/app/templates/project_files/universe.jpg new file mode 100644 index 0000000000..ed19e7d677 Binary files /dev/null and b/services/web/app/templates/project_files/universe.jpg differ diff --git a/services/web/app/views/_metadata.pug b/services/web/app/views/_metadata.pug new file mode 100644 index 0000000000..14f84c85d1 --- /dev/null +++ b/services/web/app/views/_metadata.pug @@ -0,0 +1,115 @@ + +//- Title +if (metadata && metadata.title) + title= metadata.title + ' - ' + settings.appName + ', ' + translate("online_latex_editor") + meta(name="twitter:title", content=metadata.title) + meta(name="og:title", content=metadata.title) +else if (typeof(title) == "undefined") + title= settings.appName + ', '+ translate("online_latex_editor") + meta(name="twitter:title", content=settings.appName + ', '+ translate("online_latex_editor")) + meta(name="og:title", content=settings.appName + ', '+ translate("online_latex_editor")) +else + title= translate(title) + ' - ' + settings.appName + ', ' + translate("online_latex_editor") + //- to do - not translate? + meta(name="twitter:title", content=translate(title)) + meta(name="og:title", content=translate(title)) + +//- Description +if (metadata && metadata.description) + meta(name="description" , content=metadata.description) + meta(itemprop="description" , content=metadata.description) + //-twitter and og descriptions handeled in their sections below +else + meta(name="description", content=translate("site_description")) + meta(itemprop="description", content=translate("site_description")) + +//- Image +if (metadata && metadata.image && metadata.image.fields) + //- from the CMS + meta(itemprop="image", content=metadata.image.fields.file.url) + meta(name="image", content=metadata.image.fields.file.url) +else if (metadata && metadata.image_src) + //- pages with custom metadata images, metadata.image_src is the full image URL + meta(itemprop="image", content=metadata.image_src) + meta(name="image", content=metadata.image_src) +else if (settings.overleaf) + //- the default image for Overleaf + meta(itemprop="image", content=buildImgPath('ol-brand/overleaf_og_logo.png')) + meta(name="image", content=buildImgPath('ol-brand/overleaf_og_logo.png')) +else + //- the default image for ShareLaTeX + meta(itemprop="image", content='/touch-icon-192x192.png') + meta(name="image", content='/touch-icon-192x192.png') + +//- Keywords +if (metadata && metadata.keywords) + meta(name="keywords" content=metadata.keywords) + +//- Misc +meta(itemprop="name", content=settings.appName + ", the Online LaTeX Editor") + +if (metadata && metadata.robotsNoindexNofollow) + meta(name="robots" content="noindex, nofollow") + +//- Twitter +meta(name="twitter:card", content=metadata && metadata.twitterCardType ? metadata.twitterCardType : 'summary') +if (settings.social && settings.social.twitter && settings.social.twitter.handle) + meta(name="twitter:site", content="@" + settings.social.twitter.handle) +if (metadata && metadata.twitterDescription) + meta(name="twitter:description", content=metadata.twitterDescription) +else + meta(name="twitter:description", content=translate("site_description")) +if (metadata && metadata.twitterImage && metadata.twitterImage.fields) + //- from the CMS + meta(name="twitter:image", content=metadata.twitterImage.fields.file.url) + meta(name="twitter:image:alt", content=metadata.twitterImage.fields.title) +else if (settings.overleaf) + //- the default image for Overleaf + meta(name="twitter:image", content=buildImgPath('ol-brand/overleaf_og_logo.png')) +else + //- the default image for ShareLaTeX + meta(name="twitter:image", content='/touch-icon-192x192.png') + +//- Open Graph +//- to do - add og:url +if (settings.social && settings.social.facebook && settings.social.facebook.appId) + meta(property="fb:app_id", content=settings.social.facebook.appId) + +if (metadata && metadata.openGraphDescription) + meta(property="og:description", content=metadata.openGraphDescription) +else + meta(property="og:description", content=translate("site_description")) + +if (metadata && metadata.openGraphImage && metadata.openGraphImage.fields) + //- from the CMS + meta(property="og:image", content=metadata.openGraphImage.fields.file.url) +else if (settings.overleaf) + //- the default image for Overleaf + meta(property="og:image", content=buildImgPath('ol-brand/overleaf_og_logo.png')) +else + //- the default image for ShareLaTeX + meta(property="og:image", content='/touch-icon-192x192.png') + +if (metadata && metadata.openGraphType) + meta(property="og:type", metadata.openGraphType) +else + meta(property="og:type", content="website") + +if (metadata && metadata.openGraphVideo) + //- from the CMS + meta(property="og:video", content=metadata.openGraphVideo) + +//- Viewport +if metadata && metadata.viewport + meta(name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes") + +//- Noindex +if settings.robotsNoindex + meta(name="robots" content="noindex") + + +//- Icons +link(rel="icon", href="/favicon.ico") +link(rel="icon", sizes="192x192", href="/touch-icon-192x192.png") +link(rel="apple-touch-icon-precomposed", href="/apple-touch-icon-precomposed.png") +link(rel="mask-icon", href="/mask-favicon.svg", color="#138A07") diff --git a/services/web/app/views/_mixins/faq_search.pug b/services/web/app/views/_mixins/faq_search.pug new file mode 100644 index 0000000000..ca51cb29c2 --- /dev/null +++ b/services/web/app/views/_mixins/faq_search.pug @@ -0,0 +1,32 @@ +mixin faq_search(headerText, headerClass) + if (typeof(settings.algolia) != "undefined" && typeof(settings.algolia.indexes) != "undefined" && typeof(settings.algolia.indexes.wiki) != "undefined") + if headerText + div(class=headerClass, ng-non-bindable) #{headerText} + .wiki(ng-controller="SearchWikiController") + form.project-search.form-horizontal(role="search") + .form-group.has-feedback.has-feedback-left + .col-sm-12 + input.form-control(type='text', ng-model='searchQueryText', ng-keyup='search()', placeholder="Search help library…") + i.fa.fa-search.form-control-feedback-left(aria-hidden="true") + i.fa.fa-times.form-control-feedback( + ng-click="clearSearchText()", + style="cursor: pointer;", + ng-show="searchQueryText.length > 0" + aria-hidden="true" + ) + button.sr-only( + type="button" + ng-click="clearSearchText()", + ng-show="searchQueryText.length > 0" + ) #{translate('clear_search')} + + .row(role="region" aria-label="search results") + .col-md-12(ng-cloak) + span.sr-only(ng-show="searchQueryText.length > 0" aria-live="polite") + span(ng-if="hits_total > config_hits_per_page") Showing first {{hits.length}} results of {{hits_total}} for {{searchQueryText}} + span(ng-if="hits_total <= config_hits_per_page") {{hits.length}} results for {{searchQueryText}} + a(ng-href='{{hit.url}}',ng-repeat='hit in hits').search-result.card.card-thin + span(ng-bind-html='hit.name') + div.search-result-content(ng-show="hit.content != ''", ng-bind-html='hit.content') + .row-spaced-small.search-result.card.card-thin(ng-if="!processingSearch && searchQueryText.length > 1 && hits.length === 0") + p #{translate("no_search_results")} diff --git a/services/web/app/views/_mixins/links.pug b/services/web/app/views/_mixins/links.pug new file mode 100644 index 0000000000..24ba1d0288 --- /dev/null +++ b/services/web/app/views/_mixins/links.pug @@ -0,0 +1,118 @@ +mixin linkAdvisors(linkText, linkClass, track) + //- To Do: verify path + - var gaCategory = track && track.category ? track.category : 'All' + - var gaAction = track && track.action ? track.action : null + - var gaLabel = track && track.label ? track.label : null + - var mb = track && track.mb ? 'true' : null + - var mbSegmentation = track && track.segmentation ? track.segmentation : null + - var trigger = track && track.trigger ? track.trigger : null + a(href="/advisors" + class=linkClass ? linkClass : '' + event-tracking-ga=gaCategory + event-tracking=gaAction + event-tracking-label=gaLabel + event-tracking-trigger=trigger + event-tracking-mb=mb + event-segmentation=mbSegmentation + ) + span(ng-non-bindable) #{linkText ? linkText : 'advisor programme'} + +mixin linkBenefits(linkText, linkClass) + a(href=(settings.siteUrl ? settings.siteUrl : '') + "/for/authors" class=linkClass ? linkClass : '', ng-non-bindable) + | #{linkText ? linkText : 'benefits'} + +mixin linkBlog(linkText, linkClass, slug) + if slug + a(href=(settings.siteUrl ? settings.siteUrl : '') + "/blog/" + slug class=linkClass ? linkClass : '', ng-non-bindable) + | #{linkText ? linkText : 'blog'} + +mixin linkContact(linkText, linkClass) + a(href=(settings.siteUrl ? settings.siteUrl : '') + "/contact" class=linkClass ? linkClass : '', ng-non-bindable) + | #{linkText ? linkText : 'contact'} + +mixin linkDash(linkText, linkClass) + a(href="/project" class=linkClass ? linkClass : '', ng-non-bindable) + | #{linkText ? linkText : 'project dashboard'} + +mixin linkEducation(linkText, linkClass) + a(href=(settings.siteUrl ? settings.siteUrl : '') + "/for/edu" class=linkClass ? linkClass : '', ng-non-bindable) + | #{linkText ? linkText : 'teaching toolkit'} + +mixin linkInvite(linkText, linkClass, track) + - var gaCategory = track && track.category ? track.category : 'All' + - var gaAction = track && track.action ? track.action : null + - var gaLabel = track && track.label ? track.label : null + - var mb = track && track.mb ? 'true' : null + - var mbSegmentation = track && track.segmentation ? track.segmentation : null + - var trigger = track && track.trigger ? track.trigger : null + + a(href="/user/bonus" + class=linkClass ? linkClass : '' + event-tracking-ga=gaCategory + event-tracking=gaAction + event-tracking-label=gaLabel + event-tracking-trigger=trigger + event-tracking-mb=mb + event-segmentation=mbSegmentation + ) + span(ng-non-bindable) #{linkText ? linkText : 'invite your friends'} + +mixin linkPlansAndPricing(linkText, linkClass) + a(href="/user/subscription/plans" class=linkClass ? linkClass : '', ng-non-bindable) + | #{linkText ? linkText : 'plans and pricing'} + +mixin linkPrintNewTab(linkText, linkClass, icon, track) + - var gaCategory = track && track.category ? track.category : null + - var gaAction = track && track.action ? track.action : null + - var gaLabel = track && track.label ? track.label : null + - var mb = track && track.mb ? 'true' : null + - var mbSegmentation = track && track.segmentation ? track.segmentation : null + - var trigger = track && track.trigger ? track.trigger : null + + a(href='?media=print' + class=linkClass ? linkClass : '' + event-tracking-ga=gaCategory + event-tracking=gaAction + event-tracking-label=gaLabel + event-tracking-trigger=trigger + event-tracking-mb=mb + event-segmentation=mbSegmentation + target="_BLANK", + rel="noopener noreferrer" + ) + if icon + i(class="fa fa-print") + |   + span(ng-non-bindable) #{linkText ? linkText : 'print'} + +mixin linkSignIn(linkText, linkClass, redirect) + a(href=`/login${redirect ? '?redir=' + redirect : ''}` class=linkClass ? linkClass : '', ng-non-bindable) + | #{linkText ? linkText : 'sign in'} + +mixin linkSignUp(linkText, linkClass, redirect) + a(href=`/register${redirect ? '?redir=' + redirect : ''}` class=linkClass ? linkClass : '', ng-non-bindable) + | #{linkText ? linkText : 'sign up'} + +mixin linkTweet(linkText, linkClass, tweetText, track) + //- twitter-share-button is required by twitter + - var gaCategory = track && track.category ? track.category : 'All' + - var gaAction = track && track.action ? track.action : null + - var gaLabel = track && track.label ? track.label : null + - var mb = track && track.mb ? 'true' : null + - var mbSegmentation = track && track.segmentation ? track.segmentation : null + - var trigger = track && track.trigger ? track.trigger : null + a(class="twitter-share-button " + linkClass + event-tracking-ga=gaCategory + event-tracking=gaAction + event-tracking-label=gaLabel + event-tracking-trigger=trigger + event-tracking-mb=mb + event-segmentation=mbSegmentation + href="https://twitter.com/intent/tweet?text=" + tweetText + target="_BLANK", + rel="noopener noreferrer" + ) #{linkText ? linkText : 'tweet'} + +mixin linkUniversities(linkText, linkClass) + a(href=(settings.siteUrl ? settings.siteUrl : '') + "/for/universities" class=linkClass ? linkClass : '', ng-non-bindable) + | #{linkText ? linkText : 'universities'} diff --git a/services/web/app/views/_mixins/pagination.pug b/services/web/app/views/_mixins/pagination.pug new file mode 100644 index 0000000000..717cf0d5e2 --- /dev/null +++ b/services/web/app/views/_mixins/pagination.pug @@ -0,0 +1,74 @@ +mixin pagination(pages, page_path, max_btns) + //- @param pages.current_page the current page viewed + //- @param pages.total_pages previously calculated, + //- based on total entries and entries per page + //- @param page_path the relative path, minus a trailing slash and page param + //- @param max_btns max number of buttons on either side of the current page + //- button and excludes first, prev, next, last + + if pages && pages.current_page && pages.total_pages && pages.total_pages > 1 + - var max_btns = max_btns || 4 + - var prev_page = Math.max(parseInt(pages.current_page, 10) - max_btns, 1) + - var next_page = parseInt(pages.current_page, 10) + 1 + - var next_index = 0; + - var full_page_path = page_path + "/page/" + + nav(role="navigation" aria-label="Pagination Navigation") + ul.pagination + if pages.current_page > 1 + li + a( + aria-label="Go to first page" + href=page_path + ) « First + li + a( + aria-label="Go to previous page" + href=full_page_path + (parseInt(pages.current_page, 10) - 1) + rel="prev" + ) ‹ Prev + + if pages.current_page - max_btns > 1 + li + span … + + while prev_page < pages.current_page + li + a( + aria-label="Go to page " + prev_page + href=full_page_path + prev_page + ) #{prev_page} + - prev_page++ + + li(class="active") + span( + aria-label="Current Page, Page " + pages.current_page + aria-current="true" + ) #{pages.current_page} + + if pages.current_page < pages.total_pages + while next_page <= pages.total_pages && next_index < max_btns + li + a( + aria-label="Go to page " + next_page + href=full_page_path + next_page + ) #{next_page} + - next_page++ + - next_index++ + + if next_page <= pages.total_pages + li + span … + + li + a( + aria-label="Go to next page" + href=full_page_path + (parseInt(pages.current_page, 10) + 1) + rel="next" + ) Next › + + li + a( + aria-label="Go to last page" + href=full_page_path + pages.total_pages + ) Last » diff --git a/services/web/app/views/_mixins/reconfirm_affiliation.pug b/services/web/app/views/_mixins/reconfirm_affiliation.pug new file mode 100644 index 0000000000..7f9443e231 --- /dev/null +++ b/services/web/app/views/_mixins/reconfirm_affiliation.pug @@ -0,0 +1,38 @@ +mixin reconfirmAffiliationNotification(location) + .reconfirm-notification(ng-controller="UserAffiliationsReconfirmController") + div(ng-if="!reconfirm[userEmail.email].reconfirmationSent" style="width:100%;") + i.fa.fa-warning + + button.btn-reconfirm.btn.btn-sm.btn-info( + data-location=location + ng-if="!ui.sentReconfirmation" + ng-click="requestReconfirmation($event, userEmail)" + ng-disabled="ui.isMakingRequest" + ) #{translate("confirm_affiliation")} + + | !{translate("are_you_still_at", {institutionName: '{{userEmail.affiliation.institution.name}}'}, ['strong'])}  + + if location == '/user/settings' + | !{translate('please_reconfirm_institutional_email', {}, [{ name: 'span' }])} + span(ng-if="userEmail.default")  #{translate('need_to_add_new_primary_before_remove')} + else + | !{translate("please_reconfirm_institutional_email", {}, [{name: 'a', attrs: {href: '/user/settings?remove={{userEmail.email}}' }}])} + + |   + a(href="/learn/how-to/Institutional_Email_Reconfirmation") #{translate("learn_more")} + + div(ng-if="reconfirm[userEmail.email].reconfirmationSent") + | !{translate("please_check_your_inbox_to_confirm", {institutionName: '{{userEmail.affiliation.institution.name}}'}, ['strong'])} + |   + button( + class="btn-inline-link" + ng-click="requestReconfirmation($event, userEmail)" + ng-disabled="ui.isMakingRequest" + ) #{translate('resend_confirmation_email')} + +mixin reconfirmedAffiliationNotification() + .alert.alert-info + .reconfirm-notification + div(style="width:100%;") + //- extra div for flex styling + | !{translate("your_affiliation_is_confirmed", {institutionName: '{{userEmail.affiliation.institution.name}}'}, ['strong'])} #{translate('thank_you_exclamation')} diff --git a/services/web/app/views/admin/index.pug b/services/web/app/views/admin/index.pug new file mode 100644 index 0000000000..a2dd4a8d1b --- /dev/null +++ b/services/web/app/views/admin/index.pug @@ -0,0 +1,86 @@ +extends ../layout + +block content + .content.content-alt + .container + .row + .col-xs-12 + .card + .page-header + h1 Admin Panel + tabset(bookmarkable-tabset ng-cloak) + tab(heading="System Messages" bookmarkable-tab="system-messages") + each message in systemMessages + .alert.alert-info.row-spaced(ng-non-bindable) #{message.content} + hr + form(method='post', action='/admin/messages') + input(name="_csrf", type="hidden", value=csrfToken) + .form-group + label(for="content") + input.form-control(name="content", type="text", placeholder="Message…", required) + button.btn.btn-primary(type="submit") Post Message + hr + form(method='post', action='/admin/messages/clear') + input(name="_csrf", type="hidden", value=csrfToken) + button.btn.btn-danger(type="submit") Clear all messages + + + tab(heading="Open Sockets" bookmarkable-tab="open-sockets") + .row-spaced + ul + each agents, url in openSockets + li(ng-non-bindable) #{url} - total : #{agents.length} + ul + each agent in agents + li(ng-non-bindable) #{agent} + + tab(heading="Open/Close Editor" bookmarkable-tab="open-close-editor") + if hasFeature('saas') + | The "Open/Close Editor" feature is not available in SAAS. + else + .row-spaced + form(method='post',action='/admin/closeEditor') + input(name="_csrf", type="hidden", value=csrfToken) + button.btn.btn-danger(type="submit") Close Editor + p.small Will stop anyone opening the editor. Will NOT disconnect already connected users. + + .row-spaced + form(method='post',action='/admin/disconnectAllUsers') + input(name="_csrf", type="hidden", value=csrfToken) + button.btn.btn-danger(type="submit") Disconnect all users + p.small Will force disconnect all users with the editor open. Make sure to close the editor first to avoid them reconnecting. + + .row-spaced + form(method='post',action='/admin/openEditor') + input(name="_csrf", type="hidden", value=csrfToken) + button.btn.btn-danger(type="submit") Reopen Editor + p.small Will reopen the editor after closing. + + if hasFeature('saas') + tab(heading="TPDS/Dropbox Management" bookmarkable-tab="tpds") + h3 Flush project to TPDS + .row + form.col-xs-6(method='post',action='/admin/flushProjectToTpds') + input(name="_csrf", type="hidden", value=csrfToken) + .form-group + label(for='project_id') project_id + input.form-control(type='text', name='project_id', placeholder='project_id', required) + .form-group + button.btn-primary.btn(type='submit') Flush + hr + h3 Poll Dropbox for user + .row + form.col-xs-6(method='post',action='/admin/pollDropboxForUser') + input(name="_csrf", type="hidden", value=csrfToken) + .form-group + label(for='user_id') user_id + input.form-control(type='text', name='user_id', placeholder='user_id', required) + .form-group + button.btn-primary.btn(type='submit') Poll + + tab(heading="Advanced" bookmarkable-tab="advanced") + .row-spaced + form(method='post',action='/admin/unregisterServiceWorker') + input(name="_csrf", type="hidden", value=csrfToken) + button.btn.btn-danger(type="submit") Unregister service worker + p.small Will force service worker reload for all users with the editor open. diff --git a/services/web/app/views/admin/register.pug b/services/web/app/views/admin/register.pug new file mode 100644 index 0000000000..4078c49484 --- /dev/null +++ b/services/web/app/views/admin/register.pug @@ -0,0 +1,40 @@ +extends ../layout + +block content + .content.content-alt + .container + .row + .col-md-12 + .card(ng-controller="RegisterUsersController") + .page-header + h1 Register New Users + form.form + .row + .col-md-4.col-xs-8 + input.form-control( + name="email", + type="text", + placeholder="jane@example.com, joe@example.com", + ng-model="inputs.emails", + on-enter="registerUsers()" + ) + .col-md-8.col-xs-4 + button.btn.btn-primary(ng-click="registerUsers()") #{translate("register")} + + .row-spaced(ng-show="error").ng-cloak.text-danger + p Sorry, an error occured + + .row-spaced(ng-show="users.length > 0").ng-cloak.text-success + p We've sent out welcome emails to the registered users. + p You can also manually send them URLs below to allow them to reset their password and log in for the first time. + p (Password reset tokens will expire after one week and the user will need registering again). + + hr(ng-show="users.length > 0").ng-cloak + table(ng-show="users.length > 0").table.table-striped.ng-cloak + tr + th #{translate("email")} + th Set Password Url + tr(ng-repeat="user in users") + td {{ user.email }} + td(style="word-break: break-all;") {{ user.setNewPasswordUrl }} + \ No newline at end of file diff --git a/services/web/app/views/beta_program/opt_in.pug b/services/web/app/views/beta_program/opt_in.pug new file mode 100644 index 0000000000..3e08d27643 --- /dev/null +++ b/services/web/app/views/beta_program/opt_in.pug @@ -0,0 +1,56 @@ +extends ../layout + +block content + main.content.content-alt#main-content + .container.beta-opt-in-wrapper + .row + .col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 + .card + .page-header.text-centered + h1 + | #{translate("sharelatex_beta_program")} + .beta-opt-in + .container-fluid + .row + .col-md-12 + if user.betaProgram + p #{translate("beta_program_already_participating")}. + p #{translate("thank_you_for_being_part_of_our_beta_program")}. + else + p #{translate("beta_program_benefits")} + + p #[strong How it works:] + ul + li #{translate("beta_program_badge_description")} #[span(aria-label=translate("beta_feature_badge") role="img").beta-badge] + li #{translate("you_will_be_able_to_contact_us_any_time_to_share_your_feedback")}. + li #{translate("we_may_also_contact_you_from_time_to_time_by_email_with_a_survey")}. + li #{translate("you_can_opt_in_and_out_of_the_program_at_any_time_on_this_page")}. + + .row.text-centered + .col-md-12 + if user.betaProgram + form(id="beta-program-opt-out", method="post", action="/beta/opt-out", novalidate) + input(type="hidden", name="_csrf", value=csrfToken) + .form-group + a( + href="https://forms.gle/CFEsmvZQTAwHCd3X9" + target="_blank" + rel="noopener noreferrer" + ).btn.btn-primary.btn-lg #{translate("give_feedback")} + .form-group + button.btn.btn-info.btn-sm( + type="submit" + ) + span #{translate("beta_program_opt_out_action")} + .form-group + a(href="/project").btn.btn-link.btn-sm #{translate("back_to_your_projects")} + else + form(id="beta-program-opt-in", method="post", action="/beta/opt-in", novalidate) + input(type="hidden", name="_csrf", value=csrfToken) + .form-group + button.btn.btn-primary( + type="submit" + ) + span #{translate("beta_program_opt_in_action")} + .form-group + a(href="/project").btn.btn-link.btn-sm #{translate("back_to_your_projects")} diff --git a/services/web/app/views/blog/blog_holder.pug b/services/web/app/views/blog/blog_holder.pug new file mode 100644 index 0000000000..d9a89d4c4b --- /dev/null +++ b/services/web/app/views/blog/blog_holder.pug @@ -0,0 +1,6 @@ +extends ../layout + +block content + .content.content-alt + .blog + | !{content} \ No newline at end of file diff --git a/services/web/app/views/general/400.pug b/services/web/app/views/general/400.pug new file mode 100644 index 0000000000..74f0182c33 --- /dev/null +++ b/services/web/app/views/general/400.pug @@ -0,0 +1,26 @@ +extends ../layout/layout-no-js + +block vars + - metadata = { title: 'Something went wrong', viewport: true } + +block body + body.full-height + main.content.content-alt.full-height#main-content + .container.full-height + .error-container.full-height + .error-details + p.error-status I'm sorry, Dave. I'm afraid I can't do that. + p.error-description There was a problem with your latest request + if(message) + |: #{message} + |. + br + | Please go back and try again. + p.error-description + | If the problem persists, please contact us at + | + a(href="mailto:" + settings.adminEmail) #{settings.adminEmail} + | . + a.error-btn(href="javascript:history.back()") Back + |   + a.btn.btn-default(href="/") Home diff --git a/services/web/app/views/general/404.pug b/services/web/app/views/general/404.pug new file mode 100644 index 0000000000..aa749166af --- /dev/null +++ b/services/web/app/views/general/404.pug @@ -0,0 +1,10 @@ +extends ../layout + +block content + main.content.content-alt#main-content + .container + .error-container + .error-details + p.error-status Not found + p.error-description #{translate("cant_find_page")} + a.error-btn(href="/") Home diff --git a/services/web/app/views/general/500.pug b/services/web/app/views/general/500.pug new file mode 100644 index 0000000000..a2f66d0a4f --- /dev/null +++ b/services/web/app/views/general/500.pug @@ -0,0 +1,22 @@ +extends ../layout/layout-no-js + +block vars + - metadata = { title: 'Something went wrong', viewport: true } + +block body + body.full-height + main.content.content-alt.full-height#main-content + .container.full-height + .error-container.full-height + .error-details + p.error-status Something went wrong, sorry. + p.error-description Our staff are probably looking into this, but if it continues, please check our status page at + | + | + a(href="http://" + settings.statusPageUrl) #{settings.statusPageUrl} + | + | or contact us at + | + a(href="mailto:" + settings.adminEmail) #{settings.adminEmail} + | . + a.error-btn(href="/") Home diff --git a/services/web/app/views/general/account-merge-error.pug b/services/web/app/views/general/account-merge-error.pug new file mode 100644 index 0000000000..44ee6b1511 --- /dev/null +++ b/services/web/app/views/general/account-merge-error.pug @@ -0,0 +1,11 @@ +extends ../layout + +block content + main.content.content-alt#main-content + .container + .row + .col-md-6.col-md-offset-3 + .card + .page-header + h1 Account Access Error + p.text-danger Sorry, an error occurred accessing your account. Please #[a(href="" ng-controller="ContactModal" ng-click="contactUsModal()") contact support] and provide any email addresses that you have used to sign in to Overleaf and/or ShareLaTeX for assistance. diff --git a/services/web/app/views/general/closed.pug b/services/web/app/views/general/closed.pug new file mode 100644 index 0000000000..ee56d89097 --- /dev/null +++ b/services/web/app/views/general/closed.pug @@ -0,0 +1,19 @@ +extends ../layout + +block content + main.content#main-content + .container + .row + .col-md-8.col-md-offset-2.text-center + .page-header + h1 Maintenance + p(ng-non-bindable) + if settings.statusPageUrl + | #{settings.appName} is currently down for maintenance. + | Please check our #[a(href='http://' + settings.statusPageUrl) status page] + | for updates. + else + | #{settings.appName} is currently down for maintenance. + | We should be back within minutes, but if not, or you have + | an urgent request, please contact us at + |  #{settings.adminEmail} diff --git a/services/web/app/views/general/post-gateway.pug b/services/web/app/views/general/post-gateway.pug new file mode 100644 index 0000000000..8be63d516d --- /dev/null +++ b/services/web/app/views/general/post-gateway.pug @@ -0,0 +1,21 @@ +extends ../layout + +block vars + - var suppressNavbar = true + - var suppressFooter = true + - var suppressSkipToContent = true + +block content + script(type="template", id="gateway-data")!= StringHelper.stringifyJsonForScript({ params: form_data }) + + .content.content-alt + .container + .row + .col-md-6.col-md-offset-3 + .card + p.text-center #{translate('processing_your_request')} + + form( + ng-controller="PostGatewayController", + ng-init="handleGateway();" + id='gateway' method='POST') diff --git a/services/web/app/views/general/unsupported-browser.pug b/services/web/app/views/general/unsupported-browser.pug new file mode 100644 index 0000000000..1d36d30fdf --- /dev/null +++ b/services/web/app/views/general/unsupported-browser.pug @@ -0,0 +1,22 @@ +extends ../layout/layout-no-js + +block vars + - metadata = { title: 'Unsupported browser', viewport: true } + +block body + body.full-height + main.content.content-alt.full-height#main-content + .container.full-height + .error-container.full-height + .error-details + h1.error-status Unsupported Browser + p.error-description + | Sorry, we don't support your browser anymore. Find out more about #[a(href="https://www.overleaf.com/learn/how-to/What_browsers_do_you_support") what browsers we support]. + br + | If you think you're seeing this message in error, please #[a(href="/contact") let us know]. + + if fromURL + p + | URL: + | + a(href=fromURL) #{fromURL} diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug new file mode 100644 index 0000000000..aa73207736 --- /dev/null +++ b/services/web/app/views/layout.pug @@ -0,0 +1,142 @@ + +doctype html +html( + lang=(currentLngCode || 'en') +) + - metadata = metadata || {} + + block vars + + head + include ./_metadata.pug + + if (typeof(gaExperiments) != "undefined") + |!{gaExperiments} + + //- Stylesheet + link(rel='stylesheet', href=buildCssPath(getCssThemeModifier(userSettings, brandVariation)), id="main-stylesheet") + link(rel='stylesheet', href=buildStylesheetPath("libraries.css")) + + block _headLinks + + if settings.i18n.subdomainLang + each subdomainDetails in settings.i18n.subdomainLang + if !subdomainDetails.hide + link(rel="alternate", href=subdomainDetails.url+currentUrl, hreflang=subdomainDetails.lngCode) + + //- Scripts + + //- Google Analytics + if (typeof(gaToken) != "undefined") + script(type="text/javascript", nonce=scriptNonce). + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); + script(type="text/javascript", nonce=scriptNonce). + ga('create', '#{gaToken}', '#{settings.cookieDomain.replace(/^\./, "")}'); + ga('set', 'anonymizeIp', true); + ga('send', 'pageview'); + + try { + ga.isBlocked = localStorage.getItem('gaBlocked') === 'true' + if (!ga.isBlocked) { + window.addEventListener('load', function () { + setTimeout(function () { + if (!ga.loaded) localStorage.setItem('gaBlocked', 'true') + }, 4000) + }) + } + } catch (e) {} + if gaOptimize === true && typeof(gaOptimizeId) != "undefined" + //- Anti-flicker snippet + style(type='text/css') .async-hide { opacity: 0 !important} + script(type="text/javascript", nonce=scriptNonce). + if (!ga.isBlocked) { + ga('require', '#{gaOptimizeId}'); + ga('send', 'event', 'pageview', document.title.substring(0, 499), window.location.href.substring(0, 499)); + (function(a,s,y,n,c,h,i,d,e){s.className+=' '+y;h.start=1*new Date; + h.end=i=function(){s.className=s.className.replace(RegExp(' ?'+y),'')}; + (a[n]=a[n]||[]).hide=h;setTimeout(function(){i();h.end=null},c);h.timeout=c; + })(window,document.documentElement,'async-hide','dataLayer',4000, + {'#{gaOptimizeId}':true}); + } + + else + script(type="text/javascript", nonce=scriptNonce). + window.ga = function() { console.log("would send to GA", arguments) }; + + block meta + meta(name="ol-csrfToken" content=csrfToken) + //- Configure dynamically loaded assets (via webpack) to be downloaded from CDN + //- See: https://webpack.js.org/guides/public-path/#on-the-fly + meta(name="ol-baseAssetPath" content=buildBaseAssetPath()) + + meta(name="ol-usersEmail" content=getUserEmail()) + meta(name="ol-sharelatex" data-type="json" content={ + siteUrl: settings.siteUrl, + wsUrl, + }) + meta(name="ol-ab" data-type="json" content={}) + meta(name="ol-user_id" content=getLoggedInUserId()) + //- Internationalisation settings + meta(name="ol-i18n" data-type="json" content={ + currentLangCode: currentLngCode + }) + //- Expose some settings globally to the frontend + meta(name="ol-ExposedSettings" data-type="json" content=ExposedSettings) + + if (typeof(settings.algolia) != "undefined") + meta(name="ol-algolia" data-type="json" content={ + appId: settings.algolia.app_id, + apiKey: settings.algolia.read_only_api_key, + indexes: settings.algolia.indexes + }) + + if (typeof(settings.templates) != "undefined") + meta(name="ol-sharelatex.templates" data-type="json" content={ + user_id : settings.templates.user_id, + cdnDomain : settings.templates.cdnDomain, + indexName : settings.templates.indexName + }) + + block head-scripts + + + body(ng-csp=(cspEnabled ? "no-unsafe-eval" : false)) + if(settings.recaptcha && settings.recaptcha.siteKeyV3) + script(type="text/javascript", nonce=scriptNonce, src="https://www.recaptcha.net/recaptcha/api.js?render="+settings.recaptcha.siteKeyV3) + + if (typeof(suppressSkipToContent) == "undefined") + a(class="skip-to-content" href="#main-content") #{translate('skip_to_content')} + + if (typeof(suppressNavbar) == "undefined") + include layout/navbar + + block content + + if (typeof(suppressFooter) == "undefined") + include layout/footer + + != moduleIncludes("contactModal", locals) + + block foot-scripts + script(type="text/javascript", nonce=scriptNonce, src=buildJsPath('libraries.js')) + script(type="text/javascript", nonce=scriptNonce, src=buildJsPath('main.js')) + script(type="text/javascript", nonce=scriptNonce). + //- Look for bundle + var cdnBlocked = typeof Frontend === 'undefined' + //- Prevent loops + var noCdnAlreadyInUrl = window.location.href.indexOf("nocdn=true") != -1 + if (cdnBlocked && !noCdnAlreadyInUrl && navigator.userAgent.indexOf("Googlebot") == -1) { + //- Set query param, server will not set CDN url + window.location.search += "&nocdn=true"; + } + if hasFeature('saas') + script(type="text/javascript", nonce=scriptNonce). + //- Test for CDN availability and report to sentry if loading failed + var cdnLoadTest = document.createElement('img') + cdnLoadTest.addEventListener('error', function () { + throw new Error('CDN test image load error (cdn.overleaf.net)') + }) + cdnLoadTest.src = 'https://cdn.overleaf.net/img/1p.gif' diff --git a/services/web/app/views/layout/footer.pug b/services/web/app/views/layout/footer.pug new file mode 100644 index 0000000000..518e3e1caf --- /dev/null +++ b/services/web/app/views/layout/footer.pug @@ -0,0 +1,44 @@ + + +footer.site-footer + .site-footer-content.hidden-print + .row + ul.col-md-9 + + if Object.keys(settings.i18n.subdomainLang).length > 1 + li.dropdown.dropup.subdued(dropdown) + a.dropdown-toggle( + href="#", + dropdown-toggle, + data-toggle="dropdown", + aria-haspopup="true", + aria-expanded="false", + aria-label="Select " + translate('language') + tooltip=translate('language') + ) + figure(class="sprite-icon sprite-icon-lang sprite-icon-"+currentLngCode alt=translate(currentLngCode)) + + ul.dropdown-menu(role="menu") + li.dropdown-header #{translate("language")} + each subdomainDetails, subdomain in settings.i18n.subdomainLang + if !subdomainDetails.hide + li.lngOption + a.menu-indent(href=subdomainDetails.url+currentUrlWithQueryParams) + figure(class="sprite-icon sprite-icon-lang sprite-icon-"+subdomainDetails.lngCode alt=translate(subdomainDetails.lngCode)) + | #{translate(subdomainDetails.lngCode)} + //- img(src="/img/flags/24/.png") + each item in nav.left_footer + li + if item.url + a(href=item.url, class=item.class) !{translate(item.text)} + else + | !{item.text} + + ul.col-md-3.text-right + + each item in nav.right_footer + li(ng-non-bindable) + if item.url + a(href=item.url, class=item.class, aria-label=item.label) !{item.text} + else + | !{item.text} diff --git a/services/web/app/views/layout/layout-no-js.pug b/services/web/app/views/layout/layout-no-js.pug new file mode 100644 index 0000000000..c86721a810 --- /dev/null +++ b/services/web/app/views/layout/layout-no-js.pug @@ -0,0 +1,18 @@ +doctype html +html(lang="en") + + - metadata = metadata || {} + block vars + + head + if (metadata && metadata.title) + title= metadata.title + if metadata && metadata.viewport + meta(name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes") + + link(rel="icon", href="/favicon.ico") + + if buildCssPath + link(rel="stylesheet", href=buildCssPath()) + +block body diff --git a/services/web/app/views/layout/navbar.pug b/services/web/app/views/layout/navbar.pug new file mode 100644 index 0000000000..67215f06b3 --- /dev/null +++ b/services/web/app/views/layout/navbar.pug @@ -0,0 +1,96 @@ +nav.navbar.navbar-default.navbar-main + .container-fluid + .navbar-header + button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}", aria-label="Toggle " + translate('navigation')) + i.fa.fa-bars(aria-hidden="true") + if settings.nav.custom_logo + a(href='/', aria-label=settings.appName, style='background-image:url("'+settings.nav.custom_logo+'")').navbar-brand + else if (nav.title) + a(href='/', aria-label=settings.appName, ng-non-bindable).navbar-title #{nav.title} + else + a(href='/', aria-label=settings.appName).navbar-brand + + .navbar-collapse.collapse(collapse="navCollapsed") + + ul.nav.navbar-nav.navbar-right + if (getSessionUser() && getSessionUser().isAdmin) + li.dropdown(class="subdued", dropdown) + a.dropdown-toggle(href, dropdown-toggle) + | Admin + b.caret + ul.dropdown-menu + li + a(href="/admin") Manage Site + li + a(href="/admin/user") Manage Users + + + // loop over header_extras + each item in nav.header_extras + - + if ((item.only_when_logged_in && getSessionUser()) + || (item.only_when_logged_out && (!getSessionUser())) + || (!item.only_when_logged_out && !item.only_when_logged_in && !item.only_content_pages) + || (item.only_content_pages && (typeof(suppressNavContentLinks) == "undefined" || !suppressNavContentLinks)) + ){ + var showNavItem = true + } else { + var showNavItem = false + } + + if showNavItem + if item.dropdown + li.dropdown(class=item.class, dropdown) + a.dropdown-toggle(href, dropdown-toggle) + | !{translate(item.text)} + b.caret + ul.dropdown-menu + each child in item.dropdown + if child.divider + li.divider + else + li + if child.url + a(href=child.url, class=child.class) !{translate(child.text)} + else + | !{translate(child.text)} + else + li(class=item.class) + if item.url + a(href=item.url, class=item.class) !{translate(item.text)} + else + | !{translate(item.text)} + + // logged out + if !getSessionUser() + // register link + if hasFeature('registration-page') + li + a(href="/register") #{translate('register')} + + // login link + li + a(href="/login") #{translate('log_in')} + + // projects link and account menu + if getSessionUser() + li + a(href="/project") #{translate('Projects')} + li.dropdown(dropdown) + a.dropdown-toggle(href, dropdown-toggle) + | #{translate('Account')} + b.caret + ul.dropdown-menu + li + div.subdued {{ usersEmail }} + li.divider.hidden-xs.hidden-sm + li + a(href="/user/settings") #{translate('Account Settings')} + if nav.showSubscriptionLink + li + a(href="/user/subscription") #{translate('subscription')} + li.divider.hidden-xs.hidden-sm + li + form(method="POST" action="/logout") + input(name='_csrf', type='hidden', value=csrfToken) + button.btn-link.text-left.dropdown-menu-button #{translate('log_out')} diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug new file mode 100644 index 0000000000..36b87d1bc7 --- /dev/null +++ b/services/web/app/views/project/editor.pug @@ -0,0 +1,204 @@ +extends ../layout + +block vars + - var suppressNavbar = true + - var suppressFooter = true + - var suppressSkipToContent = true + - metadata.robotsNoindexNofollow = true + +block _headLinks + link(rel='stylesheet', href=buildStylesheetPath("ide.css")) + +block content + .editor(ng-controller="IdeController").full-size + //- required by react2angular-shared-context, must be rendered as a top level component + shared-context-react() + .loading-screen(ng-if="state.loading") + .loading-screen-brand-container + .loading-screen-brand( + style="height: 20%;" + ng-style="{ 'height': state.load_progress + '%' }" + ) + h3.loading-screen-label(ng-if="!state.error") #{translate("loading")} + span.loading-screen-ellip . + span.loading-screen-ellip . + span.loading-screen-ellip . + p.loading-screen-error(ng-if="state.error").ng-cloak + span(ng-bind-html="state.error") + + .global-alerts(ng-cloak ng-hide="editor.error_state") + .alert.alert-danger.small(ng-if="connection.forced_disconnect") + strong #{translate("disconnected")} + + .alert.alert-warning.small(ng-if="connection.reconnection_countdown") + strong #{translate("lost_connection")}. + | #{translate("reconnecting_in_x_secs", {seconds:"{{ connection.reconnection_countdown }}"})}. + a#try-reconnect-now-button.alert-link-as-btn.pull-right(href, ng-click="tryReconnectNow()") #{translate("try_now")} + + .alert.alert-warning.small(ng-if="connection.reconnecting && connection.stillReconnecting") + strong #{translate("reconnecting")}… + + .alert.alert-warning.small(ng-if="sync_tex_error") + strong #{translate("synctex_failed")}. + a#synctex-more-info-button.alert-link-as-btn.pull-right( + href="/learn/how-to/SyncTeX_Errors" + target="_blank" + ) #{translate("more_info")} + + .alert.alert-warning.small(ng-if="connection.inactive_disconnect") + strong #{translate("editor_disconected_click_to_reconnect")} + + .alert.alert-warning.small(ng-if="connection.debug") {{ connection.state }} + + .div(ng-controller="SavingNotificationController") + .alert.alert-warning.small(ng-repeat="(doc_id, state) in docSavingStatus" ng-if="state.unsavedSeconds > 8") #{translate("saving_notification_with_seconds", {docname:"{{ state.doc.name }}", seconds:"{{ state.unsavedSeconds }}"})} + + .div(ng-controller="SystemMessagesController") + .alert.alert-warning.system-message( + ng-repeat="message in messages" + ng-controller="SystemMessageController" + ng-hide="hidden" + ) + button(ng-hide="protected",ng-click="hide()").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} + .system-message-content(ng-bind-html="htmlContent") + + include ./editor/left-menu + + #chat-wrapper.full-size( + layout="chat", + spacing-open="{{ui.chatResizerSizeOpen}}", + spacing-closed="{{ui.chatResizerSizeClosed}}", + initial-size-east="250", + init-closed-east="true", + open-east="ui.chatOpen", + ng-hide="state.loading", + ng-cloak + ) + .ui-layout-center + if showNewNavigationUI + include ./editor/header-react + else + include ./editor/header + + != moduleIncludes("publish:body", locals) + + include ./editor/history/toolbarV2.pug + + main#ide-body( + ng-cloak, + role="main", + ng-class="{ 'ide-history-open' : (ui.view == 'history' && history.isV2) }", + layout="main", + ng-hide="state.loading", + resize-on="layout:chat:resize,history:toggle", + minimum-restore-size-west="130" + custom-toggler-pane=hasFeature('custom-togglers') ? "west" : false + custom-toggler-msg-when-open=hasFeature('custom-togglers') ? translate("tooltip_hide_filetree") : false + custom-toggler-msg-when-closed=hasFeature('custom-togglers') ? translate("tooltip_show_filetree") : false + ) + .ui-layout-west + include ./editor/file-tree-react + include ./editor/file-tree-history + include ./editor/history/fileTreeV2 + + .ui-layout-center + include ./editor/editor + + if showNewFileViewUI + include ./editor/file-view + else + include ./editor/binary-file + include ./editor/history + + if !isRestrictedTokenMember + .ui-layout-east + aside.chat( + ng-controller="ReactChatController" + ) + chat() + + script(type="text/ng-template", id="genericMessageModalTemplate") + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="done()" + aria-label="Close" + ) + span(aria-hidden="true") × + h3 {{ title }} + .modal-body(ng-bind-html="message") + .modal-footer + button.btn.btn-info(ng-click="done()") #{translate("ok")} + + script(type="text/ng-template", id="outOfSyncModalTemplate") + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="done()" + aria-label="Close" + ) + span(aria-hidden="true") × + h3 {{ title }} + .modal-body(ng-bind-html="message") + + .modal-body + button.btn.btn-info( + ng-init="showFileContents = false" + ng-click="showFileContents = !showFileContents" + ) + | {{showFileContents ? "Hide" : "Show"}} Local File Contents + .text-preview(ng-show="showFileContents") + textarea.scroll-container(readonly="readonly" rows="{{editorContentRows}}") + | {{editorContent}} + + .modal-footer + button.btn.btn-info(ng-click="done()") #{translate("reload_editor")} + + script(type="text/ng-template", id="lockEditorModalTemplate") + .modal-header + h3 {{ title }} + .modal-body(ng-bind-html="message") + +block append meta + meta(name="ol-useV2History" data-type="boolean" content=useV2History) + meta(name="ol-project_id" content=project_id) + meta(name="ol-userSettings" data-type="json" content=userSettings) + meta(name="ol-user" data-type="json" content=user) + meta(name="ol-anonymous" data-type="boolean" content=anonymous) + meta(name="ol-brandVariation" data-type="json" content=brandVariation) + meta(name="ol-anonymousAccessToken" content=anonymousAccessToken) + meta(name="ol-isTokenMember" data-type="boolean" content=isTokenMember) + meta(name="ol-isRestrictedTokenMember" data-type="boolean" content=isRestrictedTokenMember) + meta(name="ol-maxDocLength" data-type="json" content=maxDocLength) + meta(name="ol-wikiEnabled" data-type="boolean" content=!!(settings.apis.wiki && settings.apis.wiki.url)) + meta(name="ol-gitBridgePublicBaseUrl" content=gitBridgePublicBaseUrl) + //- Set base path for Ace scripts loaded on demand/workers and don't use cdn + meta(name="ol-aceBasePath" content="/js/" + lib('ace')) + //- Set path for PDFjs CMaps + meta(name="ol-pdfCMapsPath" content="/js/cmaps/") + //- enable doc hash checking for all projects + //- used in public/js/libs/sharejs.js + meta(name="ol-useShareJsHash" data-type="boolean" content=true) + meta(name="ol-wsRetryHandshake" data-type="json" content=settings.wsRetryHandshake) + meta(name="ol-showNewLogsUI" data-type="boolean" content=showNewLogsUI) + meta(name="ol-logsUISubvariant" content=logsUISubvariant) + meta(name="ol-showSymbolPalette" data-type="boolean" content=showSymbolPalette) + meta(name="ol-enablePdfCaching" data-type="boolean" content=enablePdfCaching) + meta(name="ol-trackPdfDownload" data-type="boolean" content=trackPdfDownload) + meta(name="ol-resetServiceWorker" data-type="boolean" content=resetServiceWorker) + + - var fileActionI18n = ['edited', 'renamed', 'created', 'deleted'].reduce((acc, i) => {acc[i] = translate('file_action_' + i); return acc}, {}) + meta(name="ol-fileActionI18n" data-type="json" content=fileActionI18n) + + if (settings.overleaf != null) + meta(name="ol-overallThemes" data-type="json" content=overallThemes) + +block foot-scripts + script(type="text/javascript", nonce=scriptNonce, src=(wsUrl || '/socket.io') + '/socket.io.js') + script(type="text/javascript", nonce=scriptNonce, src=mathJaxPath) + script(type="text/javascript", nonce=scriptNonce, src=buildJsPath('libraries.js')) + script(type="text/javascript", nonce=scriptNonce, src=buildJsPath('ide.js')) diff --git a/services/web/app/views/project/editor/binary-file-header.pug b/services/web/app/views/project/editor/binary-file-header.pug new file mode 100644 index 0000000000..3b26632191 --- /dev/null +++ b/services/web/app/views/project/editor/binary-file-header.pug @@ -0,0 +1,67 @@ +div.binary-file-header + // Linked Files: URL + div(ng-if="openFile.linkedFileData.provider == 'url'") + p + i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon + | Imported from + | + a(ng-href='{{openFile.linkedFileData.url}}') {{ displayUrl(openFile.linkedFileData.url) }} + | + | at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }} + + // Linked Files: Project File + div(ng-if="openFile.linkedFileData.provider == 'project_file'") + p + i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon + | Imported from + | + a(ng-if='!openFile.linkedFileData.v1_source_doc_id' + ng-href='/project/{{openFile.linkedFileData.source_project_id}}' target="_blank") + | Another project + span(ng-if='openFile.linkedFileData.v1_source_doc_id') + | Another project + | /{{ openFile.linkedFileData.source_entity_path.slice(1) }}, + | + | at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }} + + // Linked Files: Project Output File + div(ng-if="openFile.linkedFileData.provider == 'project_output_file'") + p + i.fa.fa-fw.fa-external-link-square.fa-rotate-180.linked-file-icon + | Imported from the output of + | + a(ng-if='!openFile.linkedFileData.v1_source_doc_id' + ng-href='/project/{{openFile.linkedFileData.source_project_id}}' target="_blank") + | Another project + span(ng-if='openFile.linkedFileData.v1_source_doc_id') + | Another project + | : {{ openFile.linkedFileData.source_output_file_path }}, + | + | at {{ openFile.created | formatDate:'h:mm a' }} {{ openFile.created | relativeDate }} + + != moduleIncludes("binaryFile:linkedFileInfo", locals) + + // Bottom Controls + span(ng-if="openFile.linkedFileData.provider") + button.btn.btn-success( + href, ng-click="refreshFile(openFile)", + ng-disabled="refreshing" + ) + i.fa.fa-fw.fa-refresh(ng-class={'fa-spin': refreshing}) + | + span(ng-show="!refreshing") Refresh + span(ng-show="refreshing") Refreshing… + |   + a.btn.btn-info( + ng-href="/project/{{ project_id }}/file/{{ openFile.id }}" + ) + i.fa.fa-fw.fa-download + | + | #{translate("download")} + + // Refresh Error + div(ng-if="refreshError").row + br + .alert.alert-danger.col-md-6.col-md-offset-3 + | Error: {{ refreshError}} + != moduleIncludes("binaryFile:linkedFileRefreshError", locals) diff --git a/services/web/app/views/project/editor/binary-file.pug b/services/web/app/views/project/editor/binary-file.pug new file mode 100644 index 0000000000..ce4147c083 --- /dev/null +++ b/services/web/app/views/project/editor/binary-file.pug @@ -0,0 +1,41 @@ +div.binary-file.full-size( + ng-controller="BinaryFileController" + ng-show="ui.view == 'file'" + ng-if="openFile" +) + + include ./binary-file-header + + img( + ng-show="!failedLoad" + ng-src="/project/{{ project_id }}/file/{{ openFile.id }}" + ng-if="isImageFile()" + ng-class="{'img-preview': !imgLoaded}" + onerror="sl_binaryFilePreviewError()" + onabort="sl_binaryFilePreviewError()" + onload="sl_binaryFilePreviewLoaded()" + ) + + img( + ng-show="!failedLoad" + ng-src="/project/{{ project_id }}/file/{{ openFile.id }}?format=png" + ng-if="isPreviewableFile()" + ng-class="{'img-preview': !imgLoaded}" + onerror="sl_binaryFilePreviewError()" + onabort="sl_binaryFilePreviewError()" + onload="sl_binaryFilePreviewLoaded()" + ) + + div(ng-if="isTextFile() && !textPreview.error") + div.text-loading(ng-show="textPreview.loading && !textPreview.error") + | #{translate('loading')}… + div.text-preview(ng-show="textPreview.data && !textPreview.loading && !textPreview.error") + div.scroll-container + p + | {{ textPreview.data }} + p(ng-show="textPreview.shouldShowDots") + | … + + p.no-preview( + ng-if="failedLoad || textPreview.error || isUnpreviewableFile()" + ) #{translate("no_preview_available")} diff --git a/services/web/app/views/project/editor/editor-no-symbol-palette.pug b/services/web/app/views/project/editor/editor-no-symbol-palette.pug new file mode 100644 index 0000000000..d858481736 --- /dev/null +++ b/services/web/app/views/project/editor/editor-no-symbol-palette.pug @@ -0,0 +1,40 @@ +.ui-layout-center( + ng-controller="ReviewPanelController", + ng-class="{\ + 'rp-unsupported': editor.showRichText,\ + 'rp-state-current-file': (reviewPanel.subView === SubViews.CUR_FILE),\ + 'rp-state-current-file-expanded': (reviewPanel.subView === SubViews.CUR_FILE && ui.reviewPanelOpen),\ + 'rp-state-current-file-mini': (reviewPanel.subView === SubViews.CUR_FILE && !ui.reviewPanelOpen),\ + 'rp-state-overview': (reviewPanel.subView === SubViews.OVERVIEW),\ + 'rp-size-mini': ui.miniReviewPanelVisible,\ + 'rp-size-expanded': ui.reviewPanelOpen,\ + 'rp-layout-left': reviewPanel.layoutToLeft,\ + 'rp-loading-threads': reviewPanel.loadingThreads,\ + }" + ) + .loading-panel( + ng-show="(!editor.sharejs_doc || editor.opening) && !editor.error_state", + style=showRichText ? "top: 32px" : "", + ) + span(ng-show="editor.open_doc_id") + i.fa.fa-spin.fa-refresh + |   #{translate("loading")}… + span(ng-show="!editor.open_doc_id") + i.fa.fa-arrow-left + |   #{translate("open_a_file_on_the_left")} + + if moduleIncludesAvailable('editor:main') + != moduleIncludes('editor:main', locals) + else + .toolbar.toolbar-editor + + .multi-selection-ongoing( + ng-show="multiSelectedCount > 0" + ) + .multi-selection-message + h4 {{ multiSelectedCount }} #{translate('files_selected')} + + include ./source-editor + + if !isRestrictedTokenMember + include ./review-panel diff --git a/services/web/app/views/project/editor/editor-with-symbol-palette.pug b/services/web/app/views/project/editor/editor-with-symbol-palette.pug new file mode 100644 index 0000000000..32fb59b0ac --- /dev/null +++ b/services/web/app/views/project/editor/editor-with-symbol-palette.pug @@ -0,0 +1,55 @@ +.ui-layout-center( + ng-controller="ReviewPanelController", + ng-class="{\ + 'rp-unsupported': editor.showRichText,\ + 'rp-state-current-file': (reviewPanel.subView === SubViews.CUR_FILE),\ + 'rp-state-current-file-expanded': (reviewPanel.subView === SubViews.CUR_FILE && ui.reviewPanelOpen),\ + 'rp-state-current-file-mini': (reviewPanel.subView === SubViews.CUR_FILE && !ui.reviewPanelOpen),\ + 'rp-state-overview': (reviewPanel.subView === SubViews.OVERVIEW),\ + 'rp-size-mini': ui.miniReviewPanelVisible,\ + 'rp-size-expanded': ui.reviewPanelOpen,\ + 'rp-layout-left': reviewPanel.layoutToLeft,\ + 'rp-loading-threads': reviewPanel.loadingThreads,\ + }" + ) + + .editor-container.full-size( + vertical-resizable-panes="symbol-palette-resizer" + vertical-resizable-panes-hidden-externally-on="symbol-palette-toggled" + vertical-resizable-panes-hidden-initially="true" + vertical-resizable-panes-default-size="196" + vertical-resizable-panes-min-size="144" + vertical-resizable-panes-max-size="336" + vertical-resizable-panes-resize-on="left-pane-resize-all" + ) + .div(vertical-resizable-top) + + .loading-panel( + ng-show="(!editor.sharejs_doc || editor.opening) && !editor.error_state", + style=showRichText ? "top: 32px" : "", + ) + span(ng-show="editor.open_doc_id") + i.fa.fa-spin.fa-refresh + |   #{translate("loading")}… + span(ng-show="!editor.open_doc_id") + i.fa.fa-arrow-left + |   #{translate("open_a_file_on_the_left")} + + if moduleIncludesAvailable('editor:main') + != moduleIncludes('editor:main', locals) + else + .toolbar.toolbar-editor + + .multi-selection-ongoing( + ng-show="multiSelectedCount > 0" + ) + .multi-selection-message + h4 {{ multiSelectedCount }} #{translate('files_selected')} + + include ./source-editor + + if !isRestrictedTokenMember + include ./review-panel + + .div(vertical-resizable-bottom) + include ./symbol-palette diff --git a/services/web/app/views/project/editor/editor.pug b/services/web/app/views/project/editor/editor.pug new file mode 100644 index 0000000000..a6a364fe2e --- /dev/null +++ b/services/web/app/views/project/editor/editor.pug @@ -0,0 +1,74 @@ +div.full-size( + ng-show="ui.view == 'editor'" + layout="pdf" + layout-disabled="ui.pdfLayout != 'sideBySide'" + mask-iframes-on-resize="true" + resize-on="layout:main:resize" + resize-proportionally="true" + initial-size-east="'50%'" + minimum-restore-size-east="300" + allow-overflow-on="'center'" + custom-toggler-pane=hasFeature('custom-togglers') ? "east" : false + custom-toggler-msg-when-open=hasFeature('custom-togglers') ? translate("tooltip_hide_pdf") : false + custom-toggler-msg-when-closed=hasFeature('custom-togglers') ? translate("tooltip_show_pdf") : false +) + if showSymbolPalette + include ./editor-with-symbol-palette + else + include ./editor-no-symbol-palette + + .ui-layout-east + div(ng-if="ui.pdfLayout == 'sideBySide'") + include ./pdf + + .ui-layout-resizer-controls.synctex-controls( + ng-show="!!pdf.url && settings.pdfViewer == 'pdfjs'" + ng-controller="PdfSynctexController" + ) + a.btn.btn-default.btn-xs.synctex-control.synctex-control-goto-pdf( + tooltip=translate('go_to_code_location_in_pdf') + tooltip-placement="right" + tooltip-append-to-body="true" + ng-click="syncToPdf()" + ng-disabled="syncToPdfInFlight" + ) + i.synctex-control-icon(ng-show="!syncToPdfInFlight") + i.synctex-spin-icon.fa.fa-refresh.fa-spin(ng-show="syncToPdfInFlight") + a.btn.btn-default.btn-xs.synctex-control.synctex-control-goto-code( + tooltip=translate('go_to_pdf_location_in_code') + tooltip-placement="right" + tooltip-append-to-body="true" + ng-click="syncToCode()" + ng-disabled="syncToCodeInFlight" + ) + i.synctex-control-icon(ng-show="!syncToCodeInFlight") + i.synctex-spin-icon.fa.fa-refresh.fa-spin(ng-show="syncToCodeInFlight") + +div.full-size( + ng-if="ui.pdfLayout == 'flat'" + ng-show="ui.view == 'pdf'" +) + include ./pdf + +// fallback, shown when no file/view is selected +div.full-size.no-file-selection( + ng-if="!ui.view" +) + .no-file-selection-message( + ng-if="rootFolder.children && rootFolder.children.length > 0" + ) + h3 + | #{translate('no_selection_select_file')} + .no-file-selection-message( + ng-if="rootFolder.children && rootFolder.children.length === 0" + ) + h3 + | #{translate('no_selection_create_new_file')} + div( + ng-controller="FileTreeController" + ) + button.btn.btn-primary( + ng-click="openNewDocModal()" + ) + | #{translate('new_file')} + diff --git a/services/web/app/views/project/editor/file-tree-history.pug b/services/web/app/views/project/editor/file-tree-history.pug new file mode 100644 index 0000000000..3d45489b2e --- /dev/null +++ b/services/web/app/views/project/editor/file-tree-history.pug @@ -0,0 +1,114 @@ +aside.editor-sidebar.full-size( + ng-controller="FileTreeController" + ng-class="{ 'multi-selected': multiSelectedCount > 0 }" + ng-show="ui.view == 'history' && !history.isV2" +) + .file-tree + .file-tree-inner( + ng-if="rootFolder", + ng-class="no-toolbar" + ) + ul.list-unstyled.file-tree-list + + file-entity( + entity="entity", + ng-repeat="entity in rootFolder.children | orderBy:[orderByFoldersFirst, 'name']" + ) + + li(ng-show="deletedDocs.length > 0 && ui.view == 'history'") + h3 #{translate("deleted_files")} + li( + ng-class="{ 'selected': entity.selected }", + ng-repeat="entity in deletedDocs | orderBy:'name'", + ng-controller="FileTreeEntityController", + ng-show="ui.view == 'history'" + ) + .entity + .entity-name( + ng-click="select($event)" + ) + //- Just a spacer to align with folders + i.fa.fa-fw.toggle + i.fa.fa-fw.fa-file + + span {{ entity.name }} + + +script(type='text/ng-template', id='entityListItemTemplate') + li( + ng-class="{ 'selected': entity.selected, 'multi-selected': entity.multiSelected }", + ng-controller="FileTreeEntityController" + ) + .entity(ng-if="entity.type != 'folder'") + .entity-name( + ng-click="select($event)" + context-menu + data-target="context-menu-{{ entity.id }}" + context-menu-container="body" + context-menu-disabled="true" + ) + //- Just a spacer to align with folders + i.fa.fa-fw.toggle(ng-if="entity.type != 'folder'") + + i.fa.fa-fw(ng-if="entity.type != 'folder'", ng-class="'fa-' + iconTypeFromName(entity.name)") + i.fa.fa-external-link-square.fa-rotate-180.linked-file-highlight( + ng-if="entity.linkedFileData.provider" + ) + span( + ng-hide="entity.renaming" + ) {{ entity.renamingToName || entity.name }} + + .entity(ng-if="entity.type == 'folder'", ng-controller="FileTreeFolderController") + .entity-name( + ng-click="select($event)" + ) + div( + context-menu + data-target="context-menu-{{ entity.id }}" + context-menu-container="body" + context-menu-disabled="true" + ) + i.fa.fa-fw.toggle( + ng-if="entity.type == 'folder'" + ng-class="{'fa-angle-right': !expanded, 'fa-angle-down': expanded}" + ng-click="toggleExpanded()" + ) + + i.fa.fa-fw( + ng-if="entity.type == 'folder'" + ng-class="{\ + 'fa-folder': !expanded, \ + 'fa-folder-open': expanded \ + }" + ng-click="select($event)" + ) + + span( + ng-hide="entity.renaming" + ) {{ entity.renamingToName || entity.name }} + + ul.list-unstyled( + ng-if="entity.type == 'folder' && (depth == null || depth < MAX_DEPTH)" + ng-show="expanded" + ) + file-entity( + entity="child", + ng-repeat="child in entity.children | orderBy:[orderByFoldersFirst, 'name']" + depth="(depth || 0) + 1" + ) + + .entity-limit-hit( + ng-if="depth === MAX_DEPTH" + ng-show="expanded" + ) + i.fa.fa-fw + span.entity-limit-hit-message + | Some files might be hidden + | + i.fa.fa-question-circle.entity-limit-hit-tooltip-trigger( + tooltip="Your project has hit Overleaf's maximum file depth limit. Files within this folder won't be visible." + tooltip-append-to-body="true" + aria-hidden="true" + ) + span.sr-only + | Your project has hit Overleaf's maximum file depth limit. Files within this folder won't be visible. diff --git a/services/web/app/views/project/editor/file-tree-react.pug b/services/web/app/views/project/editor/file-tree-react.pug new file mode 100644 index 0000000000..21372664a2 --- /dev/null +++ b/services/web/app/views/project/editor/file-tree-react.pug @@ -0,0 +1,41 @@ +aside.editor-sidebar.full-size( + ng-show="ui.view != 'history'" + vertical-resizable-panes="outline-resizer" + vertical-resizable-panes-toggled-externally-on="outline-toggled" + vertical-resizable-panes-min-size="32" + vertical-resizable-panes-max-size="75%" + vertical-resizable-panes-resize-on="left-pane-resize-all" +) + + .file-tree( + ng-controller="ReactFileTreeController" + vertical-resizable-top + ) + file-tree-root( + project-id="projectId" + root-folder="rootFolder" + root-doc-id="rootDocId" + has-write-permissions="hasWritePermissions" + on-select="onSelect" + on-init="onInit" + is-connected="isConnected" + user-has-feature="userHasFeature" + ref-providers="refProviders" + reindex-references="reindexReferences" + set-ref-provider-enabled="setRefProviderEnabled" + set-started-free-trial="setStartedFreeTrial" + ) + + .outline-container( + vertical-resizable-bottom + ng-controller="OutlineController" + ) + outline-pane( + is-tex-file="isTexFile" + outline="outline" + project-id="project_id" + jump-to-line="jumpToLine" + on-toggle="onToggle" + event-tracking="eventTracking" + highlighted-line="highlightedLine" + ) diff --git a/services/web/app/views/project/editor/file-view.pug b/services/web/app/views/project/editor/file-view.pug new file mode 100644 index 0000000000..d16be07b50 --- /dev/null +++ b/services/web/app/views/project/editor/file-view.pug @@ -0,0 +1,9 @@ +div( + ng-controller="FileViewController" + ng-show="ui.view == 'file'" + ng-if="openFile" +) + file-view( + file='file' + store-references-keys='storeReferencesKeys' + ) diff --git a/services/web/app/views/project/editor/header-react.pug b/services/web/app/views/project/editor/header-react.pug new file mode 100644 index 0000000000..1382120b56 --- /dev/null +++ b/services/web/app/views/project/editor/header-react.pug @@ -0,0 +1,13 @@ +div(ng-controller="ReactShareProjectModalController") + share-project-modal( + handle-hide="handleHide" + show="show" + is-admin="isAdmin" + ) + + div(ng-controller="EditorNavigationToolbarController") + editor-navigation-toolbar-root( + open-doc="openDoc" + online-users-array="onlineUsersArray" + open-share-project-modal="openShareProjectModal" + ) diff --git a/services/web/app/views/project/editor/header.pug b/services/web/app/views/project/editor/header.pug new file mode 100644 index 0000000000..e6951a16e0 --- /dev/null +++ b/services/web/app/views/project/editor/header.pug @@ -0,0 +1,155 @@ +header.toolbar.toolbar-header.toolbar-with-labels( + ng-cloak, + ng-hide="state.loading" +) + .toolbar-left + a.btn.btn-full-height( + href, + ng-click="ui.leftMenuShown = true;", + ) + i.fa.fa-fw.fa-bars.editor-menu-icon + p.toolbar-label #{translate("menu")} + a.btn.btn-full-height.header-cobranding-logo-container( + ng-if="::(cobranding.isProjectCobranded && cobranding.logoImgUrl)" + ng-href="{{ ::cobranding.brandVariationHomeUrl }}" + target="_blank" + rel="noreferrer noopener" + ) + img.header-cobranding-logo( + ng-src="{{ ::cobranding.logoImgUrl }}" + alt="{{ ::cobranding.brandVariationName }}" + ) + + a.toolbar-header-back-projects( + href="/project" + ) + i.fa.fa-fw.fa-level-up + + span(ng-controller="PdfViewToggleController") + a.btn.btn-full-height.btn-full-height-no-border( + href, + ng-show="ui.pdfLayout == 'flat'", + tooltip="PDF", + tooltip-placement="bottom", + tooltip-append-to-body="true", + ng-click="togglePdfView()", + ng-class="{ 'active': ui.view == 'pdf' }" + ) + i.fa.fa-file-pdf-o + + .toolbar-center.project-name(ng-controller="ProjectNameController") + span.name( + ng-dblclick="!permissions.admin || startRenaming()", + ng-show="!state.renaming" + tooltip="{{ project.name }}", + tooltip-class="project-name-tooltip" + tooltip-placement="bottom", + tooltip-append-to-body="true", + tooltip-enable="state.overflowed" + ) {{ project.name }} + + input.form-control( + type="text" + ng-model="inputs.name", + ng-show="state.renaming", + on-enter="finishRenaming()", + ng-blur="finishRenaming()", + select-name-when="state.renaming" + ) + + a.rename( + ng-if="permissions.admin", + href='#', + tooltip-placement="bottom", + tooltip=translate('rename'), + tooltip-append-to-body="true", + ng-click="startRenaming()", + ng-show="!state.renaming" + ) + i.fa.fa-pencil + + .toolbar-right + .online-users( + ng-if="onlineUsersArray.length < 4" + ng-controller="OnlineUsersController" + ) + span.online-user( + ng-repeat="user in onlineUsersArray", + ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }", + popover="{{ user.name }}" + popover-placement="bottom" + popover-append-to-body="true" + popover-trigger="mouseenter" + ng-click="gotoUser(user)" + ) {{ userInitial(user) }} + + .online-users.dropdown( + dropdown + ng-if="onlineUsersArray.length >= 4" + ng-controller="OnlineUsersController" + ) + span.online-user.online-user-multi( + dropdown-toggle, + tooltip=translate('connected_users'), + tooltip-placement="left" + ) + strong {{ onlineUsersArray.length }} + i.fa.fa-fw.fa-users + ul.dropdown-menu.pull-right + li.dropdown-header #{translate('connected_users')} + li(ng-repeat="user in onlineUsersArray") + a(href, ng-click="gotoUser(user)") + span.online-user( + ng-style="{ 'background-color': 'hsl({{ getHueForUserId(user.user_id) }}, 70%, 50%)' }" + ) {{ user.name.slice(0,1) }} + | {{ user.name }} + + if !isRestrictedTokenMember + a.btn.btn-full-height( + href, + ng-if="project.features.trackChangesVisible", + ng-class="{ active: ui.reviewPanelOpen && ui.view !== 'history' }" + ng-disabled="ui.view === 'history'" + ng-click="toggleReviewPanel()" + ) + i.review-icon + p.toolbar-label + | #{translate("review")} + + a.btn.btn-full-height( + href + ng-click="openShareProjectModal(permissions.admin);" + ng-controller="ReactShareProjectModalController" + ) + i.fa.fa-fw.fa-group + p.toolbar-label #{translate("share")} + + share-project-modal( + handle-hide="handleHide" + show="show" + is-admin="isAdmin" + ) + != moduleIncludes('publish:button', locals) + + if !isRestrictedTokenMember + a.btn.btn-full-height( + href, + ng-click="toggleHistory();", + ng-class="{ active: (ui.view == 'history') }", + ) + i.fa.fa-fw.fa-history + p.toolbar-label #{translate("history")} + a.btn.btn-full-height( + href, + ng-class="{ active: ui.chatOpen }", + ng-click="toggleChat();", + ng-controller="ChatButtonController", + ng-show="!anonymous", + ) + i.fa.fa-fw.fa-comment( + ng-class="{ 'bounce': unreadMessages > 0 }" + ) + span.label.label-info( + ng-show="unreadMessages > 0" + ) {{ unreadMessages }} + p.toolbar-label #{translate("chat")} diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug new file mode 100644 index 0000000000..c72c47135d --- /dev/null +++ b/services/web/app/views/project/editor/history.pug @@ -0,0 +1,89 @@ +div#history(ng-show="ui.view == 'history' && history.updates.length > 0") + include ./history/entriesListV1 + include ./history/entriesListV2 + + include ./history/diffPanelV1 + include ./history/previewPanelV2 + +.full-size(ng-if="ui.view == 'history' && history.updates.length === 0 && !isHistoryLoading()") + .no-history-available + h3 + | #{translate('no_history_available')} +script(type="text/ng-template", id="historyRestoreDiffModalTemplate") + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="cancel()" + aria-label="Close" + ) + span(aria-hidden="true") × + h3 #{translate("restore")} {{diff.doc.name}} + .modal-body.modal-body-share + p !{translate("sure_you_want_to_restore_before", {filename: "{{diff.doc.name}}", date:"{{diff.start_ts | formatDate}}"}, ['strong'])} + .modal-footer + button.btn.btn-default( + ng-click="cancel()", + ng-disabled="state.inflight" + ) #{translate("cancel")} + button.btn.btn-danger( + ng-click="restore()", + ng-disabled="state.inflight" + ) + span(ng-show="!state.inflight") #{translate("restore")} + span(ng-show="state.inflight") #{translate("restoring")} … + + +script(type="text/ng-template", id="historyLabelTpl") + .history-label( + ng-class="{\ + 'history-label-own' : $ctrl.isOwnedByCurrentUser,\ + 'history-label-pseudo-current-state': $ctrl.isPseudoCurrentStateLabel,\ + }" + ) + span.history-label-comment( + tooltip-append-to-body="true" + tooltip-template="'historyLabelTooltipTpl'" + tooltip-placement="left" + tooltip-enable="$ctrl.showTooltip" + ) + i.fa.fa-tag + |  {{ ::$ctrl.isPseudoCurrentStateLabel ? '#{translate("history_label_project_current_state")}' : $ctrl.labelText }} + button.history-label-delete-btn( + ng-if="$ctrl.isOwnedByCurrentUser && !$ctrl.isPseudoCurrentStateLabel" + stop-propagation="click" + ng-click="$ctrl.onLabelDelete()" + aria-label=translate("delete") + ) + span(aria-hidden="true") × + +script(type="text/ng-template", id="historyLabelTooltipTpl") + .history-label-tooltip + p.history-label-tooltip-title + i.fa.fa-tag + |  {{ $ctrl.labelText }} + p.history-label-tooltip-owner #{translate("history_label_created_by")} {{ $ctrl.labelOwnerName }} + time.history-label-tooltip-datetime {{ $ctrl.labelCreationDateTime | formatDate }} + + +script(type="text/ng-template", id="historyV2DeleteLabelModalTemplate") + .modal-header + h3 #{translate("history_delete_label")} + .modal-body + .alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} + .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} + p(ng-if="labelDetails") + | #{translate("history_are_you_sure_delete_label")} + strong "{{ labelDetails.comment }}" + | ? + .modal-footer + button.btn.btn-default( + type="button" + ng-disabled="state.inflight" + ng-click="$dismiss()" + ) #{translate("cancel")} + button.btn.btn-primary( + type="button" + ng-click="deleteLabel()" + ng-disabled="state.inflight" + ) {{ state.inflight ? '#{translate("history_deleting_label")}' : '#{translate("history_delete_label")}' }} diff --git a/services/web/app/views/project/editor/history/diffPanelV1.pug b/services/web/app/views/project/editor/history/diffPanelV1.pug new file mode 100644 index 0000000000..14af4b2f47 --- /dev/null +++ b/services/web/app/views/project/editor/history/diffPanelV1.pug @@ -0,0 +1,58 @@ +.diff-panel.full-size(ng-if="!history.isV2", ng-controller="HistoryDiffController") + .diff( + ng-if="!!history.diff && !history.diff.loading && !history.diff.deleted && !history.diff.error && !history.diff.binary" + ) + .toolbar.toolbar-alt + span.name + | {{history.diff.highlights.length}} + ng-pluralize( + count="history.diff.highlights.length", + when="{\ + 'one': 'change',\ + 'other': 'changes'\ + }" + ) + | in {{history.diff.pathname}} + .toolbar-right(ng-if="permissions.write") + a.btn.btn-danger.btn-xs( + href, + ng-click="openRestoreDiffModal()" + ) #{translate("restore_to_before_these_changes")} + .diff-editor.hide-ace-cursor( + ace-editor="history", + theme="settings.editorTheme", + font-size="settings.fontSize", + text="history.diff.text", + highlights="history.diff.highlights", + read-only="true", + resize-on="layout:main:resize", + navigate-highlights="true" + ) + + .diff-deleted.text-centered( + ng-show="history.diff.deleted && !history.diff.restoreDeletedSuccess" + ) + p.text-serif #{translate("file_has_been_deleted", {filename:"{{ history.diff.doc.name }} "})} + p + a.btn.btn-primary.btn-lg( + href, + ng-click="restoreDeletedDoc()", + ng-disabled="history.diff.restoreInProgress" + ) #{translate("restore")} + + .diff-deleted.text-centered( + ng-show="history.diff.deleted && history.diff.restoreDeletedSuccess" + ) + p.text-serif #{translate("file_restored", {filename:"{{ history.diff.doc.name }} "})} + p.text-serif #{translate("file_restored_back_to_editor")} + p + a.btn.btn-default( + href, + ng-click="backToEditorAfterRestore()", + ) #{translate("file_restored_back_to_editor_btn")} + + .loading-panel(ng-show="history.diff.loading") + i.fa.fa-spin.fa-refresh + |   #{translate("loading")}… + .error-panel(ng-show="history.diff.error") + .alert.alert-danger #{translate("generic_something_went_wrong")} diff --git a/services/web/app/views/project/editor/history/entriesListV1.pug b/services/web/app/views/project/editor/history/entriesListV1.pug new file mode 100644 index 0000000000..3071de0bcb --- /dev/null +++ b/services/web/app/views/project/editor/history/entriesListV1.pug @@ -0,0 +1,82 @@ +aside.change-list( + ng-if="!history.isV2" + ng-controller="HistoryListController" + infinite-scroll="loadMore()" + infinite-scroll-disabled="history.loading || history.atEnd" + infinite-scroll-initialize="ui.view == 'history'" + ) + .infinite-scroll-inner + ul.list-unstyled( + ng-class="{\ + 'hover-state': history.hoveringOverListSelectors\ + }" + ) + li.change( + ng-repeat="update in history.updates" + ng-class="{\ + 'first-in-day': update.meta.first_in_day,\ + 'selected': update.inSelection,\ + 'selected-to': update.selectedTo,\ + 'selected-from': update.selectedFrom,\ + 'hover-selected': update.inHoverSelection,\ + 'hover-selected-to': update.hoverSelectedTo,\ + 'hover-selected-from': update.hoverSelectedFrom,\ + }" + ng-controller="HistoryListItemController" + ) + + div.day(ng-show="update.meta.first_in_day") {{ update.meta.end_ts | relativeDate }} + + div.selectors + div.range + form + input.selector-from( + type="radio" + name="fromVersion" + ng-model="update.selectedFrom" + ng-value="true" + ng-mouseover="mouseOverSelectedFrom()" + ng-mouseout="mouseOutSelectedFrom()" + ng-show="update.afterSelection || update.inSelection" + ) + form + input.selector-to( + type="radio" + name="toVersion" + ng-model="update.selectedTo" + ng-value="true" + ng-mouseover="mouseOverSelectedTo()" + ng-mouseout="mouseOutSelectedTo()" + ng-show="update.beforeSelection || update.inSelection" + ) + + div.description(ng-click="select()") + div.time {{ update.meta.end_ts | formatDate:'h:mm a' }} + div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0") + | #{translate("file_action_edited")} + div.docs(ng-repeat="pathname in update.pathnames") + .doc {{ pathname }} + div.docs(ng-repeat="project_op in update.project_ops") + div(ng-if="project_op.rename") + .action #{translate("file_action_renamed")} + .doc {{ project_op.rename.pathname }} → {{ project_op.rename.newPathname }} + div(ng-if="project_op.add") + .action #{translate("file_action_created")} + .doc {{ project_op.add.pathname }} + div(ng-if="project_op.remove") + .action #{translate("file_action_deleted")} + .doc {{ project_op.remove.pathname }} + div.users + div.user(ng-repeat="update_user in update.meta.users") + .color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}") + .color-square(ng-if="update_user == null", ng-style="{'background-color': 'hsl(100, 70%, 50%)'}") + .name(ng-if="update_user && update_user.id != user.id" ng-bind="displayName(update_user)") + .name(ng-if="update_user && update_user.id == user.id") You + .name(ng-if="update_user == null") #{translate("anonymous")} + div.user(ng-if="update.meta.users.length == 0") + .color-square(style="background-color: hsl(100, 100%, 50%)") + span #{translate("anonymous")} + + .loading(ng-show="history.loading") + i.fa.fa-spin.fa-refresh + |    #{translate("loading")}… diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug new file mode 100644 index 0000000000..300bfde368 --- /dev/null +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -0,0 +1,252 @@ +aside.change-list( + ng-if="history.isV2" + ng-controller="HistoryV2ListController" +) + history-entries-list( + ng-if="!history.showOnlyLabels && !history.error" + entries="history.updates" + range-selection-enabled="history.viewMode === HistoryViewModes.COMPARE" + selected-history-version="history.selection.range.toV" + selected-history-range="history.selection.range" + current-user="user" + current-user-is-owner="project.owner._id === user.id" + users="projectUsers" + load-entries="loadMore()" + load-disabled="history.loading || history.atEnd" + load-initialize="ui.view == 'history'" + is-loading="history.loading" + free-history-limit-hit="history.freeHistoryLimitHit" + on-version-select="handleVersionSelect(version)" + on-range-select="handleRangeSelect(selectedToV, selectedFromV)" + on-label-delete="handleLabelDelete(label)" + ) + history-labels-list( + ng-if="history.showOnlyLabels && !history.error" + labels="history.labels" + range-selection-enabled="history.viewMode === HistoryViewModes.COMPARE" + selected-history-version="history.selection.range.toV" + selected-history-range="history.selection.range" + current-user="user" + users="projectUsers" + is-loading="history.loading" + on-version-select="handleVersionSelect(version)" + on-range-select="handleRangeSelect(selectedToV, selectedFromV)" + on-label-delete="handleLabelDelete(label)" + ) + +script(type="text/ng-template", id="historyEntriesListTpl") + .history-entries( + infinite-scroll="$ctrl.loadEntries()" + infinite-scroll-disabled="$ctrl.loadDisabled" + infinite-scroll-initialize="$ctrl.loadInitialize" + ) + .infinite-scroll-inner + history-entry( + ng-repeat="entry in $ctrl.entries" + range-selection-enabled="$ctrl.rangeSelectionEnabled" + is-dragging="$ctrl.isDragging" + selected-history-version="$ctrl.selectedHistoryVersion" + selected-history-range="$ctrl.selectedHistoryRange" + hovered-history-range="$ctrl.hoveredHistoryRange" + entry="entry" + current-user="$ctrl.currentUser" + users="$ctrl.users" + on-select="$ctrl.handleEntrySelect(selectedEntry)" + on-label-delete="$ctrl.onLabelDelete({ label: label })" + ) + .loading(ng-show="$ctrl.isLoading") + i.fa.fa-spin.fa-refresh + |    #{translate("loading")}… + .history-entries-list-upgrade-prompt( + ng-if="$ctrl.freeHistoryLimitHit && $ctrl.currentUserIsOwner" + ng-controller="FreeTrialModalController" + ) + p #{translate("currently_seeing_only_24_hrs_history")} + p: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})} + ul.list-unstyled + li + i.fa.fa-check   + | #{translate("unlimited_projects")} + + li + i.fa.fa-check   + | #{translate("collabs_per_proj", {collabcount:'Multiple'})} + + li + i.fa.fa-check   + | #{translate("full_doc_history")} + + li + i.fa.fa-check   + | #{translate("sync_to_dropbox")} + + li + i.fa.fa-check   + | #{translate("sync_to_github")} + + li + i.fa.fa-check   + |#{translate("compile_larger_projects")} + p.text-center + a.btn.btn-success( + href + ng-class="buttonClass" + ng-click="startFreeTrial('history')" + ) #{translate("start_free_trial")} + p.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")} + .history-entries-list-upgrade-prompt( + ng-if="$ctrl.freeHistoryLimitHit && !$ctrl.currentUserIsOwner" + ) + p #{translate("currently_seeing_only_24_hrs_history")} + strong #{translate("ask_proj_owner_to_upgrade_for_full_history")} + +script(type="text/ng-template", id="historyEntryTpl") + time.history-entry-day(ng-if="::$ctrl.entry.meta.first_in_day") {{ ::$ctrl.entry.meta.end_ts | relativeDate }} + + .history-entry( + ng-class="{\ + 'history-entry-first-in-day': $ctrl.entry.meta.first_in_day,\ + 'history-entry-selected': !$ctrl.isDragging && $ctrl.isEntrySelected(),\ + 'history-entry-selected-to': $ctrl.rangeSelectionEnabled && !$ctrl.isDragging && $ctrl.selectedHistoryRange.toV === $ctrl.entry.toV,\ + 'history-entry-selected-from': $ctrl.rangeSelectionEnabled && !$ctrl.isDragging && $ctrl.selectedHistoryRange.fromV === $ctrl.entry.fromV,\ + 'history-entry-hover-selected': $ctrl.rangeSelectionEnabled && $ctrl.isDragging && $ctrl.isEntryHoverSelected(),\ + 'history-entry-hover-selected-to': $ctrl.rangeSelectionEnabled && $ctrl.isDragging && $ctrl.hoveredHistoryRange.toV === $ctrl.entry.toV,\ + 'history-entry-hover-selected-from': $ctrl.rangeSelectionEnabled && $ctrl.isDragging && $ctrl.hoveredHistoryRange.fromV === $ctrl.entry.fromV,\ + }" + history-droppable-area + history-droppable-area-on-drop="$ctrl.onDrop(boundary)" + history-droppable-area-on-over="$ctrl.onOver(boundary)" + ) + .history-entry-details( + ng-click="$ctrl.onSelect({ selectedEntry: $ctrl.entry })" + ) + .history-entry-toV-handle( + ng-show="$ctrl.rangeSelectionEnabled && $ctrl.selectedHistoryRange && ((!$ctrl.isDragging && $ctrl.selectedHistoryRange.toV === $ctrl.entry.toV) || ($ctrl.isDragging && $ctrl.hoveredHistoryRange.toV === $ctrl.entry.toV))" + history-draggable-boundary="toV" + history-draggable-boundary-on-drag-start="$ctrl.onDraggingStart()" + history-draggable-boundary-on-drag-stop="$ctrl.onDraggingStop(isValidDrop, boundary)" + ) + + history-label( + ng-repeat="label in $ctrl.entry.labels | orderBy : '-created_at'" + ng-init="user = $ctrl.buildUserView(label)" + label-text="label.comment" + label-owner-name="$ctrl.displayNameById(label.user_id) || 'Anonymous'" + label-creation-date-time="label.created_at" + is-owned-by-current-user="label.user_id === $ctrl.currentUser.id" + on-label-delete="$ctrl.onLabelDelete({ label: label })" + ) + + ol.history-entry-changes + li.history-entry-change( + ng-repeat="pathname in ::$ctrl.entry.pathnames" + ) + span.history-entry-change-action #{translate("file_action_edited")} + span.history-entry-change-doc {{ ::pathname }} + li.history-entry-change( + ng-repeat="project_op in ::$ctrl.entry.project_ops" + ) + span.history-entry-change-action( + ng-if="::project_op.rename" + ) #{translate("file_action_renamed")} + span.history-entry-change-action( + ng-if="::project_op.add" + ) #{translate("file_action_created")} + span.history-entry-change-action( + ng-if="::project_op.remove" + ) #{translate("file_action_deleted")} + span.history-entry-change-doc {{ ::$ctrl.getProjectOpDoc(project_op) }} + .history-entry-metadata + time.history-entry-metadata-time {{ ::$ctrl.entry.meta.end_ts | formatDate:'h:mm a' }} + span + | + | • + | + ol.history-entry-metadata-users + li.history-entry-metadata-user(ng-repeat="update_user in ::$ctrl.entry.meta.users") + span.name( + ng-if="::update_user && update_user.id != $ctrl.currentUser.id" + ng-style="$ctrl.getUserCSSStyle(update_user);" + ) {{ ::$ctrl.displayName(update_user) }} + span.name( + ng-if="::update_user && update_user.id == $ctrl.currentUser.id" + ng-style="$ctrl.getUserCSSStyle(update_user);" + ) You + span.name( + ng-if="::update_user == null" + ng-style="$ctrl.getUserCSSStyle(update_user);" + ) #{translate("anonymous")} + li.history-entry-metadata-user(ng-if="::$ctrl.entry.meta.users.length == 0") + span.name( + ng-style="$ctrl.getUserCSSStyle();" + ) #{translate("anonymous")} + + .history-entry-fromV-handle( + ng-show="$ctrl.rangeSelectionEnabled && $ctrl.selectedHistoryRange && ((!$ctrl.isDragging && $ctrl.selectedHistoryRange.fromV === $ctrl.entry.fromV) || ($ctrl.isDragging && $ctrl.hoveredHistoryRange.fromV === $ctrl.entry.fromV))" + history-draggable-boundary="fromV" + history-draggable-boundary-on-drag-start="$ctrl.onDraggingStart()" + history-draggable-boundary-on-drag-stop="$ctrl.onDraggingStop(isValidDrop, boundary)" + ) + +script(type="text/ng-template", id="historyLabelsListTpl") + .history-labels-list + .history-version-with-label( + ng-repeat="versionWithLabel in $ctrl.versionsWithLabels | orderBy:'-version' track by versionWithLabel.version" + ng-class="{\ + 'history-version-with-label-selected': !$ctrl.isDragging && $ctrl.isVersionSelected(versionWithLabel.version),\ + 'history-version-with-label-selected-to': !$ctrl.isDragging && $ctrl.selectedHistoryRange.toV === versionWithLabel.version,\ + 'history-version-with-label-selected-from': !$ctrl.isDragging && $ctrl.selectedHistoryRange.fromV === versionWithLabel.version,\ + 'history-version-with-label-hover-selected': $ctrl.isDragging && $ctrl.isVersionHoverSelected(versionWithLabel.version),\ + 'history-version-with-label-hover-selected-to': $ctrl.isDragging && $ctrl.hoveredHistoryRange.toV === versionWithLabel.version,\ + 'history-version-with-label-hover-selected-from': $ctrl.isDragging && $ctrl.hoveredHistoryRange.fromV === versionWithLabel.version,\ + }" + ng-click="$ctrl.handleVersionSelect(versionWithLabel)" + history-droppable-area + history-droppable-area-on-drop="$ctrl.onDrop(boundary, versionWithLabel)" + history-droppable-area-on-over="$ctrl.onOver(boundary, versionWithLabel)" + ) + .history-entry-toV-handle( + ng-show="$ctrl.rangeSelectionEnabled && $ctrl.selectedHistoryRange && ((!$ctrl.isDragging && $ctrl.selectedHistoryRange.toV === versionWithLabel.version) || ($ctrl.isDragging && $ctrl.hoveredHistoryRange.toV === versionWithLabel.version))" + history-draggable-boundary="toV" + history-draggable-boundary-on-drag-start="$ctrl.onDraggingStart()" + history-draggable-boundary-on-drag-stop="$ctrl.onDraggingStop(isValidDrop, boundary)" + ) + div( + ng-repeat="label in versionWithLabel.labels track by label.id" + ) + history-label( + show-tooltip="false" + label-text="label.comment" + is-owned-by-current-user="label.user_id === $ctrl.currentUser.id" + on-label-delete="$ctrl.onLabelDelete({ label: label })" + is-pseudo-current-state-label="label.isPseudoCurrentStateLabel" + ) + .history-entry-label-metadata + .history-entry-label-metadata-user( + ng-if="!label.isPseudoCurrentStateLabel" + ng-init="user = $ctrl.buildUserView(label)" + ) + | Saved by + span.name( + ng-if="user && user._id !== $ctrl.currentUser.id" + ng-style="$ctrl.getUserCSSStyle(user, versionWithLabel);" + ) {{ ::user.displayName }} + span.name( + ng-if="user && user._id == $ctrl.currentUser.id" + ng-style="$ctrl.getUserCSSStyle(user, versionWithLabel);" + ) You + span.name( + ng-if="user == null" + ng-style="$ctrl.getUserCSSStyle(user, versionWithLabel);" + ) #{translate("anonymous")} + time.history-entry-label-metadata-time {{ ::label.created_at | formatDate }} + .history-entry-fromV-handle( + ng-show="$ctrl.rangeSelectionEnabled && $ctrl.selectedHistoryRange && ((!$ctrl.isDragging && $ctrl.selectedHistoryRange.fromV === versionWithLabel.version) || ($ctrl.isDragging && $ctrl.hoveredHistoryRange.fromV === versionWithLabel.version))" + history-draggable-boundary="fromV" + history-draggable-boundary-on-drag-start="$ctrl.onDraggingStart()" + history-draggable-boundary-on-drag-stop="$ctrl.onDraggingStop(isValidDrop, boundary)" + ) + + .loading(ng-show="$ctrl.isLoading") + i.fa.fa-spin.fa-refresh + |    #{translate("loading")}… diff --git a/services/web/app/views/project/editor/history/fileTreeV2.pug b/services/web/app/views/project/editor/history/fileTreeV2.pug new file mode 100644 index 0000000000..f9eef7cf0d --- /dev/null +++ b/services/web/app/views/project/editor/history/fileTreeV2.pug @@ -0,0 +1,57 @@ +aside.editor-sidebar.full-size( + ng-controller="HistoryV2FileTreeController" + ng-if="ui.view == 'history' && history.isV2" +) + .history-file-tree-inner + history-file-tree( + files="history.selection.files" + selected-pathname="history.selection.pathname" + on-selected-file-change="handleFileSelection(file)" + is-loading="history.loadingFileTree" + ) + +script(type="text/ng-template", id="historyFileTreeTpl") + .history-file-tree + history-file-entity( + ng-repeat="fileEntity in $ctrl._fileTree | orderBy : [ '-type', 'operation', 'name' ]" + file-entity="fileEntity" + ng-show="!$ctrl.isLoading" + ) + +script(type="text/ng-template", id="historyFileEntityTpl") + .history-file-entity-wrapper + a.history-file-entity-link( + href + ng-click="$ctrl.isSelected ? '' : $ctrl.handleClick()" + ng-class="{ 'history-file-entity-link-selected': $ctrl.isSelected }" + ) + span.history-file-entity-name-container + i.history-file-entity-icon.history-file-entity-icon-folder-state.fa.fa-fw( + ng-class="{\ + 'fa-chevron-down': ($ctrl.fileEntity.type === 'folder' && $ctrl.isOpen),\ + 'fa-chevron-right': ($ctrl.fileEntity.type === 'folder' && !$ctrl.isOpen)\ + }" + ) + i.history-file-entity-icon.fa( + ng-class="::$ctrl.entityTypeIconClass" + ) + span.history-file-entity-name( + ng-class="::$ctrl.entityOpTextClass" + ) {{ ::$ctrl.fileEntity.name }} + span.history-file-entity-operation-badge( + ng-if="::$ctrl.hasOperation && $ctrl.fileEntity.operation !== 'renamed' && $ctrl.fileEntity.operation !== 'removed'" + ) {{ ::$ctrl.getFileOperationName() }} + span.history-file-entity-operation-badge( + ng-if="::$ctrl.hasOperation && $ctrl.fileEntity.operation === 'renamed'" + tooltip-append-to-body="true" + tooltip-placement="right" + tooltip-class="tooltip-history-file-tree" + tooltip-html=`::$ctrl.getRenameTooltip()` + ) {{ ::$ctrl.getFileOperationName() }} + div( + ng-show="$ctrl.isOpen" + ) + history-file-entity( + ng-repeat="childEntity in $ctrl.fileEntity.children" + file-entity="childEntity" + ) diff --git a/services/web/app/views/project/editor/history/previewPanelV2.pug b/services/web/app/views/project/editor/history/previewPanelV2.pug new file mode 100644 index 0000000000..e8bc1213fa --- /dev/null +++ b/services/web/app/views/project/editor/history/previewPanelV2.pug @@ -0,0 +1,62 @@ +.diff-panel.full-size( + ng-if="history.isV2 && history.viewMode === HistoryViewModes.COMPARE && history.updates.length !== 0" +) + .diff( + ng-show="!!history.selection.diff && !isHistoryLoading() && !history.selection.diff.error", + ng-class="{ 'diff-binary': history.selection.diff.binary }" + ) + .diff-editor-v2.hide-ace-cursor( + ng-if="!history.selection.diff.binary" + ace-editor="history", + theme="settings.editorTheme", + font-size="settings.fontSize", + text="history.selection.diff.text", + highlights="history.selection.diff.highlights", + read-only="true", + resize-on="layout:main:resize,history:toggle", + navigate-highlights="true" + ) + .alert.alert-info(ng-if="history.selection.diff.binary") + | We're still working on showing image and binary changes, sorry. Stay tuned! + + .loading-panel(ng-show="isHistoryLoading()") + i.fa.fa-spin.fa-refresh + |   #{translate("loading")}… + .error-panel(ng-show="history.selection.diff.error && !isHistoryLoading()") + .alert.alert-danger #{translate("generic_something_went_wrong")} + +.point-in-time-panel.full-size( + ng-if="history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME && history.updates.length !== 0" +) + .point-in-time-editor-container( + ng-if="!!history.selection.file && !history.selection.file.loading && !history.selection.file.error" + ) + .hide-ace-cursor( + ng-if="!history.selection.file.binary" + ace-editor="history-pointintime", + theme="settings.editorTheme", + font-size="settings.fontSize", + text="history.selection.file.text", + read-only="true", + resize-on="layout:main:resize,history:toggle", + ) + .alert.alert-info(ng-if="history.selection.file.binary") + | We're still working on showing image and binary changes, sorry. Stay tuned! + .loading-panel(ng-show="isHistoryLoading()") + i.fa.fa-spin.fa-refresh + |   #{translate("loading")}… + .error-panel(ng-show="history.error") + .alert.alert-danger + p + | #{translate("generic_history_error")} + a( + ng-href="mailto:#{settings.adminEmail}?Subject=Error%20loading%20history%20for%project%20{{ project_id }}", + ng-non-bindable + ) #{settings.adminEmail} + p.clearfix + a.alert-link-as-btn.pull-right( + href + ng-click="toggleHistory()" + ) #{translate("back_to_editor")} + .error-panel(ng-show="history.selection.file.error") + .alert.alert-danger #{translate("generic_something_went_wrong")} diff --git a/services/web/app/views/project/editor/history/toolbarV2.pug b/services/web/app/views/project/editor/history/toolbarV2.pug new file mode 100644 index 0000000000..1e42c08f21 --- /dev/null +++ b/services/web/app/views/project/editor/history/toolbarV2.pug @@ -0,0 +1,135 @@ +.history-toolbar( + ng-controller="HistoryV2ToolbarController" + ng-if="ui.view == 'history' && history.isV2" +) + span.history-toolbar-selected-version(ng-show="history.loadingFileTree") + i.fa.fa-spin.fa-refresh + |    #{translate("loading")}… + + //- point-in-time mode info + span.history-toolbar-selected-version( + ng-show="!history.loadingFileTree && history.viewMode === HistoryViewModes.POINT_IN_TIME && !history.showOnlyLabels && currentUpdate && !history.error" + ) #{translate("browsing_project_as_of")}  + time.history-toolbar-time {{ currentUpdate.meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }} + span.history-toolbar-selected-version( + ng-show="!history.loadingFileTree && history.viewMode === HistoryViewModes.POINT_IN_TIME && history.showOnlyLabels && currentUpdate && !history.error" + ) + span(ng-if="currentUpdate.labels.length > 0") + | #{translate("browsing_project_labelled")}  + span.history-toolbar-selected-label( + ng-repeat="label in currentUpdate.labels" + ) + | {{ label.comment }} + span(ng-if="!$last") , + span.history-toolbar-selected-label(ng-if="currentUpdate.labels.length === 0 && history.labels[0].isPseudoCurrentStateLabel && currentUpdate.toV === history.labels[0].version") + | #{translate("browsing_project_latest_for_pseudo_label")} + + + + //- compare mode info + span.history-toolbar-selected-version(ng-if="history.viewMode === HistoryViewModes.COMPARE && history.selection.diff && !history.selection.diff.binary && !history.selection.diff.loading && !history.selection.diff.error && !history.loadingFileTree") + | {{history.selection.diff.highlights.length}} + ng-pluralize( + count="history.selection.diff.highlights.length", + when="{\ + 'one': 'change',\ + 'other': 'changes'\ + }" + ) + | in {{history.selection.diff.pathname}} + + //- point-in-time mode actions + div.history-toolbar-actions( + ng-if="history.viewMode === HistoryViewModes.POINT_IN_TIME && !history.error && history.updates.length > 0" + ) + button.history-toolbar-btn( + ng-click="showAddLabelDialog();" + ng-if="!history.showOnlyLabels && permissions.write" + ng-disabled="isHistoryLoading() || history.selection.range.toV == null || history.selection.range.fromV == null" + ) + i.fa.fa-tag + |  #{translate("history_label_this_version")} + button.history-toolbar-btn( + ng-click="toggleHistoryViewMode();" + ng-disabled="isHistoryLoading()" + ) + i.fa.fa-exchange + |  #{translate("compare_to_another_version")} + + a.history-toolbar-btn-danger.pull-right( + ng-hide="history.loadingFileTree || history.selection.range.toV == null" + ng-href="/project/{{ project_id }}/version/{{ history.selection.range.toV }}/zip" + target="_blank" + ) + i.fa.fa-download + |  #{translate("download_project_at_this_version")} + + + + //- compare mode actions + div.history-toolbar-actions( + ng-if="history.viewMode === HistoryViewModes.COMPARE && !history.error && history.updates.length > 0" + ) + button.history-toolbar-btn( + ng-click="toggleHistoryViewMode();" + ng-disabled="isHistoryLoading()" + ) + i.fa + | #{translate("view_single_version")} + button.history-toolbar-btn-danger.pull-right( + ng-if="history.selection.file.deletedAtV" + ng-click="restoreDeletedFile()" + ng-show="!restoreState.error" + ng-disabled="restoreState.inflight" + ) + i.fa.fa-fw.fa-step-backward + span(ng-show="!restoreState.inflight") + | Restore this deleted file + span(ng-show="restoreState.inflight") + | Restoring… + span.text-danger(ng-show="restoreState.error") + | Error restoring, sorry + + .history-toolbar-entries-list( + ng-if="!history.error && history.updates.length > 0" + ) + toggle-switch( + ng-model="toolbarUIConfig.showOnlyLabels" + label-true=translate("history_view_labels") + label-false=translate("history_view_all") + description=translate("history_view_a11y_description") + ) + +script(type="text/ng-template", id="historyV2AddLabelModalTemplate") + form( + name="addLabelModalForm" + ng-submit="addLabelModalFormSubmit();" + novalidate + ) + .modal-header + h3 #{translate("history_add_label")} + .modal-body + .alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} + .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} + .form-group + input.form-control( + type="text" + placeholder=translate("history_new_label_name") + ng-model="inputs.labelName" + focus-on="open" + required + ) + p.help-block(ng-if="update") + | #{translate("history_new_label_added_at")} + strong {{ update.meta.end_ts | formatDate:'ddd Do MMM YYYY, h:mm a' }} + .modal-footer + button.btn.btn-default( + type="button" + ng-disabled="state.inflight" + ng-click="$dismiss()" + ) #{translate("cancel")} + input.btn.btn-primary( + ng-disabled="addLabelModalForm.$invalid || state.inflight" + ng-value="state.inflight ? '" + translate("history_adding_label") + "' : '" + translate("history_add_label") + "'" + type="submit" + ) diff --git a/services/web/app/views/project/editor/left-menu.pug b/services/web/app/views/project/editor/left-menu.pug new file mode 100644 index 0000000000..bb42f5ce9c --- /dev/null +++ b/services/web/app/views/project/editor/left-menu.pug @@ -0,0 +1,315 @@ +aside#left-menu.full-size( + ng-class="{ 'shown': ui.leftMenuShown }" + ng-cloak +) + h4 #{translate("download")} + + ul.list-unstyled.nav.nav-downloads.text-center + li + a( + ng-href="/project/{{project_id}}/download/zip" + target="_blank" + ) + i.fa.fa-file-archive-o.fa-2x + br + | #{translate("source")} + li + a( + ng-href="{{pdf.downloadUrl || pdf.url}}" + target="_blank" + ng-if="pdf.url" + ) + i.fa.fa-file-pdf-o.fa-2x + br + | PDF + div.link-disabled( + ng-if="!pdf.url" + tooltip=translate('please_compile_pdf_before_download') + tooltip-placement="bottom" + ) + i.fa.fa-file-pdf-o.fa-2x + br + | PDF + + span(ng-show="!anonymous") + h4 #{translate("actions")} + ul.list-unstyled.nav + li(ng-controller="LeftMenuCloneProjectModalController") + a( + href, + ng-click="openCloneProjectModal()" + ) + i.fa.fa-fw.fa-copy + |    #{translate("copy_project")} + + clone-project-modal( + handle-hide="handleHide" + project-id="projectId" + project-name="projectName" + open-project="openProject" + show="show" + ) + + != moduleIncludes("editorLeftMenu:actions", locals) + li(ng-controller="WordCountModalController") + a(href, ng-if="pdf.url", ng-click="openWordCountModal()") + i.fa.fa-fw.fa-eye + span    #{translate("word_count")} + a.link-disabled(href, ng-if="!pdf.url", tooltip=translate('please_compile_pdf_before_word_count')) + i.fa.fa-fw.fa-eye + span.link-disabled    #{translate("word_count")} + + word-count-modal( + clsi-server-id="clsiServerId" + handle-hide="handleHide" + project-id="projectId" + show="show" + ) + + if (moduleIncludesAvailable("editorLeftMenu:sync")) + div(ng-show="!anonymous") + h4() #{translate("sync")} + != moduleIncludes("editorLeftMenu:sync", locals) + + if (moduleIncludesAvailable("editorLeftMenu:editing_services")) + span(ng-show="!anonymous") + h4 #{translate("services")} + != moduleIncludes("editorLeftMenu:editing_services", locals) + + h4(ng-show="!anonymous") #{translate("settings")} + form.settings(ng-controller="SettingsController", ng-show="!anonymous") + .containter-fluid + .form-controls(ng-show="permissions.write") + label(for="compiler") #{translate("compiler")} + select( + name="compiler" + ng-model="project.compiler" + ) + option(value='pdflatex') pdfLaTeX + option(value='latex') LaTeX + option(value='xelatex') XeLaTeX + option(value='lualatex') LuaLaTeX + + if (typeof(allowedImageNames) !== 'undefined' && allowedImageNames.length > 0) + .form-controls(ng-show="permissions.write") + label(for="imageName") #{translate("tex_live_version")} + select( + name="imageName" + ng-model="project.imageName" + ) + each image in allowedImageNames + option(value=image.imageName) #{image.imageDesc} + + .form-controls(ng-show="permissions.write") + label(for="rootDoc_id") #{translate("main_document")} + select( + name="rootDoc_id", + ng-model="project.rootDoc_id", + ng-options="doc.doc.id as doc.path for doc in getValidMainDocs()" + ) + + .form-controls + label(for="spellCheckLanguage") #{translate("spell_check")} + select( + name="spellCheckLanguage" + ng-model="project.spellCheckLanguage" + ) + option(value="") #{translate("off")} + optgroup(label="Language") + for language in languages + option( + value=language.code + )= language.name + + .form-controls + label(for="autoComplete") #{translate("auto_complete")} + select( + name="autoComplete" + ng-model="settings.autoComplete" + ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]" + ) + + .form-controls + label(for="autoPairDelimiters") #{translate("auto_close_brackets")} + select( + name="autoPairDelimiters" + ng-model="settings.autoPairDelimiters" + ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]" + ) + + .form-controls.code-check-setting + label(for="syntaxValidation") #{translate("syntax_validation")} + select( + name="syntaxValidation" + ng-model="settings.syntaxValidation" + ng-options="o.v as o.n for o in [{ n: 'On', v: true }, { n: 'Off', v: false }]" + ) + + .form-controls + label(for="editorTheme") #{translate("editor_theme")} + select( + name="editorTheme" + ng-model="settings.editorTheme" + ) + each editorTheme in editorThemes + option(value=editorTheme) #{editorTheme.replace(/_/g, ' ')} + + if (settings.overleaf != null && !isIEEE(brandVariation)) + .form-controls + label(for="overallTheme") #{translate("overall_theme")} + select( + name="overallTheme" + ng-if="!ui.loadingStyleSheet" + ng-model="settings.overallTheme" + ng-options="overallTheme.val as overallTheme.name for overallTheme in overallThemesList" + ) + p.loading.pull-right( + ng-if="ui.loadingStyleSheet" + ) + i.fa.fa-fw.fa-spin.fa-refresh + + .form-controls(ng-show="!anonymous") + label(for="mode") #{translate("keybindings")} + select( + name="mode" + ng-model="settings.mode" + ) + option(value='default') None + option(value='vim') Vim + option(value='emacs') Emacs + + .form-controls + label(for="fontSize") #{translate("font_size")} + select( + name="fontSize" + ng-model="fontSizeAsStr" + ng-model-options="{ getterSetter: true }" + ) + each size in ['10','11','12','13','14','16','18','20','22','24'] + option(value=size) #{size}px + + .form-controls + label(for="fontFamily") #{translate("font_family")} + select( + name="fontFamily" + ng-model="settings.fontFamily" + ) + option(value='monaco') Monaco / Menlo / Consolas + option(value='lucida') Lucida / Source Code Pro + .form-controls + label(for="lineHeight") #{translate("line_height")} + select( + name="lineHeight" + ng-model="settings.lineHeight" + ) + each lineHeight in ['compact', 'normal', 'wide'] + option(value=lineHeight) #{translate(lineHeight)} + + .form-controls + label(for="pdfViewer") #{translate("pdf_viewer")} + select( + name="pdfViewer" + ng-model="settings.pdfViewer" + ) + option(value="pdfjs") #{translate("built_in")} + option(value="native") #{translate("native")} + + h4 #{translate("help")} + ul.list-unstyled.nav + li(ng-controller="HotkeysModalController") + a(ng-click="openHotkeysModal()") + i.fa.fa-keyboard-o.fa-fw + |    #{translate("show_hotkeys")} + + hotkeys-modal( + handle-hide="handleHide" + show="show" + track-changes-visible="trackChangesVisible" + is-mac="isMac" + ) + if showSupport + li + a(href='/learn', target="_blank") + i.fa.fa-book.fa-fw + |    #{translate('documentation')} + li + a(ng-controller="ContactModal", ng-click="contactUsModal()") + i.fa.fa-question.fa-fw + |    #{translate("contact_us")} + +#left-menu-mask( + ng-show="ui.leftMenuShown", + ng-click="ui.leftMenuShown = false" + ng-cloak +) + +script(type='text/ng-template', id='cloneProjectModalTemplate') + .modal-header + h3 #{translate("copy_project")} + .modal-body + .alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} + .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} + form(name="cloneProjectForm", novalidate) + .form-group + label #{translate("new_name")} + input.form-control( + type="text", + placeholder="New Project Name", + required, + ng-model="inputs.projectName", + on-enter="clone()", + focus-on="open" + ) + .modal-footer + button.btn.btn-default( + ng-disabled="state.inflight" + ng-click="cancel()" + ) #{translate("cancel")} + button.btn.btn-primary( + ng-disabled="cloneProjectForm.$invalid || state.inflight" + ng-click="clone()" + ) + span(ng-hide="state.inflight") #{translate("copy")} + span(ng-show="state.inflight") #{translate("copying")}… + +script(type='text/ng-template', id='wordCountModalTemplate') + .modal-header + h3 #{translate("word_count")} + .modal-body + div(ng-if="status.loading") + .loading(ng-show="!status.error && status.loading") + i.fa.fa-refresh.fa-spin.fa-fw + span   #{translate("loading")}… + div.pdf-disabled( + ng-if="!pdf.url" + tooltip=translate('please_compile_pdf_before_word_count') + tooltip-placement="bottom" + ) + div(ng-if="!status.loading") + .container-fluid + .row(ng-show='data.messages.length > 0') + .col-xs-12 + .alert.alert-danger + p(style="white-space: pre-wrap") {{data.messages}} + .row + .col-xs-4 + .pull-right #{translate("total_words")} : + .col-xs-6 {{data.textWords}} + .row + .col-xs-4 + .pull-right #{translate("headers")} : + .col-xs-6 {{data.headers}} + .row + .col-xs-4 + .pull-right #{translate("math_inline")} : + .col-xs-6 {{data.mathInline}} + .row + .col-xs-4 + .pull-right #{translate("math_display")} : + .col-xs-6 {{data.mathDisplay}} + .modal-footer + button.btn.btn-default( + ng-disabled="state.inflight" + ng-click="cancel()" + ) #{translate("done")} + diff --git a/services/web/app/views/project/editor/new-file-modal.pug b/services/web/app/views/project/editor/new-file-modal.pug new file mode 100644 index 0000000000..9ab6832f12 --- /dev/null +++ b/services/web/app/views/project/editor/new-file-modal.pug @@ -0,0 +1,232 @@ +script(type='text/ng-template', id='newFileModalTemplate') + .modal-header + h3 Add Files + .modal-body.modal-new-file(ng-show="file_count < 2000") + table + tr + td.modal-new-file--list + ul.list-unstyled + li(ng-class="type == 'doc' ? 'active' : null") + a(href, ng-click="type = 'doc'") + i.fa.fa-fw.fa-file + | + | New File + li(ng-class="type == 'upload' ? 'active' : null") + a(href, ng-click="type = 'upload'") + i.fa.fa-fw.fa-upload + | + | Upload + li(ng-class="type == 'project' ? 'active' : null") + a(href, ng-click="type = 'project'") + i.fa.fa-fw.fa-folder-open + | + | From Another Project + if hasFeature('link-url') + li(ng-class="type == 'url' ? 'active' : null") + a(href, ng-click="type = 'url'") + i.fa.fa-fw.fa-globe + | + | From External URL + != moduleIncludes("newFileModal:selector", locals) + + td(class="modal-new-file--body modal-new-file--body-{{type}}") + div(ng-if="type == 'doc'", ng-controller="NewDocModalController") + form(novalidate, name="newDocForm") + label(for="name") File Name + input.form-control( + type="text", + placeholder="File Name", + required, + ng-model="inputs.name", + on-enter="create()", + select-name-on="open", + valid-file, + name="name" + ) + div.alert.alert-danger.row-spaced-small(ng-if="error") + div(ng-switch="error") + span(ng-switch-when="already exists") #{translate("file_already_exists")} + span(ng-switch-default) {{error}} + div.alert.alert-danger.row-spaced-small(ng-show="newDocForm.name.$error.validFile && newDocForm.name.$viewValue.length") + | #{translate('files_cannot_include_invalid_characters')} + div(ng-if="type == 'upload'", ng-controller="UploadFileModalController") + .alert.alert-warning.small(ng-if="tooManyFiles") #{translate("maximum_files_uploaded_together", {max:"{{max_files}}"})} + .alert.alert-warning.small(ng-if="rateLimitHit") #{translate("too_many_files_uploaded_throttled_short_period")} + .alert.alert-warning.small(ng-if="notLoggedIn") #{translate("session_expired_redirecting_to_login", {seconds:"{{secondsToRedirect}}"})} + .alert.alert-warning.small(ng-if="conflicts.length > 0") + p.text-center + | The following files already exist in this project: + ul.text-center.list-unstyled.row-spaced-small + li(ng-repeat="conflict in conflicts track by $index"): strong {{ conflict }} + p.text-center.row-spaced-small + | Do you want to overwrite them? + p.text-center + a(href, ng-click="doUpload()").btn.btn-primary Overwrite + |   + a(href, ng-click="cancel()").btn.btn-default Cancel + div( + fine-upload + endpoint="/project/{{ project_id }}/upload" + template-id="qq-file-uploader-template" + multiple="true" + auto-upload="false" + on-complete-callback="onComplete" + on-upload-callback="onUpload" + on-validate-batch="onValidateBatch" + on-error-callback="onError" + on-submit-callback="onSubmit" + on-cancel-callback="onCancel" + control="control" + params="{'folder_id': parent_folder_id}" + ) + div(ng-if="type == 'project'", ng-controller="ProjectLinkedFileModalController") + div + form + .form-controls + label(for="project-select") Select a Project + span(ng-show="state.inFlight.projects") + |   + i.fa.fa-spinner.fa-spin + select.form-control( + name="project-select" + ng-model="data.selectedProjectId" + ng-disabled="!shouldEnableProjectSelect()" + ) + option(value="" disabled selected) - Please Select a Project + option( + ng-repeat="project in data.projects" + value="{{ project._id }}" + ) {{ project.name }} + small(ng-if="hasNoProjects() && shouldEnableProjectSelect() ") + | No other projects found, please create another project first + + + .form-controls.row-spaced-small(ng-if="!state.isOutputFilesMode") + label(for="project-entity-select") Select a File + span(ng-show="state.inFlight.entities") + |   + i.fa.fa-spinner.fa-spin + select.form-control( + name="project-entity-select" + ng-model="data.selectedProjectEntity" + ng-disabled="!shouldEnableProjectEntitySelect()" + ) + option(value="" disabled selected) - Please Select a File + option( + ng-repeat="projectEntity in data.projectEntities" + value="{{ projectEntity.path }}" + ) {{ projectEntity.path.slice(1) }} + + .form-controls.row-spaced-small(ng-if="state.isOutputFilesMode") + label(for="project-entity-select") Select an Output File + span(ng-show="state.inFlight.compile") + |   + i.fa.fa-spinner.fa-spin + select.form-control( + name="project-output-file-select" + ng-model="data.selectedProjectOutputFile" + ng-disabled="!shouldEnableProjectOutputFileSelect()" + ) + option(value="" disabled selected) - Please Select an Output File + option( + ng-repeat="outputFile in data.projectOutputFiles" + value="{{ outputFile.path }}" + ) {{ outputFile.path }} + div.toggle-output-files-button + | or  + a( + href="#" + ng-click="toggleOutputFilesMode()" + ) + span(ng-show="state.isOutputFilesMode") select from source files + span(ng-show="!state.isOutputFilesMode") select from output files + + .form-controls.row-spaced-small + label(for="name") #{translate("file_name_in_this_project")} + input.form-control( + type="text" + placeholder="example.tex" + required + ng-model="data.name" + name="name" + ) + div.alert.alert-danger.row-spaced-small(ng-if="error") + div(ng-switch="error") + span(ng-switch-when="already exists") #{translate("file_already_exists")} + span(ng-switch-when="too many files") #{translate("project_has_too_many_files")} + span(ng-switch-default) Error, something went wrong! + div(ng-if="type == 'url'", ng-controller="UrlLinkedFileModalController") + form(novalidate, name="newLinkedFileForm") + label(for="url") URL to fetch the file from + input.form-control( + type="text", + placeholder="www.example.com/my_file", + required, + ng-model="inputs.url", + focus-on="open", + on-enter="create()", + name="url" + ) + .row-spaced-small + label(for="name") #{translate("file_name_in_this_project")} + input.form-control( + type="text", + placeholder="my_file", + required, + ng-model="inputs.name", + ng-change="nameChangedByUser = true" + valid-file, + on-enter="create()", + name="name" + ) + .text-danger.row-spaced-small(ng-show="newDocForm.name.$error.validFile") + | #{translate('files_cannot_include_invalid_characters')} + div.alert.alert-danger.row-spaced-small(ng-if="error") + div(ng-switch="error") + span(ng-switch-when="already exists") #{translate("file_already_exists")} + span(ng-switch-when="too many files") #{translate("project_has_too_many_files")} + span(ng-switch-default) {{error}} + + != moduleIncludes("newFileModal:panel", locals) + + .modal-footer + .modal-footer-left.approaching-file-limit(ng-if="file_count > 1900 && file_count < 2000") + | #{translate("project_approaching_file_limit")} ({{file_count}}/2000) + .alert.alert-warning.at-file-limit(ng-if="file_count >= 2000") + | #{translate("project_has_too_many_files")} + button.btn.btn-default( + ng-disabled="state.inflight" + ng-click="cancel()" + ) #{translate("cancel")} + button.btn.btn-primary( + ng-disabled="state.inflight || !state.valid" + ng-click="create()" + ng-hide="type == 'upload'" + ) + 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 Drag here + div.row-spaced-small.small #{translate('or')} + div.row-spaced-small + div.qq-upload-button-selector.btn.btn-primary + | Select from your computer + 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 diff --git a/services/web/app/views/project/editor/new_from_template.pug b/services/web/app/views/project/editor/new_from_template.pug new file mode 100644 index 0000000000..554b1048f8 --- /dev/null +++ b/services/web/app/views/project/editor/new_from_template.pug @@ -0,0 +1,31 @@ +extends ../../layout + +block content + .editor.full-size + .loading-screen() + .loading-screen-brand-container + .loading-screen-brand( + style="height: 20%;" + ) + + h3.loading-screen-label() #{translate("Opening template")} + span.loading-screen-ellip . + span.loading-screen-ellip . + span.loading-screen-ellip . + + form(id='create_form' method='POST' action='/project/new/template/' ng-non-bindable) + input(type="hidden", name="_csrf", value=csrfToken) + input(type="hidden" name="templateId" value=templateId) + input(type="hidden" name="templateVersionId" value=templateVersionId) + input(type="hidden" name="templateName" value=name) + input(type="hidden" name="compiler" value=compiler) + input(type="hidden" name="imageName" value=imageName) + input(type="hidden" name="mainFile" value=mainFile) + if brandVariationId + input(type="hidden" name="brandVariationId" value=brandVariationId) + +block append foot-scripts + script(type="text/javascript", nonce=scriptNonce). + $(document).ready(function(){ + $('#create_form').submit(); + }); diff --git a/services/web/app/views/project/editor/pdf.pug b/services/web/app/views/project/editor/pdf.pug new file mode 100644 index 0000000000..96718b6e0d --- /dev/null +++ b/services/web/app/views/project/editor/pdf.pug @@ -0,0 +1,430 @@ +div.full-size.pdf(ng-controller="PdfController") + if showNewLogsUI + preview-pane( + compiler-state=`{ + autoCompileHasChanges: changesToAutoCompile, + autoCompileHasLintingError: autoCompileLintingError, + isAutoCompileOn: autocompile_enabled, + isClearingCache: pdf.clearingCache, + isCompiling: pdf.compiling, + isDraftModeOn: draft, + isSyntaxCheckOn: stop_on_validation_error, + lastCompileTimestamp: pdf.lastCompileTimestamp, + logEntries: pdf.logEntries, + validationIssues: pdf.validation, + rawLog: pdf.rawLog, + compileFailed: pdf.compileFailed, + errors: { + error: pdf.error, + renderingError: pdf.renderingError, + clsiMaintenance: pdf.clsiMaintenance, + clsiUnavailable: pdf.clsiUnavailable, + tooRecentlyCompiled: pdf.tooRecentlyCompiled, + compileTerminated: pdf.compileTerminated, + rateLimited: pdf.rateLimited, + compileInProgress: pdf.compileInProgress, + timedout: pdf.timedout, + projectTooLarge: pdf.projectTooLarge, + autoCompileDisabled: pdf.autoCompileDisabled, + failure: pdf.failure + } + }` + on-clear-cache="clearCache" + on-recompile="recompile" + on-recompile-from-scratch="recompileFromScratch" + on-run-syntax-check-now="runSyntaxCheckNow" + on-set-auto-compile="setAutoCompile" + on-set-draft-mode="setDraftMode" + on-set-syntax-check="setSyntaxCheck" + on-toggle-logs="toggleLogs" + output-files="pdf.outputFiles" + pdf-download-url="pdf.downloadUrl" + split-layout="ui.pdfLayout === 'sideBySide'" + on-set-split-layout="setPdfSplitLayout" + on-set-full-layout="setPdfFullLayout" + on-stop-compilation="stop" + variant-with-first-error-popup="logsUISubvariant === 'new-logs-ui-with-popup'" + show-logs="shouldShowLogs" + on-log-entry-location-click="openInEditor" + ) + else + .toolbar.toolbar-pdf(ng-class="{ 'changes-to-autocompile': changesToAutoCompile && !autoCompileLintingError }") + .btn-group.btn-recompile-group#recompile( + dropdown, + tooltip-html="'"+translate('recompile_pdf')+" ({{modifierKey}} + Enter)'" + tooltip-class="keyboard-tooltip" + tooltip-popup-delay="500" + tooltip-append-to-body="true" + tooltip-placement="bottom" + ) + a.btn.btn-recompile( + href, + ng-disabled="pdf.compiling", + ng-click="recompile()" + ) + i.fa.fa-refresh( + ng-class="{'fa-spin': pdf.compiling }" + ) + span.btn-recompile-label(ng-show="!pdf.compiling") #{translate("recompile")} + span.btn-recompile-label(ng-show="pdf.compiling") #{translate("compiling")}… + + a.btn.btn-recompile.dropdown-toggle( + href, + ng-disabled="pdf.compiling", + dropdown-toggle + ) + span.caret + ul.dropdown-menu.dropdown-menu-left + li.dropdown-header #{translate("auto_compile")} + li + a(href, ng-click="autocompile_enabled = true") + i.fa.fa-fw(ng-class="{'fa-check': autocompile_enabled}") + |  #{translate('on')} + li + a(href, ng-click="autocompile_enabled = false") + i.fa.fa-fw(ng-class="{'fa-check': !autocompile_enabled}") + |  #{translate('off')} + li.dropdown-header #{translate("compile_mode")} + li + a(href, ng-click="draft = false") + i.fa.fa-fw(ng-class="{'fa-check': !draft}") + |  #{translate("normal")} + li + a(href, ng-click="draft = true") + i.fa.fa-fw(ng-class="{'fa-check': draft}") + |  #{translate("fast")}  + span.subdued [draft] + li.dropdown-header #{translate("compile_time_checks")} + li + a(href, ng-click="stop_on_validation_error = true") + i.fa.fa-fw(ng-class="{'fa-check': stop_on_validation_error}") + |  #{translate("stop_on_validation_error")} + li + a(href, ng-click="stop_on_validation_error = false") + i.fa.fa-fw(ng-class="{'fa-check': !stop_on_validation_error}") + |  #{translate("ignore_validation_errors")} + li + a(href, ng-click="recompile({check:true})") + i.fa.fa-fw() + |  #{translate("run_syntax_check_now")} + a( + href + ng-click="stop()" + ng-show="pdf.compiling", + tooltip=translate('stop_compile') + tooltip-placement="bottom" + ) + i.fa.fa-fw.fa-stop() + a.log-btn( + href + ng-click="toggleLogs()" + ng-class="{ 'active': shouldShowLogs == true }" + tooltip=translate('logs_and_output_files') + tooltip-placement="bottom" + ) + i.fa.fa-fw.fa-file-text-o + span.label( + ng-show="pdf.logEntries.warnings.length + pdf.logEntries.errors.length > 0" + ng-class="{\ + 'label-warning': pdf.logEntries.errors.length == 0,\ + 'label-danger': pdf.logEntries.errors.length > 0\ + }" + ) {{ pdf.logEntries.errors.length + pdf.logEntries.warnings.length }} + + a( + ng-if="!pdf.downloadUrl" + disabled + href="#" + tooltip=translate('please_compile_pdf_before_download') + tooltip-placement="bottom" + ) + i.fa.fa-fw.fa-download + a( + ng-href="{{pdf.downloadUrl || pdf.url}}" + target="_blank" + ng-if="pdf.url" + tooltip=translate('download_pdf') + tooltip-placement="bottom" + ) + i.fa.fa-fw.fa-download + + .toolbar-right + span.auto-compile-status.small( + ng-show="changesToAutoCompile && !compiling && !autoCompileLintingError" + ) #{translate('uncompiled_changes')} + span.auto-compile-status.auto-compile-error.small( + ng-show="autoCompileLintingError" + tooltip-placement="bottom" + tooltip=translate("code_check_failed_explanation") + tooltip-append-to-body="true" + ) + i.fa.fa-fw.fa-exclamation-triangle + | + | #{translate("code_check_failed")} + a( + href, + ng-click="switchToFlatLayout('pdf')" + ng-show="ui.pdfLayout == 'sideBySide'" + tooltip=translate('full_screen') + tooltip-placement="bottom" + tooltip-append-to-body="true" + ) + i.fa.fa-expand + a( + href, + ng-click="switchToSideBySideLayout('editor')" + ng-show="ui.pdfLayout == 'flat'" + tooltip=translate('split_screen') + tooltip-placement="bottom" + tooltip-append-to-body="true" + ) + i.fa.fa-compress + // end of toolbar + + // logs view + .pdf-logs(ng-show="shouldShowLogs") + .alert.alert-success(ng-show="pdf.logEntries.all.length == 0 && !pdf.failure") + | #{translate("no_errors_good_job")} + + .alert.alert-danger(ng-show="pdf.failure") + strong #{translate("compile_error")}. + span #{translate("generic_failed_compile_message")}. + + .alert.alert-danger(ng-show="pdf.failedCheck") + strong #{translate("failed_compile_check")}. + p + p.text-center(ng-show="!check") + a.text-info( + href, + ng-disabled="pdf.compiling", + ng-click="recompile({try:true})" + ) #{translate("failed_compile_check_try")} + | #{translate("failed_compile_option_or")} + a.text-info( + href, + ng-disabled="pdf.compiling", + ng-click="recompile({force:true})" + ) #{translate("failed_compile_check_ignore")} + | . + + div(ng-repeat="entry in pdf.logEntries.all") + .alert( + ng-class="{\ + 'alert-danger': entry.level == 'error',\ + 'alert-warning': entry.level == 'warning',\ + 'alert-info': entry.level == 'typesetting'\ + }" + ng-click="openInEditor(entry)" + ) + span.line-no + i.fa.fa-link(aria-hidden="true") + |   + span(ng-show="entry.file") {{ entry.file }} + span(ng-show="entry.line") , line {{ entry.line }} + p.entry-message(ng-show="entry.message") + | {{ entry.type }} {{ entry.message }} + .card.card-hint( + ng-if="entry.humanReadableHint" + stop-propagation="click" + ) + figure.card-hint-icon-container + i.fa.fa-lightbulb-o(aria-hidden="true") + p.card-hint-text( + ng-show="entry.humanReadableHint", + ng-bind-html="entry.humanReadableHint") + .card-hint-footer.clearfix + .card-hint-ext-link(ng-if="entry.extraInfoURL") + a( + ng-href="{{ entry.extraInfoURL }}", + ng-click="trackLogHintsLearnMore()" + target="_blank" + ) + i.fa.fa-external-link + | #{translate("log_hint_extra_info")} + + p.entry-content(ng-show="entry.content") {{ entry.content.trim() }} + + div + .files-dropdown-container + a.btn.btn-default.btn-sm( + href, + tooltip=translate('clear_cached_files'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="openClearCacheModal()" + ) + i.fa.fa-trash-o + |   + div.files-dropdown( + ng-class="shouldDropUp ? 'dropup' : 'dropdown'" + dropdown + ) + a.btn.btn-default.btn-sm( + href + dropdown-toggle + ) + | #{translate("other_logs_and_files")} + span.caret + ul.dropdown-menu.dropdown-menu-right + li(ng-repeat="file in pdf.outputFiles") + a( + ng-href="{{file.url}}" + target="_blank" + ) {{ file.name }} + a.btn.btn-info.btn-sm(href, ng-click="toggleRawLog()") + span(ng-show="!pdf.showRawLog") #{translate("view_raw_logs")} + span(ng-show="pdf.showRawLog") #{translate("hide_raw_logs")} + + pre(ng-bind="pdf.rawLog", ng-show="pdf.showRawLog") + + + // non-log views (pdf and errors) + div(ng-show="!shouldShowLogs", ng-switch on="pdf.view") + .pdf-uncompiled(ng-switch-when="uncompiled" ng-show="!pdf.compiling") + |   + i.fa.fa-level-up.fa-flip-horizontal.fa-2x + |   #{translate('click_here_to_preview_pdf')} + + .pdf-viewer(ng-switch-when="pdf") + div( + pdfng + ng-if="settings.pdfViewer == 'pdfjs'" + pdf-src="pdf.url" + key="{{ project_id }}" + resize-on="layout:main:resize,layout:pdf:resize" + highlights="pdf.highlights" + position="pdf.position" + first-render-done="pdf.firstRenderDone" + update-consumed-bandwidth="pdf.updateConsumedBandwidth" + dbl-click-callback="syncToCode" + ) + iframe( + ng-src="{{ pdf.url | trusted }}" + ng-if="settings.pdfViewer == 'native'" + ) + + if !showNewLogsUI + .pdf-validation-problems(ng-switch-when="validation-problems") + + .alert.alert-danger(ng-show="pdf.validation.sizeCheck") + strong #{translate("project_too_large")} + div #{translate("project_too_large_please_reduce")} + div + li(ng-repeat="entry in pdf.validation.sizeCheck.resources") {{ '/'+entry['path'] }} - {{entry['kbSize']}}kb + + .alert.alert-danger(ng-show="pdf.validation.conflictedPaths") + div + strong #{translate("conflicting_paths_found")} + div !{translate("following_paths_conflict")} + div + li(ng-repeat="entry in pdf.validation.conflictedPaths") {{ '/'+entry['path'] }} + + .alert.alert-danger(ng-show="pdf.validation.mainFile") + strong #{translate("main_file_not_found")} + span #{translate("please_set_main_file")} + + .pdf-errors(ng-switch-when="errors") + + .alert.alert-danger(ng-show="pdf.error") + strong #{translate("server_error")} + span #{translate("somthing_went_wrong_compiling")} + + .alert.alert-danger(ng-show="pdf.renderingError") + strong #{translate("pdf_rendering_error")} + span #{translate("something_went_wrong_rendering_pdf")} + + .alert.alert-danger(ng-show="pdf.clsiMaintenance") + strong #{translate("server_error")} + span #{translate("clsi_maintenance")} + + .alert.alert-danger(ng-show="pdf.clsiUnavailable") + strong #{translate("server_error")} + span #{translate("clsi_unavailable")} + + .alert.alert-danger(ng-show="pdf.tooRecentlyCompiled") + strong #{translate("server_error")} + span #{translate("too_recently_compiled")} + + .alert.alert-danger(ng-show="pdf.compileTerminated") + strong #{translate("terminated")}. + span #{translate("compile_terminated_by_user")} + + .alert.alert-danger(ng-show="pdf.rateLimited") + strong #{translate("pdf_compile_rate_limit_hit")} + span #{translate("project_flagged_too_many_compiles")} + + .alert.alert-danger(ng-show="pdf.compileInProgress") + strong #{translate("pdf_compile_in_progress_error")}. + span #{translate("pdf_compile_try_again")} + + .alert.alert-danger(ng-show="pdf.timedout") + p + strong #{translate("timedout")}. + span #{translate("proj_timed_out_reason")} + p + a.text-info(href="https://www.sharelatex.com/learn/Debugging_Compilation_timeout_errors", target="_blank") + | #{translate("learn_how_to_make_documents_compile_quickly")} + + if settings.enableSubscriptions + .alert.alert-success(ng-show="pdf.timedout && !hasPremiumCompile") + p(ng-if="project.owner._id == user.id") + strong #{translate("upgrade_for_longer_compiles")} + p(ng-if="project.owner._id != user.id") + strong #{translate("ask_proj_owner_to_upgrade_for_longer_compiles")} + p #{translate("free_accounts_have_timeout_upgrade_to_increase")} + p #{translate("plus_upgraded_accounts_receive")}: + div + ul.list-unstyled + li + i.fa.fa-check   + | #{translate("unlimited_projects")} + li + i.fa.fa-check   + | #{translate("collabs_per_proj", {collabcount:'Multiple'})} + li + i.fa.fa-check   + | #{translate("full_doc_history")} + li + i.fa.fa-check   + | #{translate("sync_to_dropbox")} + li + i.fa.fa-check   + | #{translate("sync_to_github")} + li + i.fa.fa-check   + |#{translate("compile_larger_projects")} + p(ng-controller="FreeTrialModalController", ng-if="project.owner._id == user.id") + a.btn.btn-success.row-spaced-small( + href + ng-class="buttonClass" + ng-click="startFreeTrial('compile-timeout')" + ) #{translate("start_free_trial")} + + .alert.alert-danger(ng-show="pdf.autoCompileDisabled") + p + strong #{translate("autocompile_disabled")}. + span #{translate("autocompile_disabled_reason")} + + .alert.alert-danger(ng-show="pdf.projectTooLarge") + strong #{translate("project_too_large")} + span #{translate("project_too_much_editable_text")} + + +script(type='text/ng-template', id='clearCacheModalTemplate') + .modal-header + h3 #{translate("clear_cache")}? + .modal-body + p #{translate("clear_cache_explanation")} + p #{translate("clear_cache_is_safe")} + .alert.alert-danger(ng-if="state.error") #{translate("generic_something_went_wrong")}. + .modal-footer + button.btn.btn-default( + ng-click="cancel()" + ng-disabled="state.inflight" + ) #{translate("cancel")} + button.btn.btn-info( + ng-click="clear()" + ng-disabled="state.inflight" + ) + span(ng-show="!state.inflight") #{translate("clear_cache")} + span(ng-show="state.inflight") #{translate("clearing")}… diff --git a/services/web/app/views/project/editor/review-panel.pug b/services/web/app/views/project/editor/review-panel.pug new file mode 100644 index 0000000000..aa9cff0228 --- /dev/null +++ b/services/web/app/views/project/editor/review-panel.pug @@ -0,0 +1,628 @@ +#review-panel + .rp-in-editor-widgets + a.rp-track-changes-indicator( + href + ng-if="editor.wantTrackChanges" + ng-click="toggleReviewPanel();" + ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }" + ) !{translate("track_changes_is_on")} + a.rp-bulk-actions-btn( + href + ng-if="reviewPanel.nVisibleSelectedChanges > 1" + ng-click="showBulkAcceptDialog();" + ) + i.fa.fa-check + |  #{translate("accept_all")} + | ({{ reviewPanel.nVisibleSelectedChanges }}) + a.rp-bulk-actions-btn( + href + ng-if="reviewPanel.nVisibleSelectedChanges > 1" + ng-click="showBulkRejectDialog();" + ) + i.fa.fa-times + |  #{translate("reject_all")} + | ({{ reviewPanel.nVisibleSelectedChanges }}) + if hasFeature('track-changes') + a.rp-add-comment-btn( + href + ng-if="reviewPanel.entries[editor.open_doc_id]['add-comment'] != null && permissions.comment" + ng-click="addNewComment();" + ) + i.fa.fa-comment + |  #{translate("add_comment")} + a.review-panel-toggler( + href + ng-click="handleTogglerClick($event);" + ) + .review-panel-toolbar + resolved-comments-dropdown( + class="rp-flex-block" + entries="reviewPanel.resolvedComments" + threads="reviewPanel.commentThreads" + resolved-ids="reviewPanel.resolvedThreadIds" + docs="docs" + on-open="refreshResolvedCommentsDropdown();" + on-unresolve="unresolveComment(threadId);" + on-delete="deleteThread(entryId, docId, threadId);" + is-loading="reviewPanel.dropdown.loading" + permissions="permissions" + ) + span.review-panel-toolbar-label + span.review-panel-toolbar-icon-on( + ng-if="editor.wantTrackChanges === true" + ) + i.fa.fa-circle + span(ng-click="toggleFullTCStateCollapse();") + span(ng-if="editor.wantTrackChanges === false") !{translate("track_changes_is_off")} + span(ng-if="editor.wantTrackChanges === true") !{translate("track_changes_is_on")} + span.rp-tc-state-collapse( + ng-class="{ 'rp-tc-state-collapse-on': reviewPanel.fullTCStateCollapsed }" + ) + i.fa.fa-angle-down + ul.rp-tc-state( + review-panel-collapse-height="reviewPanel.fullTCStateCollapsed" + ) + li.rp-tc-state-item.rp-tc-state-item-everyone + span.rp-tc-state-item-name( + tooltip=translate('tc_switch_everyone_tip') + tooltip-placement="left" + tooltip-append-to-body="true" + tooltip-popup-delay="1000" + ) !{translate("tc_everyone")} + review-panel-toggle( + description="Track changes for everyone" + ng-model="reviewPanel.trackChangesOnForEveryone" + on-toggle="toggleTrackChangesForEveryone(isOn);" + is-disabled="!project.features.trackChanges || !permissions.write" + ) + li.rp-tc-state-item( + ng-repeat="member in reviewPanel.formattedProjectMembers" + ) + span.rp-tc-state-item-name( + ng-class="{ 'rp-tc-state-item-name-disabled' : reviewPanel.trackChangesOnForEveryone}" + style="color: hsl({{ member.hue }}, 70%, 40%);" + tooltip=translate('tc_switch_user_tip') + tooltip-placement="left" + tooltip-append-to-body="true" + tooltip-popup-delay="1000" + ) {{ member.name }} + review-panel-toggle( + description="Track changes for {{ member.name }}" + ng-model="reviewPanel.trackChangesState[member.id].value" + on-toggle="toggleTrackChangesForUser(isOn, member.id);" + is-disabled="reviewPanel.trackChangesOnForEveryone || !project.features.trackChanges || !permissions.write" + ) + + li.rp-tc-state-separator + li.rp-tc-state-item.rp-tc-state-item-guests + span.rp-tc-state-item-name( + ng-class="{ 'rp-tc-state-item-name-disabled' : reviewPanel.trackChangesOnForEveryone}" + tooltip=translate('tc_switch_guests_tip') + tooltip-placement="left" + tooltip-append-to-body="true" + tooltip-popup-delay="1000" + ) !{translate("tc_guests")} + review-panel-toggle( + description="Track changes for guests" + ng-model="reviewPanel.trackChangesOnForGuests" + on-toggle="toggleTrackChangesForGuests(isOn);" + is-disabled="reviewPanel.trackChangesOnForEveryone || !project.features.trackChanges || !permissions.write || !reviewPanel.trackChangesForGuestsAvailable" + ) + + .rp-entry-list( + review-panel-sorted + ng-if="reviewPanel.subView === SubViews.CUR_FILE" + ) + .rp-entry-list-inner + .rp-entry-wrapper( + ng-repeat="(entry_id, entry) in reviewPanel.entries[editor.open_doc_id]" + ng-if="entry.visible" + ) + div(ng-if="entry.type === 'insert' || entry.type === 'delete'") + change-entry( + entry="entry" + user="users[entry.metadata.user_id]" + on-reject="rejectChanges(entry.entry_ids);" + on-accept="acceptChanges(entry.entry_ids);" + on-indicator-click="toggleReviewPanel();" + on-body-click="gotoEntry(editor.open_doc_id, entry)" + permissions="permissions" + ) + + div(ng-if="entry.type === 'aggregate-change'") + aggregate-change-entry( + entry="entry" + user="users[entry.metadata.user_id]" + on-reject="rejectChanges(entry.entry_ids);" + on-accept="acceptChanges(entry.entry_ids);" + on-indicator-click="toggleReviewPanel();" + on-body-click="gotoEntry(editor.open_doc_id, entry)" + permissions="permissions" + ) + + div(ng-if="entry.type === 'comment'") + comment-entry( + entry="entry" + threads="reviewPanel.commentThreads" + on-resolve="resolveComment(entry, entry_id)" + on-reply="submitReply(entry, entry_id);" + on-indicator-click="toggleReviewPanel();" + on-save-edit="saveEdit(entry.thread_id, comment)" + on-delete="deleteComment(entry.thread_id, comment)" + on-body-click="gotoEntry(editor.open_doc_id, entry)" + permissions="permissions" + ng-if="!reviewPanel.loadingThreads" + ) + + div(ng-if="entry.type === 'add-comment' && permissions.comment") + add-comment-entry( + on-start-new="startNewComment();" + on-submit="submitNewComment(content);" + on-cancel="cancelNewComment();" + ) + div(ng-if="entry.type === 'bulk-actions'") + bulk-actions-entry( + on-bulk-accept="showBulkAcceptDialog();" + on-bulk-reject="showBulkRejectDialog();" + n-entries="reviewPanel.nVisibleSelectedChanges" + ) + + .rp-entry-list( + ng-if="reviewPanel.subView === SubViews.OVERVIEW" + ) + .rp-loading(ng-if="reviewPanel.overview.loading") + i.fa.fa-spinner.fa-spin + .rp-overview-file( + ng-repeat="doc in docs" + ng-if="!reviewPanel.overview.loading" + ) + .rp-overview-file-header( + ng-if="(reviewPanel.entries[doc.doc.id] != null) && (reviewPanel.entries[doc.doc.id] | notEmpty)" + ng-click="reviewPanel.overview.docsCollapsedState[doc.doc.id] = ! reviewPanel.overview.docsCollapsedState[doc.doc.id]" + ) + span.rp-overview-file-header-collapse( + ng-class="{ 'rp-overview-file-header-collapse-on': reviewPanel.overview.docsCollapsedState[doc.doc.id] }" + ) + i.fa.fa-angle-down + | {{ doc.path }} + span.rp-overview-file-num-entries( + ng-show="reviewPanel.overview.docsCollapsedState[doc.doc.id]" + )  ({{ reviewPanel.entries[doc.doc.id] | numKeys }}) + + .rp-overview-file-entries( + review-panel-collapse-height="reviewPanel.overview.docsCollapsedState[doc.doc.id]" + ) + .rp-entry-wrapper( + ng-repeat="(entry_id, entry) in reviewPanel.entries[doc.doc.id] | orderOverviewEntries" + ng-if="!(entry.type === 'comment' && reviewPanel.commentThreads[entry.thread_id].resolved === true)" + ) + div(ng-if="entry.type === 'insert' || entry.type === 'delete'") + change-entry( + entry="entry" + user="users[entry.metadata.user_id]" + ng-click="gotoEntry(doc.doc.id, entry)" + permissions="permissions" + ) + + div(ng-if="entry.type === 'aggregate-change'") + aggregate-change-entry( + entry="entry" + user="users[entry.metadata.user_id]" + ng-click="gotoEntry(editor.open_doc_id, entry)" + permissions="permissions" + ) + + div(ng-if="entry.type === 'comment'") + comment-entry( + entry="entry" + threads="reviewPanel.commentThreads" + on-reply="submitReply(entry, entry_id);" + on-save-edit="saveEdit(entry.thread_id, comment)" + on-delete="deleteComment(entry.thread_id, comment)" + ng-click="gotoEntry(doc.doc.id, entry)" + permissions="permissions" + ) + + .rp-nav + a.rp-nav-item( + href + ng-click="setSubView(SubViews.CUR_FILE);" + ng-class="{ 'rp-nav-item-active' : reviewPanel.subView === SubViews.CUR_FILE }" + ) + i.fa.fa-file-text-o + span.rp-nav-label #{translate("current_file")} + a.rp-nav-item( + href + ng-click="setSubView(SubViews.OVERVIEW);" + ng-class="{ 'rp-nav-item-active' : reviewPanel.subView === SubViews.OVERVIEW }" + ) + i.fa.fa-list + span.rp-nav-label #{translate("overview")} + + .rp-unsupported-msg-wrapper + .rp-unsupported-msg + i.fa.fa-5x.fa-exclamation-triangle + p.rp-unsupported-msg-title Track Changes support in rich text mode is a work in progress. + p You can see tracked insertions and deletions, but you can't see comments and changes in this side bar yet. + p We're working hard to add them as soon as they're ready. + + +script(type='text/ng-template', id='changeEntryTemplate') + div + .rp-entry-callout( + ng-class="'rp-entry-callout-' + entry.type" + ) + .rp-entry-indicator( + ng-switch="entry.type" + ng-class="{ 'rp-entry-indicator-focused': entry.focused }" + ng-click="onIndicatorClick();" + ) + i.fa.fa-pencil(ng-switch-when="insert") + i.rp-icon-delete(ng-switch-when="delete") + .rp-entry( + ng-class="[ 'rp-entry-' + entry.type, (entry.focused ? 'rp-entry-focused' : '')]" + ) + .rp-entry-body + .rp-entry-action-icon(ng-switch="entry.type") + i.fa.fa-pencil(ng-switch-when="insert") + i.rp-icon-delete(ng-switch-when="delete") + .rp-entry-details + .rp-entry-description(ng-switch="entry.type") + span(ng-switch-when="insert") #{translate("tracked_change_added")}  + ins.rp-content-highlight {{ entry.content | limitTo:(isCollapsed ? contentLimit : entry.content.length) }} + a.rp-collapse-toggle( + href + ng-if="needsCollapsing" + ng-click="toggleCollapse();" + ) {{ isCollapsed ? '… (#{translate("show_all")})' : ' (#{translate("show_less")})' }} + span(ng-switch-when="delete") #{translate("tracked_change_deleted")}  + del.rp-content-highlight {{ entry.content | limitTo:(isCollapsed ? contentLimit : entry.content.length) }} + a.rp-collapse-toggle( + href + ng-if="needsCollapsing" + ng-click="toggleCollapse();" + ) {{ isCollapsed ? '… (#{translate("show_all")})' : ' (#{translate("show_less")})' }} + .rp-entry-metadata + | {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }} •  + span.rp-entry-user(ng-switch="user.name" style="color: hsl({{ user.hue }}, 70%, 40%);") + span(ng-switch-when="undefined") #{translate("anonymous")} + span(ng-switch-default) {{ user.name }} + .rp-entry-actions(ng-if="permissions.write") + a.rp-entry-button(href, ng-click="onReject();") + i.fa.fa-times + |  #{translate("reject")} + a.rp-entry-button(href, ng-click="onAccept();") + i.fa.fa-check + |  #{translate("accept")} + +script(type='text/ng-template', id='aggregateChangeEntryTemplate') + div + .rp-entry-callout.rp-entry-callout-aggregate + .rp-entry-indicator( + ng-class="{ 'rp-entry-indicator-focused': entry.focused }" + ng-click="onIndicatorClick();" + ) + i.fa.fa-pencil + .rp-entry.rp-entry-aggregate( + ng-class="{ 'rp-entry-focused': entry.focused }" + ) + .rp-entry-body + .rp-entry-action-icon + i.fa.fa-pencil + .rp-entry-details + .rp-entry-description + | #{translate("aggregate_changed")}  + del.rp-content-highlight + | {{ entry.metadata.replaced_content | limitTo:(isDeletionCollapsed ? contentLimit : entry.metadata.replaced_contentlength) }} + a.rp-collapse-toggle( + href + ng-if="deletionNeedsCollapsing" + ng-click="toggleDeletionCollapse();" + ) {{ isDeletionCollapsed ? '… (#{translate("show_all")})' : ' (#{translate("show_less")})' }} + | #{translate("aggregate_to")}  + ins.rp-content-highlight + | {{ entry.content | limitTo:(isInsertionCollapsed ? contentLimit : entry.contentlength) }} + a.rp-collapse-toggle( + href + ng-if="insertionNeedsCollapsing" + ng-click="toggleInsertionCollapse();" + ) {{ isInsertionCollapsed ? '… (#{translate("show_all")})' : ' (#{translate("show_less")})' }} + .rp-entry-metadata + | {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }} •  + span.rp-entry-user(ng-switch="user.name" style="color: hsl({{ user.hue }}, 70%, 40%);") + span(ng-switch-when="undefined") #{translate("anonymous")} + span(ng-switch-default) {{ user.name }} + .rp-entry-actions(ng-if="permissions.write") + a.rp-entry-button(href, ng-click="onReject();") + i.fa.fa-times + |  #{translate("reject")} + a.rp-entry-button(href, ng-click="onAccept();") + i.fa.fa-check + |  #{translate("accept")} + +script(type='text/ng-template', id='commentEntryTemplate') + .rp-comment-wrapper( + ng-class="{ 'rp-comment-wrapper-resolving': state.animating }" + ) + .rp-entry-callout.rp-entry-callout-comment + .rp-entry-indicator( + ng-class="{ 'rp-entry-indicator-focused': entry.focused }" + ng-click="onIndicatorClick();" + ) + i.fa.fa-comment + .rp-entry.rp-entry-comment( + ng-class="{ 'rp-entry-focused': entry.focused, 'rp-entry-comment-resolving': state.animating }" + ) + + .rp-loading(ng-if="!threads[entry.thread_id].submitting && (!threads[entry.thread_id] || threads[entry.thread_id].messages.length == 0)") + | #{translate("no_comments")} + .rp-comment-loaded + .rp-comment( + ng-repeat="comment in threads[entry.thread_id].messages track by comment.id" + ) + p.rp-comment-content + span(ng-if="!comment.editing") + span.rp-entry-user( + style="color: hsl({{ comment.user.hue }}, 70%, 40%);", + ) {{ comment.user.name }}:  + span(ng-bind-html="comment.content | linky:'_blank':{rel: 'noreferrer noopener'}") + textarea.rp-comment-input( + expandable-text-area + ng-if="comment.editing" + ng-model="comment.content" + ng-keypress="saveEditOnEnter($event, comment);" + ng-blur="saveEdit(comment)" + autofocus + stop-propagation="click" + ) + .rp-entry-metadata(ng-if="!comment.editing") + span(ng-if="!comment.deleting") {{ comment.timestamp | date : 'MMM d, y h:mm a' }} + span.rp-comment-actions(ng-if="comment.user.isSelf && !comment.deleting") + |  •  + a(href, ng-click="startEditing(comment)") #{translate("edit")} + span(ng-if="threads[entry.thread_id].messages.length > 1") + |  •  + a(href, ng-click="confirmDelete(comment)") #{translate("delete")} + span.rp-confim-delete(ng-if="comment.user.isSelf && comment.deleting") + | #{translate("are_you_sure")} + | •  + a(href, ng-click="doDelete(comment)") #{translate("delete")} + |  •  + a(href, ng-click="cancelDelete(comment)") #{translate("cancel")} + + .rp-loading(ng-if="threads[entry.thread_id].submitting") + i.fa.fa-spinner.fa-spin + .rp-comment-reply(ng-if="permissions.comment") + textarea.rp-comment-input( + expandable-text-area + ng-model="entry.replyContent" + ng-keypress="handleCommentReplyKeyPress($event);" + stop-propagation="click" + placeholder=translate("hit_enter_to_reply") + ) + .rp-entry-actions + button.rp-entry-button( + ng-click="animateAndCallOnResolve();" + ng-if="permissions.comment && permissions.write" + ) + i.fa.fa-inbox + |  #{translate("resolve")} + button.rp-entry-button( + ng-click="onReply();" + ng-if="permissions.comment" + ng-disabled="!entry.replyContent.length" + ) + i.fa.fa-reply + |  #{translate("reply")} + +script(type='text/ng-template', id='resolvedCommentEntryTemplate') + .rp-resolved-comment + div + .rp-resolved-comment-context + | #{translate("quoted_text_in")}  + span.rp-resolved-comment-context-file {{ thread.docName }} + p.rp-resolved-comment-context-quote + span {{ thread.content | limitTo:(isCollapsed ? contentLimit : thread.content.length)}} + a.rp-collapse-toggle( + href + ng-if="needsCollapsing" + ng-click="toggleCollapse();" + )  {{ isCollapsed ? '… (#{translate("show_all")})' : ' (#{translate("show_less")})' }} + .rp-comment( + ng-repeat="comment in thread.messages track by comment.id" + ) + p.rp-comment-content + span.rp-entry-user( + style="color: hsl({{ comment.user.hue }}, 70%, 40%);" + ng-if="$first || comment.user.id !== thread.messages[$index - 1].user.id" + ) {{ comment.user.name }}:  + span(ng-bind-html="comment.content | linky:'_blank':{rel: 'noreferrer noopener'}") + .rp-entry-metadata + | {{ comment.timestamp | date : 'MMM d, y h:mm a' }} + .rp-comment.rp-comment-resolver + p.rp-comment-resolver-content + span.rp-entry-user( + style="color: hsl({{ thread.resolved_by_user.hue }}, 70%, 40%);" + ) {{ thread.resolved_by_user.name }}:  + | #{translate("mark_as_resolved")}. + .rp-entry-metadata + | {{ thread.resolved_at | date : 'MMM d, y h:mm a' }} + + .rp-entry-actions(ng-if="permissions.comment && permissions.write") + a.rp-entry-button( + href + ng-click="onUnresolve({ 'threadId': thread.threadId });" + ) + |  #{translate("reopen")} + a.rp-entry-button( + href + ng-click="onDelete({ 'entryId': thread.entryId, 'docId': thread.docId, 'threadId': thread.threadId });" + ) + |  #{translate("delete")} + + +script(type='text/ng-template', id='addCommentEntryTemplate') + div + .rp-entry-callout.rp-entry-callout-add-comment + .rp-entry.rp-entry-add-comment( + ng-class="[ (state.isAdding ? 'rp-entry-adding-comment' : ''), (entry.focused ? 'rp-entry-focused' : '')]" + ) + a.rp-add-comment-btn( + href + ng-if="!state.isAdding" + ng-click="startNewComment();" + ) + i.fa.fa-comment + |  #{translate("add_comment")} + div(ng-if="state.isAdding") + .rp-new-comment + textarea.rp-comment-input( + expandable-text-area + ng-model="state.content" + ng-keypress="handleCommentKeyPress($event);" + placeholder=translate("add_your_comment_here") + focus-on="comment:new:open" + ) + .rp-entry-actions + button.rp-entry-button.rp-entry-button-cancel( + ng-click="cancelNewComment();" + ) + i.fa.fa-times + |  #{translate("cancel")} + button.rp-entry-button( + ng-click="submitNewComment()" + ng-disabled="!state.content.length" + ) + i.fa.fa-comment + |  #{translate("comment")} + +script(type='text/ng-template', id='bulkActionsEntryTemplate') + div(ng-if="nEntries > 1") + .rp-entry-callout.rp-entry-callout-bulk-actions + .rp-entry.rp-entry-bulk-actions + a.rp-bulk-actions-btn( + href + ng-click="bulkReject();" + ) + i.fa.fa-times + |  #{translate("reject_all")} + | ({{ nEntries }}) + a.rp-bulk-actions-btn( + href + ng-click="bulkAccept();" + ) + i.fa.fa-check + |  #{translate("accept_all")} + | ({{ nEntries }}) + +script(type='text/ng-template', id='resolvedCommentsDropdownTemplate') + .resolved-comments + .resolved-comments-backdrop( + ng-class="{ 'resolved-comments-backdrop-visible' : state.isOpen }" + ng-click="state.isOpen = false" + ) + a.resolved-comments-toggle( + href + ng-click="toggleOpenState();" + tooltip=translate("resolved_comments") + tooltip-placement="bottom" + tooltip-append-to-body="true" + ) + i.fa.fa-inbox + .resolved-comments-dropdown( + ng-class="{ 'resolved-comments-dropdown-open' : state.isOpen }" + ) + .rp-loading(ng-if="isLoading") + i.fa.fa-spinner.fa-spin + .resolved-comments-scroller( + ng-if="!isLoading" + ) + resolved-comment-entry( + ng-repeat="thread in resolvedComments | orderBy:'resolved_at':true" + thread="thread" + on-unresolve="handleUnresolve(threadId);" + on-delete="handleDelete(entryId, docId, threadId);" + permissions="permissions" + ) + .rp-loading(ng-if="!resolvedComments.length") + | #{translate("no_resolved_threads")}. + +script(type="text/ng-template", id="trackChangesUpgradeModalTemplate") + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="cancel()" + aria-label="Close" + ) + span(aria-hidden="true") × + h3 #{translate("upgrade_to_track_changes")} + .modal-body + .teaser-video-container + video.teaser-video(autoplay, loop) + source(ng-src="{{ '/img/teasers/track-changes/teaser-track-changes.mp4' }}", type="video/mp4") + img(src="/img/teasers/track-changes/teaser-track-changes.gif") + + h4.teaser-title #{translate("see_changes_in_your_documents_live")} + + p.small(ng-show="startedFreeTrial") + | #{translate("refresh_page_after_starting_free_trial")} + + .row + .col-md-10.col-md-offset-1 + ul.list-unstyled + li + i.fa.fa-check   + | #{translate("track_any_change_in_real_time")} + + li + i.fa.fa-check   + | #{translate("review_your_peers_work")} + + li + i.fa.fa-check   + | #{translate("accept_or_reject_each_changes_individually")} + + .row.text-center + div(ng-show="user.allowedFreeTrial" ng-controller="FreeTrialModalController") + a.btn.btn-success( + href + ng-click="startFreeTrial('track-changes')" + ng-show="project.owner._id == user.id" + ) #{translate("try_it_for_free")} + div(ng-show="!user.allowedFreeTrial" ng-controller="UpgradeModalController") + a.btn.btn-success( + href + ng-click="upgradePlan('project-sharing')" + ng-show="project.owner._id == user.id" + ) #{translate("upgrade")} + p(ng-show="project.owner._id != user.id"): strong #{translate("please_ask_the_project_owner_to_upgrade_to_track_changes")} + + .modal-footer() + button.btn.btn-default( + ng-click="cancel()" + ) + span #{translate("close")} + +script(type="text/ng-template", id="bulkActionsModalTemplate") + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="cancel()" + aria-label="Close" + ) + span(aria-hidden="true") × + h3 {{ isAccept ? '#{translate("accept_all")}' : '#{translate("reject_all")}' }} + .modal-body + p(ng-if="isAccept") #{translate("bulk_accept_confirm", { nChanges: "{{ nChanges }}"})} + p(ng-if="!isAccept") #{translate("bulk_reject_confirm", { nChanges: "{{ nChanges }}"})} + .modal-footer() + button.btn.btn-default( + ng-click="cancel()" + ) + span #{translate("cancel")} + button.btn.btn-primary( + ng-click="confirm()" + ) + span #{translate("ok")} diff --git a/services/web/app/views/project/editor/source-editor.pug b/services/web/app/views/project/editor/source-editor.pug new file mode 100644 index 0000000000..7fc51ce40a --- /dev/null +++ b/services/web/app/views/project/editor/source-editor.pug @@ -0,0 +1,35 @@ +#editor( + ace-editor="editor", + ng-if="!editor.showRichText", + ng-show="!!editor.sharejs_doc && !editor.opening && multiSelectedCount === 0 && !editor.error_state", + theme="settings.editorTheme", + keybindings="settings.mode", + font-size="settings.fontSize", + auto-complete="settings.autoComplete", + auto-pair-delimiters="settings.autoPairDelimiters", + spell-check="!anonymous", + spell-check-language="project.spellCheckLanguage" + highlights="onlineUserCursorHighlights[editor.open_doc_id]" + show-print-margin="false", + sharejs-doc="editor.sharejs_doc", + last-updated="editor.last_updated", + cursor-position="editor.cursorPosition", + goto-line="editor.gotoLine", + resize-on="layout:main:resize,layout:pdf:resize,layout:review:resize,review-panel:toggle,layout:flat-screen:toggle", + annotations="pdf.logEntryAnnotations[editor.open_doc_id]", + read-only="!permissions.write", + file-name="editor.open_doc_name", + on-ctrl-enter="recompileViaKey", + on-save="recompileViaKey", + on-ctrl-j="toggleReviewPanel", + on-ctrl-shift-c="addNewCommentFromKbdShortcut", + on-ctrl-shift-a="toggleTrackChangesFromKbdShortcut", + syntax-validation="settings.syntaxValidation", + review-panel="reviewPanel", + events-bridge="reviewPanelEventsBridge" + track-changes= "editor.trackChanges", + doc-id="editor.open_doc_id" + renderer-data="reviewPanel.rendererData" + font-family="settings.fontFamily" + line-height="settings.lineHeight" +) diff --git a/services/web/app/views/project/editor/symbol-palette.pug b/services/web/app/views/project/editor/symbol-palette.pug new file mode 100644 index 0000000000..660aa78cc1 --- /dev/null +++ b/services/web/app/views/project/editor/symbol-palette.pug @@ -0,0 +1,2 @@ +if showSymbolPalette + symbol-palette(show="editor.showSymbolPalette" handle-select="editor.insertSymbol") diff --git a/services/web/app/views/project/importing.pug b/services/web/app/views/project/importing.pug new file mode 100644 index 0000000000..6d62114806 --- /dev/null +++ b/services/web/app/views/project/importing.pug @@ -0,0 +1,20 @@ +extends ../layout + +block vars + - var suppressNavbar = true + - var suppressFooter = true + - var suppressSkipToContent = true + - metadata.robotsNoindexNofollow = true + +block content + .editor(ng-controller="ImportingController").full-size + .loading-screen + .loading-screen-brand-container + .loading-screen-brand( + style="height: 20%;" + ng-style="{ 'height': state.load_progress + '%' }" + ) + h3.loading-screen-label #{translate("importing")} + span.loading-screen-ellip . + span.loading-screen-ellip . + span.loading-screen-ellip . diff --git a/services/web/app/views/project/invite/not-valid.pug b/services/web/app/views/project/invite/not-valid.pug new file mode 100644 index 0000000000..15f5599a85 --- /dev/null +++ b/services/web/app/views/project/invite/not-valid.pug @@ -0,0 +1,18 @@ +extends ../../layout + +block content + main.content.content-alt#main-content + .container + .row + .col-md-8.col-md-offset-2 + .card.project-invite-invalid + .page-header.text-centered + h1 #{translate("invite_not_valid")} + .row.text-center + .col-md-12 + p + | #{translate("invite_not_valid_description")}. + .row.text-center.actions + .col-md-12 + a.btn.btn-info(href="/project") #{translate("back_to_your_projects")} + diff --git a/services/web/app/views/project/invite/show.pug b/services/web/app/views/project/invite/show.pug new file mode 100644 index 0000000000..9b759bc76f --- /dev/null +++ b/services/web/app/views/project/invite/show.pug @@ -0,0 +1,30 @@ +extends ../../layout + +block content + main.content.content-alt#main-content + .container + .row + .col-md-8.col-md-offset-2 + .card.project-invite-accept + .page-header.text-centered + h1(ng-non-bindable) #{translate("user_wants_you_to_see_project", {username:owner.first_name, projectname:""})} + br + em(ng-non-bindable) #{project.name} + .row.text-center + .col-md-12 + p + | #{translate("accepting_invite_as")}  + em(ng-non-bindable) #{user.email} + .row + .col-md-12 + form.form( + name="acceptForm", + method="POST", + action="/project/"+invite.projectId+"/invite/token/"+invite.token+"/accept" + ) + input(name='_csrf', type='hidden', value=csrfToken) + input(name='token', type='hidden', value=invite.token) + .form-group.text-center + button.btn.btn-lg.btn-primary(type="submit") + | #{translate("join_project")} + .form-group.text-center diff --git a/services/web/app/views/project/list.pug b/services/web/app/views/project/list.pug new file mode 100644 index 0000000000..ea7cbc4b59 --- /dev/null +++ b/services/web/app/views/project/list.pug @@ -0,0 +1,57 @@ +extends ../layout + +block vars + - var suppressNavContentLinks = true + +block append meta + meta(name="ol-projects" data-type="json" content=projects) + meta(name="ol-tags" data-type="json" content=tags) + meta(name="ol-notifications" data-type="json" content=notifications) + meta(name="ol-notificationsInstitution" data-type="json" content=notificationsInstitution) + meta(name="ol-userAffiliations" data-type="json" content=userAffiliations) + meta(name="ol-userEmails" data-type="json" content=userEmails) + meta(name="ol-userHasNoSubscription" data-type="boolean" content=!!(settings.enableSubscriptions && !hasSubscription)) + meta(name="ol-allInReconfirmNotificationPeriods" data-type="json" content=allInReconfirmNotificationPeriods) + meta(name="ol-reconfirmedViaSAML" content=reconfirmedViaSAML) + +block content + + main.content.content-alt.project-list-page#main-content( + ng-controller="ProjectPageController" + role="main" + ) + .system-messages( + ng-cloak + ng-controller="SystemMessagesController" + ) + .system-message( + ng-repeat="message in messages" + ng-controller="SystemMessageController" + ng-hide="hidden" + ) + button(ng-hide="protected",ng-click="hide()").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} + .system-message-content(ng-bind-html="htmlContent") + + include ../translations/translation_message + + .project-list-content(event-tracking=settings.overleaf ? "loads_v2_dash" : "", onboard=settings.overleaf ? "true" : "", event-tracking-trigger=settings.overleaf ? "load" : "", event-tracking-mb="true", event-segmentation="{location: 'dash', v2_onboard: true}") + .project-list-row(ng-cloak) + .project-list-container.row(ng-if="projects.length > 0") + .project-list-sidebar-wrapper.col-md-2.col-xs-3 + aside.project-list-sidebar + include ./list/side-bar + + .project-list-main.col-md-10.col-xs-9 + include ./list/notifications + include ./list/project-list + + .project-list-empty.row(ng-if="projects.length === 0") + .project-list-empty-col.col-md-offset-2.col-md-8.col-md-offset-2.col-xs-8.col-xs-offset-2 + include ./list/empty-project-list + .row.row-spaced + .col-sm-12 + include ./list/notifications + + include ./list/modals diff --git a/services/web/app/views/project/list/empty-project-list.pug b/services/web/app/views/project/list/empty-project-list.pug new file mode 100644 index 0000000000..d5ce1dde0e --- /dev/null +++ b/services/web/app/views/project/list/empty-project-list.pug @@ -0,0 +1,46 @@ +.row.row-spaced + .col-xs-12 + .card.card-thin + div.welcome.text-centered(ng-cloak) + h2 #{translate("welcome_to_sl")} + p #{translate("new_to_latex_look_at")} + a(href="/templates") #{translate("templates").toLowerCase()} + | #{translate("or")} + a(href="/learn") #{translate("latex_help_guide")} + + + .row + .col-md-offset-4.col-md-4 + .dropdown.minimal-create-proj-dropdown(dropdown) + a.btn.btn-success.dropdown-toggle( + href="#", + data-toggle="dropdown", + dropdown-toggle + ) + | Create First Project + + ul.dropdown-menu.minimal-create-proj-dropdown-menu(role="menu") + li + a( + href, + ng-click="openCreateProjectModal()" + ) #{translate("blank_project")} + li + a( + href, + ng-click="openCreateProjectModal('example')" + ) #{translate("example_project")} + li + a( + href, + ng-click="openUploadProjectModal()" + ) #{translate("upload_project")} + != moduleIncludes("newProjectMenu", locals) + if (templates) + li.divider + li.dropdown-header #{translate("templates")} + each item in templates + li + a.menu-indent(href=item.url) #{translate(item.name)} + + diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug new file mode 100644 index 0000000000..d82a84e97c --- /dev/null +++ b/services/web/app/views/project/list/item.pug @@ -0,0 +1,136 @@ +td.project-list-table-name-cell + .project-list-table-name-container + input.project-list-table-select-item( + select-individual, + type="checkbox", + ng-model="project.selected" + stop-propagation="click" + aria-label=translate('select_project') + " '{{ project.name }}'" + ) + span.project-list-table-name + a.project-list-table-name-link( + ng-href="{{projectLink(project)}}" + stop-propagation="click" + ) {{project.name}} + span( + ng-controller="TagListController" + ) + .tag-label( + ng-repeat='tag in project.tags' + stop-propagation="click" + ) + button.label.label-default.tag-label-name( + ng-click="selectTag(tag)" + aria-label="Select tag {{ tag.name }}" + ) + i.fa.fa-circle( + aria-hidden="true" + ng-style="{ 'color': 'hsl({{ getHueForTagId(tag._id) }}, 70%, 45%)' }" + ) + | {{tag.name}} + button.label.label-default.tag-label-remove( + ng-click="removeProjectFromTag(project, tag)" + aria-label="Remove tag {{ tag.name }}" + ) + span(aria-hidden="true") × + +td.project-list-table-owner-cell + span.owner(ng-if='project.owner') {{getOwnerName(project)}} + |   + i.fa.fa-question-circle.small( + ng-if="hasGenericOwnerName()" + tooltip="This project is owned by a user who hasn’t yet migrated their account to Overleaf v2" + tooltip-append-to-body="true" + aria-hidden="true" + ) + span(ng-if="isLinkSharingProject(project)") + |   + i.fa.fa-link.small( + tooltip=translate("link_sharing") + tooltip-placement="right" + tooltip-append-to-body="true" + aria-label=translate("link_sharing") + ) + +td.project-list-table-lastupdated-cell + span.last-modified(tooltip="{{project.lastUpdated | formatDate}}") + | {{project.lastUpdated | fromNowDate}} + span(ng-show='project.lastUpdatedBy') + | + | #{translate('by')} + | {{getUserName(project.lastUpdatedBy)}} + + +td.project-list-table-actions-cell + div + button.btn.btn-link.action-btn( + ng-if="!(project.archived || project.trashed)" + aria-label=translate('copy'), + tooltip=translate('copy'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="openCloneProjectModal(project)" + ) + i.icon.fa.fa-files-o(aria-hidden="true") + button.btn.btn-link.action-btn( + aria-label=translate('download'), + tooltip=translate('download'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="download($event)" + ) + i.icon.fa.fa-cloud-download(aria-hidden="true") + button.btn.btn-link.action-btn( + ng-if="!project.archived" + aria-label=translate('archive'), + tooltip=translate('archive'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="archive($event)" + ) + i.icon.fa.fa-inbox(aria-hidden="true") + button.btn.btn-link.action-btn( + ng-if="!project.trashed" + aria-label=translate('trash'), + tooltip=translate('trash'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="trash($event)" + ) + i.icon.fa.fa-trash(aria-hidden="true") + button.btn.btn-link.action-btn( + ng-if="project.archived && !project.trashed" + aria-label=translate('unarchive'), + tooltip=translate('unarchive'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="unarchive($event)" + ) + i.icon.fa.fa-reply(aria-hidden="true") + button.btn.btn-link.action-btn( + ng-if="project.trashed && !project.archived" + aria-label=translate('untrash'), + tooltip=translate('untrash'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="untrash($event)" + ) + i.icon.fa.fa-reply(aria-hidden="true") + button.btn.btn-link.action-btn( + ng-if="project.trashed && !project.archived && !isOwner()" + aria-label=translate('leave'), + tooltip=translate('leave'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="leave($event)" + ) + i.icon.fa.fa-sign-out(aria-hidden="true") + button.btn.btn-link.action-btn( + ng-if="project.trashed && !project.archived && isOwner()" + aria-label=translate('delete'), + tooltip=translate('delete'), + tooltip-placement="top", + tooltip-append-to-body="true", + ng-click="delete($event)" + ) + i.icon.fa.fa-ban(aria-hidden="true") diff --git a/services/web/app/views/project/list/modals.pug b/services/web/app/views/project/list/modals.pug new file mode 100644 index 0000000000..e2d8465ed3 --- /dev/null +++ b/services/web/app/views/project/list/modals.pug @@ -0,0 +1,303 @@ +script(type='text/ng-template', id='newTagModalTemplate') + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="cancel()" + aria-label="Close" + ) + span(aria-hidden="true") × + h3 #{translate("create_new_folder")} + .modal-body + form(name="newTagForm", novalidate) + input.form-control( + type="text", + placeholder="New Folder Name", + required, + ng-model="inputs.newTagName", + on-enter="create()", + focus-on="open", + stop-propagation="click" + ) + .modal-footer + .modal-footer-left + span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")} + //- We stop propagation to stop the clicks from closing the + //- 'move to folder' menu. + button.btn.btn-default( + ng-click="cancel()" + stop-propagation="click" + ) #{translate("cancel")} + button.btn.btn-primary( + ng-disabled="newTagForm.$invalid || state.inflight" + ng-click="create()" + stop-propagation="click" + ) + span(ng-show="!state.inflight") #{translate("create")} + span(ng-show="state.inflight") #{translate("creating")}… + +script(type='text/ng-template', id='deleteTagModalTemplate') + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="cancel()" + aria-label="Close" + ) + span(aria-hidden="true") × + h3 #{translate("delete_folder")} + .modal-body + p #{translate("about_to_delete_folder")} + ul + li + strong {{tag.name}} + .modal-footer + .modal-footer-left + span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")} + button.btn.btn-default( + ng-click="cancel()" + ) #{translate("cancel")} + button.btn.btn-danger( + ng-click="delete()", + ng-disabled="state.inflight" + ) + span(ng-show="state.inflight") #{translate("deleting")}… + span(ng-show="!state.inflight") #{translate("delete")} + +script(type='text/ng-template', id='renameTagModalTemplate') + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="cancel()" + aria-label="Close" + ) + span(aria-hidden="true") × + h3 #{translate("rename_folder")} + .modal-body + form(name="renameTagForm", novalidate) + input.form-control( + type="text", + placeholder="Tag Name", + ng-model="inputs.tagName", + required, + on-enter="rename()", + focus-on="open" + ) + .modal-footer + .modal-footer-left + span.text-danger.error(ng-show="state.error") #{translate("generic_something_went_wrong")} + button.btn.btn-default(ng-click="cancel()") #{translate("cancel")} + button.btn.btn-primary( + ng-click="rename()", + ng-disabled="renameTagForm.$invalid || state.inflight" + ) + span(ng-show="!state.inflight") #{translate("rename")} + span(ng-show="state.inflight") #{translate("renaming")}… + +script(type='text/ng-template', id='renameProjectModalTemplate') + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="cancel()" + aria-label="Close" + ) + span(aria-hidden="true") × + h3 #{translate("rename_project")} + .modal-body + .alert.alert-danger(ng-show="state.error.message") {{state.error.message}} + .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} + form(name="renameProjectForm", novalidate) + input.form-control( + type="text", + placeholder="Project Name", + ng-model="inputs.projectName", + required, + on-enter="rename()", + focus-on="open" + ) + .modal-footer + button.btn.btn-default(ng-click="cancel()") #{translate("cancel")} + button.btn.btn-primary( + ng-click="rename()", + ng-disabled="renameProjectForm.$invalid || state.inflight" + ) + span(ng-show="!state.inflight") #{translate("rename")} + span(ng-show="state.inflight") #{translate("renaming")}… + +script(type='text/ng-template', id='cloneProjectModalTemplate') + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="cancel()" + aria-label="Close" + ) + span(aria-hidden="true") × + h3 #{translate("copy_project")} + .modal-body + .alert.alert-danger(ng-show="state.error.message") {{state.error.message === "invalid element name" ? "#{translate("invalid_element_name")}" : state.error.message}} + .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} + form(name="cloneProjectForm", novalidate) + .form-group + label #{translate("new_name")} + input.form-control( + type="text", + placeholder="New Project Name", + required, + ng-model="inputs.projectName", + on-enter="clone()", + focus-on="open" + ) + .modal-footer + button.btn.btn-default( + ng-disabled="state.inflight" + ng-click="cancel()" + ) #{translate("cancel")} + button.btn.btn-primary( + ng-disabled="cloneProjectForm.$invalid || state.inflight" + ng-click="clone()" + ) + span(ng-hide="state.inflight") #{translate("copy")} + span(ng-show="state.inflight") #{translate("copying")} … + +script(type='text/ng-template', id='newProjectModalTemplate') + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="cancel()" + aria-label="Close" + ) + span(aria-hidden="true") × + h3 #{translate("new_project")} + .modal-body + .alert.alert-danger(ng-show="state.error.message") {{state.error.message}} + .alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")} + form(novalidate, name="newProjectForm") + input.form-control( + type="text", + placeholder="Project Name", + required, + ng-model="inputs.projectName", + on-enter="create()", + focus-on="open" + ) + .modal-footer + button.btn.btn-default( + ng-disabled="state.inflight" + ng-click="cancel()" + ) #{translate("cancel")} + button.btn.btn-primary( + ng-disabled="newProjectForm.$invalid || state.inflight" + ng-click="create()" + ) + span(ng-hide="state.inflight") #{translate("create")} + span(ng-show="state.inflight") #{translate("creating")} … + +script(type='text/ng-template', id='archiveTrashLeaveOrDeleteProjectsModalTemplate') + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="cancel()" + aria-label="Close" + ) + span(aria-hidden="true") × + h3(ng-if="action === 'archive'") #{translate("archive_projects")} + h3(ng-if="action === 'trash'") #{translate("trash_projects")} + h3(ng-if="action === 'leave'") #{translate("leave_projects")} + h3(ng-if="action === 'delete'") #{translate("delete_projects")} + h3(ng-if="action === 'leaveOrDelete'") #{translate("delete_and_leave_projects")} + .modal-body + div(ng-if="action !== 'leaveOrDelete'") + p(ng-if="action === 'archive'") #{translate("about_to_archive_projects")} + p(ng-if="action === 'trash'") #{translate("about_to_trash_projects")} + p(ng-if="action === 'leave'") #{translate("about_to_leave_projects")} + p(ng-if="action === 'delete'") #{translate("about_to_delete_projects")} + ul + li(ng-repeat="project in projects | orderBy:'name'") + strong {{project.name}} + div(ng-if="action === 'archive'") + p #{translate("archiving_projects_wont_affect_collaborators")} + |   + a( + href="https://www.overleaf.com/blog/new-feature-using-archive-and-trash-to-keep-your-projects-organized" + target="_blank" + ) #{translate("find_out_more_nt")} + div(ng-if="action === 'trash'") + p #{translate("trashing_projects_wont_affect_collaborators")} + |   + a( + href="https://www.overleaf.com/blog/new-feature-using-archive-and-trash-to-keep-your-projects-organized" + target="_blank" + ) #{translate("find_out_more_nt")} + .project-action-alert.alert.alert-warning(ng-if="action === 'leave' || action === 'delete'") + i.fa.fa-fw.fa-exclamation-triangle + .project-action-alert-msg #{translate("this_action_cannot_be_undone")} + div(ng-if="action === 'leaveOrDelete'") + p #{translate("about_to_delete_projects")} + ul + li(ng-repeat="project in projects | filter:{accessLevel: 'owner'} | orderBy:'name'") + strong {{project.name}} + p #{translate("about_to_leave_projects")} + ul + li(ng-repeat="project in projects | filter:{accessLevel: '!owner'} | orderBy:'name'") + strong {{project.name}} + .project-action-alert.alert.alert-warning + i.fa.fa-fw.fa-exclamation-triangle + .project-action-alert-msg #{translate("this_action_cannot_be_undone")} + .modal-footer + button.btn.btn-default( + ng-click="cancel()" + ) #{translate("cancel")} + button.btn.btn-danger( + ng-click="confirm()" + ) #{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')} + 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( + type="button" + data-dismiss="modal" + ng-click="cancel()" + aria-label="Close" + ) + span(aria-hidden="true") × + h3 #{translate("upload_zipped_project")} + .modal-body( + fine-upload + endpoint="/project/new/upload" + template-id="qq-project-uploader-template" + multiple="false" + size-limit=zipFileSizeLimit + allowed-extensions="['zip']" + on-complete-callback="onComplete" + ) + .modal-footer + button.btn.btn-default(ng-click="cancel()") #{translate("cancel")} diff --git a/services/web/app/views/project/list/notifications.pug b/services/web/app/views/project/list/notifications.pug new file mode 100644 index 0000000000..bf47dbb655 --- /dev/null +++ b/services/web/app/views/project/list/notifications.pug @@ -0,0 +1,271 @@ +include ../../_mixins/reconfirm_affiliation + +.user-notifications(ng-controller="NotificationsController") + ul.list-unstyled( + ng-if="notifications.length > 0 && projects.length > 0", + ng-cloak + ) + li.notification-entry( + ng-repeat="notification in notifications" + ) + div(ng-switch="notification.templateKey" ng-hide="notification.hide") + .alert.alert-info( + ng-switch-when="notification_project_invite", + ng-controller="ProjectInviteNotificationController" + ) + .notification-body + span(ng-show="!notification.accepted") !{translate("notification_project_invite_message", { userName: "{{ userName }}", projectName: "{{ projectName }}" })} + span(ng-show="notification.accepted") !{translate("notification_project_invite_accepted_message", { projectName: "{{ projectName }}" })} + .notification-action + a.pull-right.btn.btn-sm.btn-info( + ng-show="notification.accepted", + href="/project/{{ notification.messageOpts.projectId }}" + ) #{translate("open_project")} + a.pull-right.btn.btn-sm.btn-info( + href, + ng-click="accept()", + ng-disabled="notification.inflight", + ng-show="!notification.accepted" + ) + span(ng-show="!notification.inflight") #{translate("join_project")} + span(ng-show="notification.inflight") + i.fa.fa-fw.fa-spinner.fa-spin(aria-hidden="true") + |   + | #{translate("joining")}… + .notification-close + button(ng-click="dismiss(notification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} + .alert.alert-info( + ng-switch-when="wfh_2020_upgrade_offer" + ) + .notification-body + span Important notice: Your free WFH2020 upgrade came to an end on June 30th 2020. We're still providing a number of special initiatives to help you continue collaborating throughout 2020. + .notification-action + a.pull-right.btn.btn-sm.btn-info(href="https://www.overleaf.com/events/wfh2020") View + .notification-close + button(ng-click="dismiss(notification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} + .alert.alert-info( + ng-switch-when="notification_ip_matched_affiliation" + ng-if="notification.messageOpts.ssoEnabled" + ) + .notification-body + | !{translate("looks_like_youre_at", {institutionName: '{{notification.messageOpts.university_name}}'}, ['strong'])} + br + | !{translate("you_can_now_log_in_sso", {}, ['strong'])} + br + | #{translate("link_institutional_email_get_started", {}, ['strong'])}  + a( + ng-href="{{notification.messageOpts.portalPath || 'https://www.overleaf.com/learn/how-to/Institutional_Login'}}" + ) #{translate("find_out_more_nt")} + .notification-action + a.pull-right.btn.btn-sm.btn-info( + ng-href=`{{samlInitPath}}?university_id={{notification.messageOpts.institutionId}}&auto=/project` + ) + | #{translate("link_account")} + .notification-close + button.btn-sm(ng-click="dismiss(notification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} + .alert.alert-info( + ng-switch-when="notification_ip_matched_affiliation" + ng-if="!notification.messageOpts.ssoEnabled" + ) + .notification-body + | !{translate("looks_like_youre_at", {institutionName: '{{notification.messageOpts.university_name}}'}, ['strong'])} + br + | !{translate("did_you_know_institution_providing_professional", {institutionName: '{{notification.messageOpts.university_name}}'}, ['strong'])} + br + | #{translate("add_email_to_claim_features")} + .notification-action + a.pull-right.btn.btn-sm.btn-info( + href="/user/settings" + ) #{translate("add_affiliation")} + .notification-close + button.btn-sm(ng-click="dismiss(notification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} + + .alert.alert-danger( + ng-switch-when="notification_tpds_file_limit" + ) + .notification-body + | Error: Your project + strong {{ notification.messageOpts.projectName }} + | has gone over the 2000 file limit using an integration (e.g. Dropbox or Github)
+ | Please decrease the size of your project to prevent further errors. + .notification-action + a.pull-right.btn.btn-sm.btn-info(href="/user/settings") + | Account Settings + .notification-close + button.btn-sm(ng-click="dismiss(notification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} + + .alert.alert-warning( + ng-switch-when="notification_dropbox_duplicate_project_names" + ) + .notification-body + p() + | !{translate("dropbox_duplicate_project_names", { projectName: '{{notification.messageOpts.projectName}}'}, ['strong'])} + p() + | !{translate("dropbox_duplicate_project_names_suggestion", {}, ['strong'])} + |   + a(href="/learn/how-to/Dropbox_Synchronization#Troubleshooting") #{translate("learn_more")} + |. + .notification-close + button.btn-sm(ng-click="dismiss(notification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} + + .alert.alert-info( + ng-switch-when="notification_dropbox_unlinked_due_to_lapsed_reconfirmation" + ) + .notification-body + if user.features.dropbox + | !{translate("dropbox_unlinked_premium_feature", {}, ['strong'])} + | !{translate("can_now_relink_dropbox", {}, [{name: 'a', attrs: {href: '/user/settings#dropboxSettings' }}])} + else + | !{translate("dropbox_unlinked_premium_feature", {}, ['strong'])} + | !{translate("confirm_affiliation_to_relink_dropbox")} + |   + a(href="/learn/how-to/Institutional_Email_Reconfirmation") #{translate("learn_more")} + .notification-close + button.btn-sm(ng-click="dismiss(notification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} + .alert.alert-info( + ng-switch-default + ) + span(ng-bind-html="notification.html").notification-body + .notification-close + button(ng-click="dismiss(notification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} + + ul.list-unstyled( + ng-if="notificationsInstitution.length > 0", + ng-cloak + ) + li.notification-entry( + ng-repeat="notification in notificationsInstitution" + ) + div(ng-switch="notification.templateKey" ng-hide="notification.hide") + .alert.alert-info( + ng-switch-when="notification_institution_sso_available" + ) + .notification-body + p !{translate("can_link_institution_email_acct_to_institution_acct", {appName: settings.appName, email: "{{notification.email}}", institutionName: "{{notification.institutionName}}"})} + div !{translate("doing_this_allow_log_in_through_institution", {appName: settings.appName})} + .notification-action + a.btn.btn-sm.btn-info(ng-href="{{samlInitPath}}?university_id={{notification.institutionId}}&auto=/project&email={{notification.email}}") + | #{translate('link_account')} + + .alert.alert-info( + ng-switch-when="notification_institution_sso_linked" + ) + .notification-body + div !{translate("account_has_been_link_to_institution_account", {appName: settings.appName, email: "{{notification.email}}", institutionName: "{{notification.institutionName}}"})} + .notification-close + button(ng-click="dismiss(notification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} + + .alert.alert-warning( + ng-switch-when="notification_institution_sso_non_canonical" + ) + .notification-body + div + i.fa.fa-fw.fa-exclamation-triangle(aria-hidden="true") + | !{translate("tried_to_log_in_with_email", {email: "{{notification.requestedEmail}}"})} !{translate("in_order_to_match_institutional_metadata_associated", {email: "{{notification.institutionEmail}}"})} + .notification-close + button(ng-click="dismiss(notification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} + + .alert.alert-info( + ng-switch-when="notification_institution_sso_already_registered" + ) + .notification-body + | !{translate("tried_to_register_with_email", {appName: settings.appName, email: "{{notification.email}}"})} + | #{translate("we_logged_you_in")} + .notification-action + a.btn.btn-sm.btn-info(href="/learn/how-to/Institutional_Login") + | #{translate("find_out_more")} + .notification-close + button(ng-click="dismiss(notification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} + + .alert.alert-danger(ng-switch-when="notification_institution_sso_error") + .notification-body + div + i.fa.fa-fw.fa-exclamation-triangle(aria-hidden="true") + |  #{translate("generic_something_went_wrong")}. + div(ng-if="notification.error.translatedMessage" ng-bind-html="notification.error.translatedMessage") + div(ng-else="notification.error.message") {{ notification.error.message}} + div(ng-if="notification.error.tryAgain") #{translate("try_again")}. + + .notification-close + button(ng-click="dismiss(notification)").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} + + ul.list-unstyled( + ng-controller="EmailNotificationController", + ng-cloak + ) + li.notification-entry( + ng-repeat="userEmail in userEmails", + ng-if="showConfirmEmail(userEmail) && projects.length > 0" + ) + .alert.alert-warning + .notification-body + div(ng-if="!userEmail.confirmationInflight") + | #{translate("please_confirm_email", {emailAddress: "{{ userEmail.email }}"})} + | + a( + href + ng-click="resendConfirmationEmail(userEmail)" + ) (#{translate('resend_confirmation_email')}) + div(ng-if="userEmail.confirmationInflight") + i.fa.fa-spinner.fa-spin(aria-hidden="true") + | + | #{translate('resending_confirmation_email')}… + div(ng-if="!userEmail.confirmationInflight && userEmail.error" aria-live="polite") + span(ng-if="userEmail.errorMessage") {{ userEmail.errorMessage }} + span(ng-if="!userEmail.errorMessage") #{translate('generic_something_went_wrong')} + + ui.list-unstyled(ng-controller="UserAffiliationsReconfirmController") + li.notification-entry( + ng-repeat="userEmail in allInReconfirmNotificationPeriods" + ) + .alert.alert-info() + +reconfirmAffiliationNotification('/project') + + li.notification-entry( + ng-repeat="userEmail in userEmails" + ng-if="userEmail.samlIdentifier && userEmail.samlIdentifier.providerId === reconfirmedViaSAML" + ) + +reconfirmedAffiliationNotification() + + - var hasPaidAffiliation = userAffiliations.some(affiliation => affiliation.licence && affiliation.licence !== 'free') + if settings.enableSubscriptions && !hasSubscription && !hasPaidAffiliation + ul.list-unstyled( + ng-controller="DismissableNotificationsController", + ng-cloak + ) + li.notification-entry( + ng-if="shouldShowNotification && projects.length > 0" + ) + .alert.alert-info + .notification-body + span To help you work from home throughout 2021, we're providing discounted plans and special initiatives. + .notification-action + a.pull-right.btn.btn-sm.btn-info(href="https://www.overleaf.com/events/wfh2021" event-tracking="Event-Pages" event-tracking-trigger="click" event-tracking-ga="WFH-Offer-Click" event-tracking-label="Dash-Banner") Upgrade + .notification-close + button(ng-click="dismiss()").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} diff --git a/services/web/app/views/project/list/project-list.pug b/services/web/app/views/project/list/project-list.pug new file mode 100644 index 0000000000..bfdb87871c --- /dev/null +++ b/services/web/app/views/project/list/project-list.pug @@ -0,0 +1,219 @@ +.row + .col-xs-12(ng-cloak) + form.project-search.form-horizontal(role="form") + .form-group.has-feedback.has-feedback-left.col-md-7.col-xs-12 + input.form-control.col-md-7.col-xs-12( + placeholder=translate('search_projects')+"…", + aria-label=translate('search_projects')+"…", + autofocus='autofocus', + ng-model="searchText.value", + focus-on='search:clear', + ng-keyup="searchProjects()" + ) + i.fa.fa-search.form-control-feedback-left(aria-hidden="true") + i.fa.fa-times.form-control-feedback( + ng-click="clearSearchText()", + style="cursor: pointer;", + ng-show="searchText.value.length > 0" + aria-hidden="true" + ) + button.sr-only( + type="button" + ng-click="clearSearchText()" + ng-show="searchText.value.length > 0" + ) #{translate('clear_search')} + + .project-tools(ng-cloak) + .btn-toolbar + .btn-group(ng-hide="selectedProjects.length < 1") + a.btn.btn-default( + href, + aria-label=translate('download'), + tooltip=translate('download'), + tooltip-placement="bottom", + tooltip-append-to-body="true", + ng-click="downloadSelectedProjects()" + ) + i.fa.fa-cloud-download(aria-hidden="true") + a.btn.btn-default( + href, + ng-if="filter !== 'archived'" + aria-label=translate("archive"), + tooltip=translate("archive"), + tooltip-placement="bottom", + tooltip-append-to-body="true", + ng-click="openArchiveProjectsModal()" + ) + i.fa.fa-inbox(aria-hidden="true") + a.btn.btn-default( + href, + ng-if="filter !== 'trashed'" + aria-label=translate("trash"), + tooltip=translate("trash"), + tooltip-placement="bottom", + tooltip-append-to-body="true", + ng-click="openTrashProjectsModal()" + ) + i.fa.fa-trash(aria-hidden="true") + .btn-group.dropdown( + ng-hide="selectedProjects.length < 1 || filter === 'archived' || filter === 'trashed'", + dropdown + ) + a.btn.btn-default.dropdown-toggle( + href, + data-toggle="dropdown", + dropdown-toggle, + tooltip=translate('add_to_folders'), + tooltip-append-to-body="true", + tooltip-placement="bottom" + ) + i.fa.fa-folder-open + | + span.caret + span.sr-only #{translate('add_to_folders')} + ul.dropdown-menu.dropdown-menu-right.js-tags-dropdown-menu.tags-dropdown-menu( + role="menu" + ng-controller="TagListController" + ) + li.dropdown-header #{translate("add_to_folder")} + li( + ng-repeat="tag in tags | orderBy:'name'", + ng-controller="TagDropdownItemController" + ) + a(href="#", ng-click="addOrRemoveProjectsFromTag()", stop-propagation="click") + i.fa( + ng-class="{\ + 'fa-check-square-o': areSelectedProjectsInTag == true,\ + 'fa-square-o': areSelectedProjectsInTag == false,\ + 'fa-minus-square-o': areSelectedProjectsInTag == 'partial'\ + }" + ) + span.sr-only Add or remove project from tag + | {{tag.name}} + li.divider + li + a(href, ng-click="openNewTagModal()", stop-propagation="click") #{translate("create_new_folder")} + + .btn-group.dropdown( + ng-hide="selectedProjects.length != 1 || filter === 'archived' || filter === 'trashed'", + dropdown + ) + a.btn.btn-default.dropdown-toggle( + href, + data-toggle="dropdown", + dropdown-toggle + ) #{translate("more")} + span.caret + ul.dropdown-menu.dropdown-menu-right(role="menu") + li(ng-show="getFirstSelectedProject().accessLevel == 'owner'") + a( + href, + ng-click="openRenameProjectModal()" + ) #{translate("rename")} + li + a( + href, + ng-click="openCloneProjectModal(getFirstSelectedProject())" + ) #{translate("make_copy")} + + .btn-group(ng-show="filter === 'archived' && selectedProjects.length > 0") + a.btn.btn-default( + href, + data-original-title=translate("unarchive"), + data-toggle="tooltip", + data-placement="bottom", + ng-click="unarchiveProjects(selectedProjects)" + ) #{translate("unarchive")} + + .btn-group(ng-show="filter === 'trashed' && selectedProjects.length > 0") + a.btn.btn-default( + href, + data-original-title=translate("untrash"), + data-toggle="tooltip", + data-placement="bottom", + ng-click="untrashProjects(selectedProjects)" + ) #{translate("untrash")} + + .btn-group(ng-show="filter === 'trashed' && selectedProjects.length > 0") + a.btn.btn-danger( + href, + ng-if="hasLeavableProjectsSelected() && !hasDeletableProjectsSelected()", + data-original-title=translate('leave'), + data-toggle="tooltip", + data-placement="bottom", + ng-click="openLeaveProjectsModal()" + ) #{translate("leave")} + + a.btn.btn-danger( + href, + ng-if="hasDeletableProjectsSelected() && !hasLeavableProjectsSelected()", + data-original-title=translate('delete'), + data-toggle="tooltip", + data-placement="bottom", + ng-click="openDeleteProjectsModal()" + ) #{translate("delete")} + + a.btn.btn-danger( + href, + ng-if="hasDeletableProjectsSelected() && hasLeavableProjectsSelected()", + data-original-title=translate('delete_and_leave'), + data-toggle="tooltip", + data-placement="bottom", + ng-click="openLeaveOrDeleteProjectsModal()" + ) #{translate("delete_and_leave")} + +.row.row-spaced + .col-xs-12 + .card.card-thin.project-list-card + ul.list-unstyled.project-list.structured-list( + select-all-list, + ng-if="projects.length > 0", + max-height="projectListHeight - 25", + ng-cloak + ) + table.project-list-table + tr.project-list-table-header-row + th.project-list-table-name-cell + .project-list-table-name-container + input.project-list-table-select-item( + select-all, + type="checkbox" + aria-label=translate('select_all_projects') + ) + span.header.clickable.project-list-table-name(ng-click="changePredicate('name')") #{translate("title")} + i.tablesort.fa(ng-class="getSortIconClass('name')" aria-hidden="true") + button.sr-only(ng-click="changePredicate('name')") Sort by #{translate("title")} + th.project-list-table-owner-cell + span.header.clickable(ng-click="changePredicate('ownerName')") #{translate("owner")} + i.tablesort.fa(ng-class="getSortIconClass('ownerName')" aria-hidden="true") + button.sr-only(ng-click="changePredicate('ownerName')") Sort by #{translate("owner")} + th.project-list-table-lastupdated-cell + span.header.clickable(ng-click="changePredicate('lastUpdated')") #{translate("last_modified")} + i.tablesort.fa(ng-class="getSortIconClass('lastUpdated')" aria-hidden="true") + button.sr-only(ng-click="changePredicate('lastUpdated')") Sort by #{translate("last_modified")} + th.project-list-table-actions-cell + span.header #{translate("actions")} + tr.project-list-table-row( + ng-repeat="project in visibleProjects | orderBy:getValueForCurrentPredicate:reverse:comparator", + ng-controller="ProjectListItemController" + select-row + ) + include ./item + tr( + ng-if="visibleProjects.length == 0", + ng-cloak + ) + td(colspan="4").project-list-table-no-projects-cell + span.small #{translate("no_projects")} + + div.welcome.text-centered(ng-if="projects.length == 0", ng-cloak) + h2 #{translate("welcome_to_sl")} + p #{translate("new_to_latex_look_at")} + a(href="/templates") #{translate("templates").toLowerCase()} + | #{translate("or")} + a(href="/learn") #{translate("latex_help_guide")} + | , + br + | #{translate("or_create_project_left")} + + diff --git a/services/web/app/views/project/list/side-bar.pug b/services/web/app/views/project/list/side-bar.pug new file mode 100644 index 0000000000..9f9fb6668b --- /dev/null +++ b/services/web/app/views/project/list/side-bar.pug @@ -0,0 +1,149 @@ +.dropdown( + dropdown + dropdown-append-to-body +) + a.btn.btn-primary.sidebar-new-proj-btn.dropdown-toggle( + href="#", + data-toggle="dropdown", + dropdown-toggle + ) + | #{translate("new_project")} + + ul.dropdown-menu(role="menu") + li + a( + href, + ng-click="openCreateProjectModal()" + ) #{translate("blank_project")} + li + a( + href, + ng-click="openCreateProjectModal('example')" + ) #{translate("example_project")} + li + a( + href, + ng-click="openUploadProjectModal()" + ) #{translate("upload_project")} + != moduleIncludes("newProjectMenu", locals) + + if portalTemplates.length > 0 + //- portalTemplates is set in ProjectController + li.divider + li.dropdown-header #{translate("institution")} #{translate("templates")} + for portal in portalTemplates + li + a.menu-indent( + href=portal.url + "#templates", + ng-non-bindable + ) #{portal.name} + + + + if (templates) + //- templates is an express local var, via settings.templateLinks + li.divider + li.dropdown-header #{translate("templates")} + each item in templates + li + a.menu-indent(href=item.url) #{translate(item.name)} + +.row-spaced(ng-if="projects.length > 0", ng-cloak) + ul.list-unstyled.folders-menu( + ng-controller="TagListController" + ) + li(ng-class="{active: (filter == 'all')}", ng-click="filterProjects('all')") + a(href) #{translate("all_projects")} + li(ng-class="{active: (filter == 'owned')}", ng-click="filterProjects('owned')") + a(href) #{translate("your_projects")} + li(ng-class="{active: (filter == 'shared')}", ng-click="filterProjects('shared')") + a(href) #{translate("shared_with_you")} + li(ng-class="{active: (filter == 'archived')}", ng-click="filterProjects('archived')") + a(href) #{settings.overleaf ? translate("archived_projects") : translate("deleted_projects")} + li(ng-class="{active: (filter == 'trashed')}", ng-click="filterProjects('trashed')") + a(href) #{translate("trashed_projects")} + li.separator + h2 #{translate("tags_slash_folders")} + li.tag(ng-cloak) + a.tag-name(href, ng-click="openNewTagModal()") + i.fa.fa-fw.fa-plus(aria-hidden="true") + span.name #{translate("new_folder")} + li.tag( + ng-repeat="tag in tags | orderBy:'name'", + ng-class="{active: tag.selected}", + ng-cloak, + ng-click="selectTag(tag)" + ) + a.tag-name(href) + i.icon.fa.fa-fw( + ng-class="{\ + 'fa-folder-open': tag.selected,\ + 'fa-folder': !tag.selected\ + }" + ng-style="{ 'color': 'hsl({{ getHueForTagId(tag._id) }}, 70%, 45%)' }" + aria-hidden="true" + ) + span.name {{tag.name}} + span.subdued ({{countProjectsForTag(tag)}}) + + span.dropdown.tag-menu(dropdown) + a.dropdown-toggle( + href="#", + data-toggle="dropdown", + dropdown-toggle, + stop-propagation="click" + ) + span.caret + ul.dropdown-menu.dropdown-menu-right( + role="menu" + ) + li + a(href, ng-click="renameTag(tag)", stop-propagation="click") + | #{translate("rename")} + li + a(href, ng-click="deleteTag(tag)", stop-propagation="click") + | #{translate("delete")} + li.tag.untagged( + ng-if="tags.length", + ng-cloak, + ng-click="selectUntagged()" + ng-class="{active: filter === 'untagged'}", + ) + a.tag-name(href) + span.name + | #{translate("uncategorized")} + span.subdued ({{ nUntagged }}) + +.row-spaced(ng-if="projects.length == 0", ng-cloak) + .first-project + div + i.fa.fa-arrow-up.fa-2x(aria-hidden="true") + div + strong #{translate("create_your_first_project")} + +if (isOverleaf) + span(ng-controller="LeftHandMenuPromoController", ng-cloak) + + .row-spaced#userProfileInformation(ng-if="hasProjects") + div(ng-hide="withAffiliations", ng-cloak) + hr + .text-centered.user-profile + p Are you affiliated with an institution? + + a.btn.btn-info( + href="/user/settings" + ) Add Affiliation + + //- .row-spaced(ng-if="hasProjects && userHasNoSubscription && !userOnPayingUniversity", ng-cloak).text-centered + //- hr + //- p.small #{translate("on_free_sl")} + //- p + //- a(href="/user/subscription/plans" ng-click="upgradeSubscription()").btn.btn-primary #{translate("upgrade")} + //- p.small.text-centered + //- | #{translate("or_unlock_features_bonus")} + //- a(href="/user/bonus" ng-click="share()") #{translate("sharing_sl")}. + .row-spaced(ng-if="hasProjects && userHasNoSubscription && !userOnPayingUniversity", ng-cloak).text-centered + hr + p.small To help you work from home throughout 2021, we're providing discounted plans and special initiatives. + p + a(href="https://www.overleaf.com/events/wfh2021" event-tracking="Event-Pages" event-tracking-trigger="click" event-tracking-ga="WFH-Offer-Click" event-tracking-label="Dash-Sidebar").btn.btn-primary Upgrade diff --git a/services/web/app/views/project/token/access.pug b/services/web/app/views/project/token/access.pug new file mode 100644 index 0000000000..a269a5183c --- /dev/null +++ b/services/web/app/views/project/token/access.pug @@ -0,0 +1,100 @@ +extends ../../layout + +block vars + - metadata = { viewport: true } + +block content + + script(type="template", id="overleaf-token-access-data")!= StringHelper.stringifyJsonForScript({ postUrl: postUrl, csrfToken: csrfToken}) + + div( + ng-controller="TokenAccessPageController", + ng-init="post()" + ) + .editor.full-size + div + |   + a(href="/project", style="font-size: 2rem; margin-left: 1rem; color: #ddd;") + i.fa.fa-arrow-left + + .loading-screen( + ng-show="mode == 'accessAttempt'" + ) + .loading-screen-brand-container + .loading-screen-brand() + + h3.loading-screen-label.text-center + | #{translate('join_project')} + span(ng-show="accessInFlight == true") + span.loading-screen-ellip . + span.loading-screen-ellip . + span.loading-screen-ellip . + + + .global-alerts.text-center(ng-cloak) + div(ng-show="accessError", ng-cloak) + br + div(ng-switch="accessError", ng-cloak) + div(ng-switch-when="not_found") + h4(aria-live="assertive") + | Project not found + + div(ng-switch-default) + .alert.alert-danger(aria-live="assertive") #{translate('token_access_failure')} + p + a(href="/") #{translate('home')} + + .loading-screen( + ng-show="mode == 'v1Import'" + ) + .container + .row + .col-sm-8.col-sm-offset-2 + h1.text-center + span(ng-if="v1ImportData.status != 'mustLogin'") Overleaf v1 Project + span(ng-if="v1ImportData.status == 'mustLogin'") Please Log In + img.v2-import__img( + src="/img/v1-import/v2-editor.png" + alt="The new V2 editor." + ) + + div(ng-if="v1ImportData.status == 'cannotImport'") + h2.text-center + | Cannot Access Overleaf v1 Project + p.text-center.row-spaced-small + | Please contact the project owner or + | + a(href="/contact") contact support + | + | for assistance. + + div(ng-if="v1ImportData.status == 'mustLogin'") + p.text-center.row-spaced-small + | You will need to log in to access this project. + + .row-spaced.text-center + a.btn.btn-primary( + href="/login?redir={{ currentPath() }}" + ) Log In To Access Project + + div(ng-if="v1ImportData.status == 'canDownloadZip'") + p.text-center.row-spaced.small + | #[strong() {{ getProjectName() }}] has not yet been moved into + | the new version of Overleaf. This project was created + | anonymously and therefore cannot be automatically imported. + | Please download a zip file of the project and upload that to + | continue editing it. If you would like to delete this project + | after you have made a copy, please contact support. + + .row-spaced.text-center + a.btn.btn-primary(ng-href="{{ buildZipDownloadPath(v1ImportData.projectId) }}") + | Download project zip file + + +block append foot-scripts + script(type="text/javascript", nonce=scriptNonce). + $(document).ready(function () { + setTimeout(function() { + $('.loading-screen-brand').css('height', '20%') + }, 500); + }); diff --git a/services/web/app/views/referal/bonus.pug b/services/web/app/views/referal/bonus.pug new file mode 100644 index 0000000000..67c202c471 --- /dev/null +++ b/services/web/app/views/referal/bonus.pug @@ -0,0 +1,169 @@ +extends ../layout + +block content + .content.content-alt + .container.bonus + .row + .col-md-8.col-md-offset-2 + .card + .container-fluid(ng-controller="BonusLinksController") + .row + .col-md-12 + .page-header + h1 #{translate("help_us_spread_word")}. + + + .row + .col-md-10.col-md-offset-1 + h2 #{translate("share_sl_to_get_rewards")} + + .row + .col-md-8.col-md-offset-2.bonus-banner + .bonus-top + + .row + .col-md-8.col-md-offset-2.bonus-banner + .title + a(href='https://twitter.com/share?text='+encodeURIComponent(translate("bonus_twitter_share_text"))+'&url='+encodeURIComponent(buildReferalUrl("t"))+'&counturl='+settings.social.twitter.counturl, target="_blank").twitter + i.fa.fa-fw.fa-2x.fa-twitter(aria-hidden="true") + | + | Tweet + + .row + .col-md-8.col-md-offset-2.bonus-banner + .title + a(href='#').facebook + i.fa.fa-fw.fa-2x.fa-facebook-square(aria-hidden="true") + | + | #{translate("post_on_facebook")} + + .row + .col-md-8.col-md-offset-2.bonus-banner + .title + a(href='mailto:?subject='+encodeURIComponent(translate("bonus_email_share_header"))+'&body='+encodeURIComponent(translate("bonus_email_share_body")+' ')+encodeURIComponent(buildReferalUrl("e")), title='Share by Email').email + i.fa.fa-fw.fa-2x.fa-envelope-open-o(aria-hidden="true") + | + | #{translate("email_us_to_your_friends")} + + .row + .col-md-8.col-md-offset-2.bonus-banner + .title + a(href='#link-modal', data-toggle="modal", ng-click="openLinkToUsModal()").link + i.fa.fa-fw.fa-2x.fa-globe(aria-hidden="true") + | + | #{translate("link_to_us")} + + .row + .col-md-10.col-md-offset-1.bonus-banner + h2.direct-link #{translate("direct_link")} + pre.text-centered #{buildReferalUrl("d")} + + .row.ab-bonus + .col-md-10.col-md-offset-1.bonus-banner + p.thanks !{translate("sl_gives_you_free_stuff_see_progress_below")} + .row.ab-bonus + .col-md-10.col-md-offset-1.bonus-banner(style="position: relative; height: 30px; margin-top: 20px;") + - for (var i = 0; i <= 10; i++) { + if (refered_user_count == i) + .number(style="left: "+i+"0%").active #{i} + else + .number(style="left: "+i+"0%") #{i} + - } + + .row.ab-bonus + .col-md-10.col-md-offset-1.bonus-banner + .progress + if (refered_user_count == 0) + div(style="text-align: center; padding: 4px;") #{translate("spread_the_word_and_fill_bar")} + .progress-bar.progress-bar-info(style="width: "+refered_user_count+"0%") + + .row.ab-bonus + .col-md-10.col-md-offset-1.bonus-banner(style="position: relative; height: 110px;") + .perk(style="left: 10%;", class = refered_user_count >= 1 ? "active" : "") #{translate("one_free_collab")} + .perk(style="left: 30%;", class = refered_user_count >= 3 ? "active" : "") #{translate("three_free_collab")} + .perk(style="left: 60%;", class = refered_user_count >= 6 ? "active" : "") #{translate("free_dropbox_and_history")} + #{translate("three_free_collab")} + .perk(style="left: 90%;", class = refered_user_count >= 9 ? "active" : "") #{translate("free_dropbox_and_history")} + #{translate("unlimited_collabs")} + .row   + + .row.ab-bonus + .col-md-10.col-md-offset-1.bonus-banner.bonus-status + if (refered_user_count == 0) + p.thanks !{translate("you_not_introed_anyone_to_sl")} + else if (refered_user_count == 1) + p.thanks !{translate("you_introed_small_number", {numberOfPeople: refered_user_count}, ['strong'])} + else + p.thanks !{translate("you_introed_high_number", {numberOfPeople: refered_user_count}, ['strong'])} + + script(type="text/ng-template", id="BonusLinkToUsModal") + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="cancel()" + aria-label="Close" + ) + span(aria-hidden="true") × + h3 #{translate("link_to_sl")} + .modal-body.modal-body-share.link-modal + + p #{translate("can_link_to_sl_with_html")} + p + textarea.col-md-12(readonly=true) + #{translate("bonus_share_link_text")} + + p #{translate("thanks")}! + + + .modal-footer() + button.btn.btn-default( + ng-click="cancel()", + ) + span #{translate("close")} + + +block append foot-scripts + script(type="text/javascript", nonce=scriptNonce). + $(document).ready(function () { + $.ajax({dataType: "script", cache: true, url: "//connect.facebook.net/en_US/all.js"}).done(function () { + window.fbAsyncInit = function() { + FB.init({appId: '#{settings.social.facebook.appId}', xfbml: true}); + } + }); + }); + + function postToFeed() { + // calling the API ... + var obj = { + method: 'feed', + redirect_uri: '#{settings.social.facebook.redirectUri}', + link: '!{buildReferalUrl("fb")}', + picture: '#{settings.social.facebook.picture}', + name: '#{translate("bonus_facebook_name").replace(/\'/g, "\\x27")}', + caption: '#{translate("bonus_facebook_caption").replace(/\'/g, "\\x27")}', + description: '#{translate("bonus_facebook_description").replace(/\'/g, "\\x27")}' + }; + + if (typeof FB !== "undefined" && FB !== null) { + FB.ui(obj); + } + } + + script(type="text/javascript", nonce=scriptNonce, src='//platform.twitter.com/widgets.js') + + script(type="text/javascript", nonce=scriptNonce). + $(function() { + $(".twitter").click(function() { + ga('send', 'event', 'referal-button', 'clicked', "twitter") + }); + $(".email").click(function() { + ga('send', 'event', 'referal-button', 'clicked', "email") + }); + $(".facebook").click(function(e) { + ga('send', 'event', 'referal-button', 'clicked', "facebook") + postToFeed() + e.preventDefault() + }); + $(".link").click(function() { + ga('send', 'event', 'referal-button', 'clicked', "direct-link") + }); + }); diff --git a/services/web/app/views/subscriptions/_modal_group_purchase.pug b/services/web/app/views/subscriptions/_modal_group_purchase.pug new file mode 100644 index 0000000000..e8da6d0b60 --- /dev/null +++ b/services/web/app/views/subscriptions/_modal_group_purchase.pug @@ -0,0 +1,56 @@ +script(type="text/ng-template", id="groupPlanModalPurchaseTemplate") + .modal-header + h3 Save 30% or more with a group license + .modal-body.plans + .container-fluid + .row + .col-md-6.text-center + .circle.circle-lg + | {{ displayPrice }} + span.small / year + br + span.circle-subtext For {{ selected.size }} users + ul.list-unstyled + li Each user will have access to: + li   + li(ng-if="selected.plan_code == 'collaborator'") + strong #{translate("collabs_per_proj", {collabcount:10})} + li(ng-if="selected.plan_code == 'professional'") + strong #{translate("unlimited_collabs")} + +features_premium + .col-md-6 + form.form + .form-group + label(for='plan_code') + | Plan + select.form-control(id="plan_code", ng-model="selected.plan_code") + option(ng-repeat="plan_code in options.plan_codes", value="{{plan_code.code}}") {{ plan_code.display }} + .form-group + label(for='size') + | Number of users + select.form-control(id="size", ng-model="selected.size") + option(ng-repeat="size in options.sizes", value="{{size}}") {{ size }} + .form-group + label(for='currency') + | Currency + select.form-control(id="currency", ng-model="selected.currency") + option(ng-repeat="currency in options.currencies", value="{{currency.code}}") {{ currency.display }} + .form-group + label(for='usage') + | Usage + select.form-control(id="usage", ng-model="selected.usage") + option(ng-repeat="usage in options.usages", value="{{usage.code}}") {{ usage.display }} + p.small.text-center.row-spaced-small(ng-show="selected.usage == 'educational'") + | The 40% educational discount can be used by students or faculty using Overleaf for teaching + p.small.text-center.row-spaced-small(ng-show="selected.usage == 'enterprise'") + | Save an additional 40% on groups of 10 or more with our educational discount + .modal-footer + .text-center + button.btn.btn-primary.btn-lg(ng-click="purchase()") Purchase Now + hr.thin + a( + href + ng-controller="ContactGeneralModal" + ng-click="openModal()" + ) Need more than 50 licenses? Please get in touch + diff --git a/services/web/app/views/subscriptions/_plans_faq.pug b/services/web/app/views/subscriptions/_plans_faq.pug new file mode 100644 index 0000000000..4fc39e49f4 --- /dev/null +++ b/services/web/app/views/subscriptions/_plans_faq.pug @@ -0,0 +1,34 @@ +.faq + .row.row-spaced-large + .col-md-12 + .page-header.plans-header.plans-subheader.text-centered + h2 FAQ + .row + .col-md-6 + h3 #{translate("faq_how_free_trial_works_question")} + p #{translate('faq_how_does_free_trial_works_answer', { appName:'{{settings.appName}}', len:'{{trial_len}}' })} + .col-md-6 + h3 #{translate('faq_change_plans_question')} + p #{translate('faq_change_plans_answer')} + .row + .col-md-6 + h3 #{translate('faq_do_collab_need_premium_question')} + p #{translate('faq_do_collab_need_premium_answer')} + .col-md-6 + h3 #{translate('faq_need_more_collab_question')} + p !{translate('faq_need_more_collab_answer', { referFriendsLink: translate('referring_your_friends') })} + .row + .col-md-6 + h3 #{translate('faq_purchase_more_licenses_question')} + p !{translate('faq_purchase_more_licenses_answer', { groupLink: translate('discounted_group_accounts') })}  + a(href='#groups', ng-click="openGroupPlanModal()") #{translate("get_in_touch_for_details")} + .col-md-6 + h3 #{translate('faq_monthly_or_annual_question')} + p #{translate('faq_monthly_or_annual_answer')} + .row + .col-md-6 + h3 #{translate('faq_how_to_pay_question')} + p #{translate('faq_how_to_pay_answer')} + .col-md-6 + h3 #{translate('faq_pay_by_invoice_question')} + p !{translate('faq_pay_by_invoice_answer', {}, [{ name: 'a', attrs: { href: "#pay-by-invoice", 'ng-controller': "ContactGeneralModal", 'ng-click': "openModal()" }}])} diff --git a/services/web/app/views/subscriptions/_plans_page_mixins.pug b/services/web/app/views/subscriptions/_plans_page_mixins.pug new file mode 100644 index 0000000000..7ed35769a9 --- /dev/null +++ b/services/web/app/views/subscriptions/_plans_page_mixins.pug @@ -0,0 +1,240 @@ +//- Buy Buttons +mixin btn_buy_collaborator(location) + a.btn.btn-primary( + ng-href="/user/subscription/new?planCode={{ getCollaboratorPlanCode() }}¤cy={{currencyCode}}&itm_campaign=plans&itm_content=" + location, + ng-click="signUpNowClicked('collaborator','" + location + "')" + ) + span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")} + span(ng-show="ui.view == 'annual'") #{translate("buy_now")} +mixin btn_buy_personal(location) + a.btn.btn-primary( + ng-href="/user/subscription/new?planCode={{ getPersonalPlanCode() }}¤cy={{currencyCode}}&itm_campaign=plans&itm_content=" + location, + ng-click="signUpNowClicked('personal','" + location + "')" + ) + span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")} + span(ng-show="ui.view == 'annual'") #{translate("buy_now")} +mixin btn_buy_free(location) + a.btn.btn-primary( + href="/register" + style=(getLoggedInUserId() === null ? "" : "visibility: hidden") + ng-click="signUpNowClicked('free','" + location + "')" + ) + span.text-capitalize #{translate('get_started_now')} +mixin btn_buy_professional(location) + a.btn.btn-primary( + ng-href="/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || planQueryString}}¤cy={{currencyCode}}&itm_campaign=plans&itm_content=" + location, + ng-click="signUpNowClicked('professional','" + location + "')" + ) + span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")} + span(ng-show="ui.view == 'annual'") #{translate("buy_now")} +mixin btn_buy_student(location, plan) + if plan == 'annual' + a.btn.btn-primary( + ng-href="/user/subscription/new?planCode=student-annual¤cy={{currencyCode}}&itm_campaign=plans&itm_content=" + location, + ng-click="signUpNowClicked('student-annual','" + location + "')" + ) #{translate("buy_now")} + else + //- planQueryString will contain _free_trial_7_days + a.btn.btn-primary( + ng-href="/user/subscription/new?planCode=student{{planQueryString}}¤cy={{currencyCode}}&itm_campaign=plans&itm_content=" + location, + ng-click="signUpNowClicked('student-monthly','" + location + "')" + ) #{translate("start_free_trial")} + +//- Cards +mixin card_student_annual(location) + .best-value + strong #{translate('best_value')} + .card-header + h2 #{translate("student")} (#{translate("annual")}) + h5.tagline #{translate('tagline_student_annual')} + .circle + span + +price_student_annual + +features_student(location, 'annual') +mixin card_student_monthly(location) + .card-header + h2 #{translate("student")} + h5.tagline #{translate('tagline_student_monthly')} + .circle + span + +price_student_monthly + +features_student(location, 'monthly') + +//- Features Lists, used within cards +mixin features_collaborator(location) + ul.list-unstyled + li + strong #{translate("collabs_per_proj", {collabcount:10})} + +features_premium + li + br + +btn_buy_collaborator(location) +mixin features_free(location) + ul.list-unstyled + li #{translate("one_collaborator")} + li(class="hidden-xs hidden-sm")   + li(class="hidden-xs hidden-sm")   + li(class="hidden-xs hidden-sm")   + li(class="hidden-xs hidden-sm")   + li(class="hidden-xs hidden-sm")   + li(class="hidden-xs hidden-sm")   + li + br + +btn_buy_free(location) +mixin features_personal(location) + ul.list-unstyled + li #{translate("one_collaborator")} + li   + li + strong #{translate('premium_features')} + li #{translate('sync_dropbox_github')} + li #{translate('full_doc_history')} + li + #{translate('more').toLowerCase()} + li(class="hidden-xs hidden-sm")   + li + br + +btn_buy_personal(location) +mixin features_premium + li   + li + strong #{translate('all_premium_features')} + li #{translate('sync_dropbox_github')} + li #{translate('full_doc_history')} + li #{translate('track_changes')} + li + #{translate('more').toLowerCase()} +mixin features_professional(location) + ul.list-unstyled + li + strong #{translate("unlimited_collabs")} + +features_premium + li + br + +btn_buy_professional(location) +mixin features_student(location, plan) + ul.list-unstyled + li + strong #{translate("collabs_per_proj", {collabcount:6})} + +features_premium + li + br + +btn_buy_student(location, plan) + +//- Prices +mixin price_personal + span(ng-if="ui.view == 'monthly'") + | {{plans[currencyCode]['personal']['monthly']}} + span.small /mo + span(ng-if="ui.view == 'annual'") + | {{plans[currencyCode]['personal']['annual']}} + span.small /yr +mixin price_collaborator + span(ng-if="ui.view == 'monthly'") + | {{plans[currencyCode]['collaborator']['monthly']}} + span.small /mo + span(ng-if="ui.view == 'annual'") + | {{plans[currencyCode]['collaborator']['annual']}} + span.small /yr +mixin price_professional + span(ng-if="ui.view == 'monthly'") + | {{plans[currencyCode]['professional']['monthly']}} + span.small /mo + span(ng-if="ui.view == 'annual'") + | {{plans[currencyCode]['professional']['annual']}} + span.small /yr +mixin price_student_annual + | {{plans[currencyCode]['student']['annual']}} + span.small /yr +mixin price_student_monthly + | {{plans[currencyCode]['student']['monthly']}} + span.small /mo + +//- UI Control +mixin currency_dropdown + .dropdown.currency-dropdown(dropdown) + a.btn.btn-default.dropdown-toggle( + href="#", + data-toggle="dropdown", + dropdown-toggle + ) + | {{currencyCode}} ({{plans[currencyCode]['symbol']}}) + span.caret + + ul.dropdown-menu.dropdown-menu-right.text-right(role="menu") + li(ng-repeat="(currency, value) in plans") + a( + href="#", + ng-click="changeCurreny($event, currency)" + ) {{currency}} ({{value['symbol']}}) +mixin plan_switch(location) + ul.nav.nav-pills + li(ng-class="{'active': ui.view == 'monthly'}") + a.btn.btn-default-outline( + href="#" + ng-click="switchToMonthly($event,'" + location + "')" + ) #{translate("monthly")} + li(ng-class="{'active': ui.view == 'annual'}") + a.btn.btn-default-outline( + href="#" + ng-click="switchToAnnual($event,'" + location + "')" + ) #{translate("annual")} + li(ng-class="{'active': ui.view == 'student'}") + a.btn.btn-default-outline( + href="#" + ng-click="switchToStudent($event,'" + location + "')" + ) #{translate("special_price_student")} + +mixin allCardsAndControls(controlsRowSpaced, listLocation) + - var location = listLocation ? 'card_' + listLocation : 'card' + .row.top-switch(class=controlsRowSpaced ? "row-spaced" : "") + .col-md-6.col-md-offset-3 + +plan_switch('card') + .col-md-2.text-right + +currency_dropdown + + .row + .col-md-10.col-md-offset-1 + .row + .card-group.text-centered(ng-if="ui.view == 'monthly' || ui.view == 'annual'") + .col-md-4 + .card.card-first + .card-header + h2 #{translate("personal")} + h5.tagline #{translate("tagline_personal")} + .circle + +price_personal + +features_personal(location) + .col-md-4 + .card.card-highlighted + .best-value + strong #{translate('best_value')} + .card-header + h2 #{translate("collaborator")} + h5.tagline #{translate("tagline_collaborator")} + .circle + +price_collaborator + +features_collaborator(location) + .col-md-4 + .card.card-last + .card-header + h2 #{translate("professional")} + h5.tagline #{translate("tagline_professional")} + .circle + +price_professional + +features_professional(location) + + .card-group.text-centered(ng-if="ui.view == 'student'") + .col-md-4 + .card.card-first + .card-header + h2 #{translate("free")} + h5.tagline #{translate("tagline_free")} + .circle #{translate("free")} + +features_free(location) + + .col-md-4 + .card.card-highlighted + +card_student_annual(location) + + .col-md-4 + .card.card-last + +card_student_monthly(location) diff --git a/services/web/app/views/subscriptions/_plans_page_tables.pug b/services/web/app/views/subscriptions/_plans_page_tables.pug new file mode 100644 index 0000000000..9db4d6b7ec --- /dev/null +++ b/services/web/app/views/subscriptions/_plans_page_tables.pug @@ -0,0 +1,117 @@ + +//- Features Tables +mixin table_premium + table.card.plans-table.plans-table-main + tr + th + th #{translate("free")} + th #{translate("personal")} + th #{translate("collaborator")} + .outer.outer-top + .outer-content + .best-value + strong #{translate('best_value')} + th #{translate("professional")} + + tr + td #{translate("price")} + td #{translate("free")} + td + +price_personal + td + +price_collaborator + td + +price_professional + + for feature in planFeatures + tr + td(event-tracking="features-table" event-tracking-trigger="hover" event-tracking-ga="subscription-funnel" event-tracking-label=`${feature.feature}`) + if feature.info + span(tooltip=translate(feature.info)) #{translate(feature.feature)} + else + | #{translate(feature.feature)} + for plan in feature.plans + td(ng-non-bindable) + if feature.value == 'str' + | #{plan} + else if plan + i.fa.fa-check(aria-hidden="true") + span.sr-only Feature included + else + i.fa.fa-times(aria-hidden="true") + span.sr-only Feature not included + + tr + td + td + +btn_buy_free('table') + td + +btn_buy_personal('table') + td + +btn_buy_collaborator('table') + .outer.outer-btm + .outer-content   + td + +btn_buy_professional('table') + +mixin table_cell_student(feature) + if feature.value == 'str' + | #{feature.student} + else if feature.student + i.fa.fa-check(aria-hidden="true") + span.sr-only Feature included + else + i.fa.fa-times(aria-hidden="true") + span.sr-only Feature not included + +mixin table_student + table.card.plans-table.plans-table-student + tr + th + th #{translate("free")} + th #{translate("student")} (#{translate("annual")}) + .outer.outer-top + .outer-content + .best-value + strong Best Value + th #{translate("student")} + + tr + td #{translate("price")} + td #{translate("free")} + td + +price_student_annual + td + +price_student_monthly + + for feature in planFeatures + tr + td(event-tracking="plans-page-table" event-tracking-trigger="hover" event-tracking-ga="subscription-funnel" event-tracking-label=`${feature.feature}`) + if feature.info + span(tooltip=translate(feature.info)) #{translate(feature.feature)} + else + | #{translate(feature.feature)} + td(ng-non-bindable) + if feature.value == 'str' + | #{feature.plans.free} + else if feature.plans.free + i.fa.fa-check(aria-hidden="true") + span.sr-only Feature included + else + i.fa.fa-times(aria-hidden="true") + span.sr-only Feature included + td(ng-non-bindable) + +table_cell_student(feature) + td(ng-non-bindable) + +table_cell_student(feature) + + tr + td + td + +btn_buy_free('table') + td + +btn_buy_student('table', 'annual') + .outer.outer-btm + .outer-content   + td + +btn_buy_student('table', 'monthly') diff --git a/services/web/app/views/subscriptions/_plans_quotes.pug b/services/web/app/views/subscriptions/_plans_quotes.pug new file mode 100644 index 0000000000..e7c6978a48 --- /dev/null +++ b/services/web/app/views/subscriptions/_plans_quotes.pug @@ -0,0 +1,25 @@ +.row.row-spaced-large + .col-md-12 + .page-header.plans-header.plans-subheader.text-centered + h2 #{translate('in_good_company')} +.row + .col-md-6 + div + .row + .col-md-3 + .circle-img + img(src=buildImgPath('advocates/schultz.jpg') alt="Kevin Schultz") + .col-md-9 + blockquote + p It is the ability to collaborate very easily that drew me to Overleaf. + footer Kevin Schultz, Assistant Professor of Physics, Hartwick College + .col-md-6 + div + .row + .col-md-3 + .circle-img + img(src=buildImgPath('advocates/dagoret-campagne.jpg') alt="Dr Sylvie Dagoret-Campagne") + .col-md-9 + blockquote + p Overleaf is a great educational tool for publishing scientific documents. + footer Dr Sylvie Dagoret-Campagne, Director of Research at CNRS, University of Paris-Saclay \ No newline at end of file diff --git a/services/web/app/views/subscriptions/_price_exceptions.pug b/services/web/app/views/subscriptions/_price_exceptions.pug new file mode 100644 index 0000000000..be0018db61 --- /dev/null +++ b/services/web/app/views/subscriptions/_price_exceptions.pug @@ -0,0 +1,9 @@ +p + i * !{translate("subject_to_additional_vat")} + +if (personalSubscription.recurly.activeCoupons.length > 0) + i * !{translate("coupons_not_included")}: + ul + each coupon in personalSubscription.recurly.activeCoupons + li + i= coupon.description || coupon.name diff --git a/services/web/app/views/subscriptions/canceled_subscription.pug b/services/web/app/views/subscriptions/canceled_subscription.pug new file mode 100644 index 0000000000..5d8c6e4bea --- /dev/null +++ b/services/web/app/views/subscriptions/canceled_subscription.pug @@ -0,0 +1,15 @@ +extends ../layout + +block content + main.content.content-alt#main-content + .container + .row + .col-md-8.col-md-offset-2 + .card(ng-cloak) + .page-header + h2 #{translate("subscription_canceled")} + .alert.alert-info + p #{translate("to_modify_your_subscription_go_to")} + a(href="/user/subscription") #{translate("manage_subscription")}. + p + a.btn.btn-primary(href="/project") < #{translate("back_to_your_projects")} diff --git a/services/web/app/views/subscriptions/dashboard.pug b/services/web/app/views/subscriptions/dashboard.pug new file mode 100644 index 0000000000..574e902548 --- /dev/null +++ b/services/web/app/views/subscriptions/dashboard.pug @@ -0,0 +1,69 @@ +extends ../layout + +include ./dashboard/_team_name_mixin + +block head-scripts + script(type="text/javascript", nonce=scriptNonce, src="https://js.recurly.com/v4/recurly.js") + +block append meta + meta(name="ol-managedInstitutions", data-type="json", content=managedInstitutions) + meta(name="ol-planCodesChangingAtTermEnd", data-type="json", content=plans.planCodesChangingAtTermEnd) + if (personalSubscription && personalSubscription.recurly) + meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey) + meta(name="ol-subscription" data-type="json" content=personalSubscription) + meta(name="ol-recomendedCurrency" content=personalSubscription.recurly.currency) + +block content + main.content.content-alt#main-content(ng-cloak) + .container + .row + .col-md-8.col-md-offset-2 + if (fromPlansPage) + .alert.alert-warning + p You already have a subscription + .card + .page-header + h1 #{translate("your_subscription")} + + -var hasDisplayedSubscription = false + if (personalSubscription) + -hasDisplayedSubscription = true + include ./dashboard/_personal_subscription + + if (managedGroupSubscriptions && managedGroupSubscriptions.length > 0) + include ./dashboard/_managed_groups + + if (managedInstitutions && managedInstitutions.length > 0) + include ./dashboard/_managed_institutions + + if (managedPublishers && managedPublishers.length > 0) + include ./dashboard/_managed_publishers + + if (memberGroupSubscriptions && memberGroupSubscriptions.length > 0) + -hasDisplayedSubscription = true + include ./dashboard/_group_memberships + + if (confirmedMemberAffiliations && confirmedMemberAffiliations.length > 0) + include ./dashboard/_institution_memberships + + if (v1SubscriptionStatus) + include ./dashboard/_v1_subscription_status + + if (!hasDisplayedSubscription) + if (hasSubscription) + -hasDisplayedSubscription = true + p(ng-non-bindable) You're on an #{settings.appName} Paid plan. Contact + a(href="mailto:support@overleaf.com") support@overleaf.com + | to find out more. + else + if (subscriptionCopy === 'new') + p(ng-non-bindable) You are on the #{settings.appName} Free plan. Upgrade to access these Premium Features: + ul + li Invite more collaborators + for feature in ['realtime_track_changes', 'full_doc_history', 'reference_search', 'reference_sync', 'dropbox_integration_lowercase', 'github_integration_lowercase', 'priority_support'] + li #{translate(feature)} + a(ng-controller="UpgradeSubscriptionController" href="/user/subscription/plans" ng-click="upgradeSubscription()").btn.btn-primary Upgrade now + else + p(ng-non-bindable) You're on the #{settings.appName} Free plan. + | + a(ng-controller="UpgradeSubscriptionController" href="/user/subscription/plans" ng-click="upgradeSubscription()").btn.btn-primary Upgrade now diff --git a/services/web/app/views/subscriptions/dashboard/_change_plans_mixins.pug b/services/web/app/views/subscriptions/dashboard/_change_plans_mixins.pug new file mode 100644 index 0000000000..c0078ccac0 --- /dev/null +++ b/services/web/app/views/subscriptions/dashboard/_change_plans_mixins.pug @@ -0,0 +1,28 @@ +mixin printPlan(plan) + if (!plan.hideFromUsers) + tr(ng-controller="ChangePlanFormController", ng-init="plan="+JSON.stringify(plan)) + td + strong(ng-non-bindable) #{plan.name} + td + if (plan.annual) + | {{price}} / #{translate("year")} + else + | {{price}} / #{translate("month")} + td + if (typeof(personalSubscription.planCode) != "undefined" && plan.planCode == personalSubscription.planCode.split("_")[0]) + if (personalSubscription.pendingPlan) + form + input(type="hidden", ng-model="plan_code", name="plan_code", value=plan.planCode) + input(type="submit", ng-click="cancelPendingPlanChange()", value=translate("keep_current_plan")).btn.btn-success + else + button.btn.disabled #{translate("your_plan")} + else if (personalSubscription.pendingPlan && typeof(personalSubscription.pendingPlan.planCode) != "undefined" && plan.planCode == personalSubscription.pendingPlan.planCode.split("_")[0]) + button.btn.disabled #{translate("your_new_plan")} + else + form + input(type="hidden", ng-model="plan_code", name="plan_code", value=plan.planCode) + input(type="submit", ng-click="changePlan()", value=translate("change_to_this_plan")).btn.btn-success + +mixin printPlans(plans) + each plan in plans + +printPlan(plan) diff --git a/services/web/app/views/subscriptions/dashboard/_group_memberships.pug b/services/web/app/views/subscriptions/dashboard/_group_memberships.pug new file mode 100644 index 0000000000..2c0e907e5e --- /dev/null +++ b/services/web/app/views/subscriptions/dashboard/_group_memberships.pug @@ -0,0 +1,32 @@ +div(ng-controller="GroupMembershipController") + each groupSubscription in memberGroupSubscriptions + if (user._id+'' != groupSubscription.admin_id._id+'') + div + p + | You are a member of + | + +teamName(groupSubscription) + if (groupSubscription.teamNotice && groupSubscription.teamNotice != '') + p + //- Team notice is sanitized in SubscriptionViewModelBuilder + em(ng-non-bindable) !{groupSubscription.teamNotice} + span + button.btn.btn-danger.text-capitalise(ng-click="removeSelfFromGroup('"+groupSubscription._id+"')") #{translate("leave_group")} + hr + +script(type='text/ng-template', id='LeaveGroupModalTemplate') + .modal-header + h3 #{translate("leave_group")} + .modal-body + p #{translate("sure_you_want_to_leave_group")} + .modal-footer + button.btn.btn-default( + ng-disabled="inflight" + ng-click="cancel()" + ) #{translate("cancel")} + button.btn.btn-danger( + ng-disabled="state.inflight" + ng-click="confirmLeaveGroup()" + ) + span(ng-hide="inflight") #{translate("leave_now")} + span(ng-show="inflight") #{translate("processing")}… diff --git a/services/web/app/views/subscriptions/dashboard/_institution_memberships.pug b/services/web/app/views/subscriptions/dashboard/_institution_memberships.pug new file mode 100644 index 0000000000..abe80a0cc3 --- /dev/null +++ b/services/web/app/views/subscriptions/dashboard/_institution_memberships.pug @@ -0,0 +1,13 @@ +each affiliation in confirmedMemberAffiliations + if (affiliation.licence && affiliation.licence != 'free') + -hasDisplayedSubscription = true + p + | You have a + | + strong(ng-non-bindable) Professional + | + | #{settings.appName} account as a confirmed member of + | + strong(ng-non-bindable)= affiliation.institution.name + hr + diff --git a/services/web/app/views/subscriptions/dashboard/_managed_groups.pug b/services/web/app/views/subscriptions/dashboard/_managed_groups.pug new file mode 100644 index 0000000000..fd7c354db7 --- /dev/null +++ b/services/web/app/views/subscriptions/dashboard/_managed_groups.pug @@ -0,0 +1,24 @@ +each managedGroupSubscription in managedGroupSubscriptions + p + | You are a manager of + | + +teamName(managedGroupSubscription) + p + a.btn.btn-primary(href="/manage/groups/" + managedGroupSubscription._id + "/members") + i.fa.fa-fw.fa-users + |   + | Manage members + |   + p + a(href="/manage/groups/" + managedGroupSubscription._id + "/managers") + i.fa.fa-fw.fa-users + |   + | Manage group managers + |   + p + a(href="/metrics/groups/" + managedGroupSubscription._id) + i.fa.fa-fw.fa-line-chart + |   + | View metrics + + hr diff --git a/services/web/app/views/subscriptions/dashboard/_managed_institutions.pug b/services/web/app/views/subscriptions/dashboard/_managed_institutions.pug new file mode 100644 index 0000000000..ad868d2664 --- /dev/null +++ b/services/web/app/views/subscriptions/dashboard/_managed_institutions.pug @@ -0,0 +1,27 @@ +each institution in managedInstitutions + p + | You are a manager of + | + strong(ng-non-bindable)= institution.name + p + a.btn.btn-primary(href="/metrics/institutions/" + institution.v1Id) + i.fa.fa-fw.fa-line-chart + |   + | View metrics + p + a(href="/institutions/" + institution.v1Id + "/hub") + i.fa.fa-fw.fa-user-circle + |   + | View hub + p + a(href="/manage/institutions/" + institution.v1Id + "/managers") + i.fa.fa-fw.fa-users + |   + | Manage institution managers + div(ng-controller="MetricsEmailController", ng-cloak) + p + span Monthly metrics emails: + a(href ng-bind-html="institutionEmailSubscription('"+institution.v1Id+"')" ng-show="!subscriptionChanging" ng-click="changeInstitutionalEmailSubscription('"+institution.v1Id+"')") + span(ng-show="subscriptionChanging") + i.fa.fa-spin.fa-refresh(aria-hidden="true") + hr diff --git a/services/web/app/views/subscriptions/dashboard/_managed_publishers.pug b/services/web/app/views/subscriptions/dashboard/_managed_publishers.pug new file mode 100644 index 0000000000..6a8fcfbc55 --- /dev/null +++ b/services/web/app/views/subscriptions/dashboard/_managed_publishers.pug @@ -0,0 +1,16 @@ +each publisher in managedPublishers + p + | You are a manager of + | + strong(ng-non-bindable)= publisher.name + p + a(href="/publishers/" + publisher.slug + "/hub") + i.fa.fa-fw.fa-user-circle + |   + | View hub + p + a(href="/manage/publishers/" + publisher.slug + "/managers") + i.fa.fa-fw.fa-users + |   + | Manage publisher managers + hr diff --git a/services/web/app/views/subscriptions/dashboard/_personal_subscription.pug b/services/web/app/views/subscriptions/dashboard/_personal_subscription.pug new file mode 100644 index 0000000000..e0a7501d92 --- /dev/null +++ b/services/web/app/views/subscriptions/dashboard/_personal_subscription.pug @@ -0,0 +1,7 @@ +if (personalSubscription.recurly) + include ./_personal_subscription_recurly + include ./_personal_subscription_recurly_sync_email +else + include ./_personal_subscription_custom + +hr diff --git a/services/web/app/views/subscriptions/dashboard/_personal_subscription_custom.pug b/services/web/app/views/subscriptions/dashboard/_personal_subscription_custom.pug new file mode 100644 index 0000000000..ec74cf4cd1 --- /dev/null +++ b/services/web/app/views/subscriptions/dashboard/_personal_subscription_custom.pug @@ -0,0 +1,6 @@ +p + | Please + | + a(href="/contact") contact support + | + | to make changes to your plan diff --git a/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly.pug b/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly.pug new file mode 100644 index 0000000000..06aae4fc5f --- /dev/null +++ b/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly.pug @@ -0,0 +1,132 @@ +div(ng-controller="RecurlySubscriptionController") + div(ng-show="!showCancellation") + if (personalSubscription.recurly.account.has_past_due_invoice && personalSubscription.recurly.account.has_past_due_invoice._ == 'true') + .alert.alert-danger #{translate("account_has_past_due_invoice_change_plan_warning")} + |   + a(href=personalSubscription.recurly.accountManagementLink, target="_blank") #{translate("view_your_invoices")}. + case personalSubscription.recurly.state + when "active" + p !{translate("currently_subscribed_to_plan", {planName: personalSubscription.plan.name}, ['strong'])} + if (personalSubscription.pendingPlan) + if (personalSubscription.pendingPlan.name != personalSubscription.plan.name) + | + | !{translate("your_plan_is_changing_at_term_end", {pendingPlanName: personalSubscription.pendingPlan.name}, ['strong'])} + if (personalSubscription.recurly.pendingAdditionalLicenses > 0 || personalSubscription.recurly.additionalLicenses > 0) + | + | !{translate("pending_additional_licenses", {pendingAdditionalLicenses: personalSubscription.recurly.pendingAdditionalLicenses, pendingTotalLicenses: personalSubscription.recurly.pendingTotalLicenses}, ['strong', 'strong'])} + else if (personalSubscription.recurly.additionalLicenses > 0) + | + | !{translate("additional_licenses", {additionalLicenses: personalSubscription.recurly.additionalLicenses, totalLicenses: personalSubscription.recurly.totalLicenses}, ['strong', 'strong'])} + |   + a(href, ng-click="switchToChangePlanView()", ng-if="showChangePlanButton") !{translate("change_plan")}. + if (personalSubscription.pendingPlan) + p #{translate("want_change_to_apply_before_plan_end")} + if (personalSubscription.recurly.trialEndsAtFormatted && personalSubscription.recurly.trial_ends_at > Date.now()) + p You're on a free trial which ends on #{personalSubscription.recurly.trialEndsAtFormatted} + p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount: personalSubscription.recurly.price, collectionDate: personalSubscription.recurly.nextPaymentDueAt}, ['strong', 'strong'])} + include ./../_price_exceptions + p.pull-right + p + a(href=personalSubscription.recurly.billingDetailsLink, target="_blank").btn.btn-info #{translate("update_your_billing_details")} + |   + a(href=personalSubscription.recurly.accountManagementLink, target="_blank").btn.btn-info #{translate("view_your_invoices")} + |   + a(href, ng-click="switchToCancellationView()", ng-hide="recurlyLoadError").btn.btn-danger !{translate("cancel_your_subscription")} + when "canceled" + p !{translate("currently_subscribed_to_plan", {planName: personalSubscription.plan.name}, ['strong'])} + p !{translate("subscription_canceled_and_terminate_on_x", {terminateDate: personalSubscription.recurly.nextPaymentDueAt}, ['strong'])} + p + a(href=personalSubscription.recurly.accountManagementLink, target="_blank").btn.btn-info #{translate("view_your_invoices")} + p: form(action="/user/subscription/reactivate",method="post") + input(type="hidden", name="_csrf", value=csrfToken) + input(type="submit",value="Reactivate your subscription").btn.btn-success + when "expired" + p !{translate("your_subscription_has_expired")} + p + a(href=personalSubscription.recurly.accountManagementLink, target="_blank").btn.btn-info #{translate("view_your_invoices")} + |   + a(href="/user/subscription/plans").btn.btn-success !{translate("create_new_subscription")} + default + p !{translate("problem_with_subscription_contact_us")} + + .alert.alert-warning(ng-show="recurlyLoadError") + strong #{translate('payment_provider_unreachable_error')} + + include ./_change_plans_mixins + div(ng-show="showChangePlan", ng-cloak) + h2 !{translate("change_plan")} + p: table.table + tr + th !{translate("name")} + th !{translate("price")} + th + +printPlans(plans.studentAccounts) + +printPlans(plans.individualMonthlyPlans) + +printPlans(plans.individualAnnualPlans) + + + .div(ng-controller="RecurlyCancellationController", ng-show="showCancellation").text-center + p + strong #{translate("wed_love_you_to_stay")} + + div(ng-show="showExtendFreeTrial") + p !{translate("have_more_days_to_try", {days:14})} + p + button(type="submit", ng-click="extendTrial()", ng-disabled='inflight').btn.btn-success #{translate("ill_take_it")} + p + a(href, ng-click="cancelSubscription()", ng-disabled='inflight') #{translate("no_thanks_cancel_now")} + + div(ng-show="showDowngradeToStudent") + div(ng-controller="ChangePlanFormController") + p !{translate("interested_in_cheaper_plan",{price:'{{studentPrice}}'})} + p + button(type="submit", ng-click="downgradeToStudent()", ng-disabled='inflight').btn.btn-success #{translate("yes_move_me_to_student_plan")} + p + a(href, ng-click="cancelSubscription()", ng-disabled='inflight') #{translate("no_thanks_cancel_now")} + + div(ng-show="showBasicCancel") + p + a(href, ng-click="switchToDefaultView()").btn.btn-info #{translate("i_want_to_stay")} + p + a(href, ng-click="cancelSubscription()", ng-disabled='inflight').btn.btn-primary #{translate("cancel_my_account")} + +script(type='text/ng-template', id='confirmChangePlanModalTemplate') + .modal-header + h3 #{translate("change_plan")} + .modal-body + .alert.alert-warning(ng-show="genericError") + strong #{translate("generic_something_went_wrong")}. #{translate("try_again")}. #{translate("generic_if_problem_continues_contact_us")}. + p !{translate("sure_you_want_to_change_plan", {planName: '{{plan.name}}'}, ['strong'])} + div(ng-show="planChangesAtTermEnd") + p #{translate("existing_plan_active_until_term_end")} + p #{translate("want_change_to_apply_before_plan_end")} + .modal-footer + button.btn.btn-default( + ng-disabled="inflight" + ng-click="cancel()" + ) #{translate("cancel")} + button.btn.btn-success( + ng-disabled="state.inflight" + ng-click="confirmChangePlan()" + ) + span(ng-hide="inflight") #{translate("change_plan")} + span(ng-show="inflight") #{translate("processing")}… + +script(type='text/ng-template', id='cancelPendingPlanChangeModalTemplate') + .modal-header + h3 #{translate("change_plan")} + .modal-body + .alert.alert-warning(ng-show="genericError") + strong #{translate("generic_something_went_wrong")}. #{translate("try_again")}. #{translate("generic_if_problem_continues_contact_us")}. + p !{translate("sure_you_want_to_cancel_plan_change", {planName: '{{plan.name}}'}, ['strong'])} + .modal-footer + button.btn.btn-default( + ng-disabled="inflight" + ng-click="cancel()" + ) #{translate("cancel")} + button.btn.btn-success( + ng-disabled="state.inflight" + ng-click="confirmCancelPendingPlanChange()" + ) + span(ng-hide="inflight") #{translate("revert_pending_plan_change")} + span(ng-show="inflight") #{translate("processing")}… \ No newline at end of file diff --git a/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly_sync_email.pug b/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly_sync_email.pug new file mode 100644 index 0000000000..3d58b2ab4a --- /dev/null +++ b/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly_sync_email.pug @@ -0,0 +1,18 @@ +-if (user.email !== personalSubscription.recurly.account.email) + div + hr + form(async-form="updateAccountEmailAddress", name="updateAccountEmailAddress", action='/user/subscription/account/email', method="POST") + input(name='_csrf', type='hidden', value=csrfToken) + .form-group + form-messages(for="updateAccountEmailAddress") + .alert.alert-success(ng-show="updateAccountEmailAddress.response.success") + | #{translate('recurly_email_updated')} + div(ng-hide="updateAccountEmailAddress.response.success") + p(ng-non-bindable) !{translate("recurly_email_update_needed", { recurlyEmail: personalSubscription.recurly.account.email, userEmail: user.email }, ['em', 'em'])} + .actions + button.btn-primary.btn( + type='submit', + ng-disabled="updateAccountEmailAddress.inflight" + ) + span(ng-show="!updateAccountEmailAddress.inflight") #{translate("update")} + span(ng-show="updateAccountEmailAddress.inflight") #{translate("updating")}… diff --git a/services/web/app/views/subscriptions/dashboard/_team_name_mixin.pug b/services/web/app/views/subscriptions/dashboard/_team_name_mixin.pug new file mode 100644 index 0000000000..f840cecfcb --- /dev/null +++ b/services/web/app/views/subscriptions/dashboard/_team_name_mixin.pug @@ -0,0 +1,9 @@ +mixin teamName(subscription) + if (subscription.teamName && subscription.teamName != '') + strong(ng-non-bindable)= subscription.teamName + else if (subscription.admin_id._id == user._id) + | a group account + else + | the group account owned by + | + strong= subscription.admin_id.email diff --git a/services/web/app/views/subscriptions/dashboard/_v1_subscription_status.pug b/services/web/app/views/subscriptions/dashboard/_v1_subscription_status.pug new file mode 100644 index 0000000000..54df7bda2f --- /dev/null +++ b/services/web/app/views/subscriptions/dashboard/_v1_subscription_status.pug @@ -0,0 +1,62 @@ +if (v1SubscriptionStatus['team'] && v1SubscriptionStatus['team']['default_plan_name'] != 'free') + - hasDisplayedSubscription = true + p + | You have a legacy group licence from Overleaf v1. + if (v1SubscriptionStatus['team']['will_end_at']) + p + | Your current group licence ends on + | + strong= moment(v1SubscriptionStatus['team']['will_end_at']).format('Do MMM YY') + | + | and will + | + if (v1SubscriptionStatus['team']['will_renew']) + | be automatically renewed. + else + | not be automatically renewed. + if (v1SubscriptionStatus['can_cancel_team']) + p + form(method="POST", action="/user/subscription/v1/cancel") + input(type="hidden", name="_csrf", value=csrfToken) + button().btn.btn-danger Stop automatic renewal + else + p + | Please + | + a(href="/contact") contact support + | + | to make changes to your plan + hr + +if (v1SubscriptionStatus['product']) + - hasDisplayedSubscription = true + p + | You have a legacy Overleaf v1 + | + strong= v1SubscriptionStatus['product']['display_name'] + | + | plan. + p + | Your plan ends on + | + strong= moment(v1SubscriptionStatus['product']['will_end_at']).format('Do MMM YY') + | + | and will + | + if (v1SubscriptionStatus['product']['will_renew']) + | be automatically renewed. + else + | not be automatically renewed. + if (v1SubscriptionStatus['can_cancel']) + p + form(method="POST", action="/user/subscription/v1/cancel") + input(type="hidden", name="_csrf", value=csrfToken) + button().btn.btn-danger Stop automatic renewal + else + p + | Please + | + a(href="/contact") contact support + | + | to make changes to your plan + hr diff --git a/services/web/app/views/subscriptions/new.pug b/services/web/app/views/subscriptions/new.pug new file mode 100644 index 0000000000..b4e58512e2 --- /dev/null +++ b/services/web/app/views/subscriptions/new.pug @@ -0,0 +1,359 @@ +extends ../layout + +block append meta + meta(name="ol-countryCode" content=countryCode) + meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey) + meta(name="ol-recomendedCurrency" content=String(currency).slice(0,3)) + +block head-scripts + script(type="text/javascript", nonce=scriptNonce, src="https://js.recurly.com/v4/recurly.js") + +block content + main.content.content-alt#main-content + .container(ng-controller="NewSubscriptionController" ng-cloak) + .row.card-group + .col-md-5.col-md-push-4 + .card.card-highlighted.card-border(ng-hide="threeDSecureFlow") + .alert.alert-danger(ng-show="recurlyLoadError") + strong #{translate('payment_provider_unreachable_error')} + .page-header(ng-hide="recurlyLoadError") + .row + .col-xs-9 + h2 {{planName}} + .col-xs-3 + div.dropdown.changePlanButton.pull-right(ng-cloak, dropdown) + a.btn.btn-default.dropdown-toggle( + href="#", + data-toggle="dropdown", + dropdown-toggle + ) + | {{currencyCode}} ({{allCurrencies[currencyCode]['symbol']}}) + span.caret + ul.dropdown-menu(role="menu") + li(ng-repeat="(currency, value) in availableCurrencies") + a( + ng-click="changeCurrency(currency)", + ) {{currency}} ({{value['symbol']}}) + .row(ng-if="planCode == 'student-annual' || planCode == 'student-monthly' || planCode == 'student_free_trial_7_days'") + .col-xs-12 + p.student-disclaimer #{translate('student_disclaimer')} + + hr.thin + .row + .col-md-12.text-center + div(ng-if="trialLength") + span !{translate("first_few_days_free", {trialLen:'{{trialLength}}'})} + span(ng-if="coupon.discountMonths && coupon.discountRate")   - {{coupon.discountMonths}} #{translate("month")}s {{coupon.discountRate}}% Off + + div(ng-if="price") + - var priceVars = { price: "{{ availableCurrencies[currencyCode]['symbol'] }}{{ price.total }}"}; + span(ng-if="!coupon.singleUse && monthlyBilling") + | !{translate("x_price_per_month", priceVars, ['strong'] )} + span(ng-if="!coupon.singleUse && !monthlyBilling") + | !{translate("x_price_per_year", priceVars, ['strong'] )} + span(ng-if="coupon.singleUse && monthlyBilling") + | !{translate("x_price_for_first_month", priceVars, ['strong'] )} + span(ng-if="coupon.singleUse && !monthlyBilling") + | !{translate("x_price_for_first_year", priceVars, ['strong'] )} + + div(ng-if="coupon && coupon.normalPrice") + - var noDiscountPriceAngularExp = "{{ availableCurrencies[currencyCode]['symbol']}}{{coupon.normalPrice | number:2 }}"; + span.small(ng-if="!coupon.singleUse && monthlyBilling") + | !{translate("normally_x_price_per_month", { price: noDiscountPriceAngularExp } )} + span.small(ng-if="!coupon.singleUse && !monthlyBilling") + | !{translate("normally_x_price_per_year", { price: noDiscountPriceAngularExp } )} + span.small(ng-if="coupon.singleUse && monthlyBilling") + | !{translate("then_x_price_per_month", { price: noDiscountPriceAngularExp } )} + span.small(ng-if="coupon.singleUse && !monthlyBilling") + | !{translate("then_x_price_per_year", { price: noDiscountPriceAngularExp } )} + + .row(ng-hide="recurlyLoadError") + div() + .col-md-12() + form( + name="simpleCCForm" + novalidate + ) + + div.payment-method-toggle + a.payment-method-toggle-switch( + href + ng-click="setPaymentMethod('credit_card');" + ng-class="paymentMethod.value === 'credit_card' ? 'payment-method-toggle-switch-selected' : ''" + ) + i.fa.fa-cc-mastercard.fa-2x(aria-hidden="true") + span   + i.fa.fa-cc-visa.fa-2x(aria-hidden="true") + span   + i.fa.fa-cc-amex.fa-2x(aria-hidden="true") + span.sr-only Pay with Mastercard, Visa, or Amex + a.payment-method-toggle-switch( + href + ng-click="setPaymentMethod('paypal');" + ng-class="paymentMethod.value === 'paypal' ? 'payment-method-toggle-switch-selected' : ''" + ) + i.fa.fa-cc-paypal.fa-2x(aria-hidden="true") + span.sr-only Pay with PayPal + + .alert.alert-warning.small(ng-show="genericError") + strong {{genericError}} + + .alert.alert-warning.small(ng-show="couponError") + strong {{couponError}} + + div(ng-show="paymentMethod.value === 'credit_card'") + .row + .col-xs-6 + .form-group(ng-class="validation.errorFields.first_name || inputHasError(simpleCCForm.firstName) ? 'has-error' : ''") + label(for="first-name") #{translate('first_name')} + input#first-name.form-control( + type="text" + maxlength='255' + data-recurly="first_name" + name="firstName" + ng-model="data.first_name" + required + ) + span.input-feedback-message(ng-if="simpleCCForm.firstName.$error.required") #{translate('this_field_is_required')} + .col-xs-6 + .form-group(ng-class="validation.errorFields.last_name || inputHasError(simpleCCForm.lastName)? 'has-error' : ''") + label(for="last-name") #{translate('last_name')} + input#last-name.form-control( + type="text" + maxlength='255' + data-recurly="last_name" + name="lastName" + ng-model="data.last_name" + required + ) + span.input-feedback-message(ng-if="simpleCCForm.lastName.$error.required") #{translate('this_field_is_required')} + + .form-group(ng-class="validation.errorFields.number ? 'has-error' : ''") + label(for="card-no") #{translate("credit_card_number")} + div#card-no( + type="text" + name="ccNumber" + data-recurly='number' + ) + + .row + .col-xs-3 + .form-group.has-feedback(ng-class="validation.errorFields.month ? 'has-error' : ''") + label(for="month").capitalised #{translate("month")} + div( + type="number" + name="month" + data-recurly="month" + ) + .col-xs-3 + .form-group.has-feedback(ng-class="validation.errorFields.year ? 'has-error' : ''") + label(for="year").capitalised #{translate("year")} + div( + type="number" + name="year" + data-recurly="year" + ) + + .col-xs-6 + .form-group.has-feedback(ng-class="validation.errorFields.cvv ? 'has-error' : ''") + label #{translate("security_code")} + div( + type="number" + ng-model="data.cvv" + data-recurly="cvv" + name="cvv" + cc-format-sec-code + ) + .form-control-feedback + a.form-helper( + href + tabindex="-1" + tooltip-template="'cvv-tooltip-tpl.html'" + tooltip-trigger="mouseenter" + tooltip-append-to-body="true" + ) ? + + div + .row + .col-xs-12 + .form-group(ng-class="validation.errorFields.address1 || inputHasError(simpleCCForm.address1) ? 'has-error' : ''") + label(for="address-line-1") #{translate('address_line_1')} + input#address-line-1.form-control( + type="text" + maxlength="255" + data-recurly="address1" + name="address1" + ng-model="data.address1" + required + ) + span.input-feedback-message(ng-if="simpleCCForm.address1.$error.required") #{translate('this_field_is_required')} + + .row + .col-xs-12 + .form-group.has-feedback(ng-class="validation.errorFields.address2 ? 'has-error' : ''") + label(for="address-line-2") #{translate('address_line_2')} + input#address-line-2.form-control( + type="text" + maxlength="255" + data-recurly="address2" + name="address2" + ng-model="data.address2" + ) + + .row + .col-xs-4 + .form-group(ng-class="validation.errorFields.postal_code || inputHasError(simpleCCForm.postalCode) ? 'has-error' : ''") + label(for="postal-code") #{translate('postal_code')} + input#postal-code.form-control( + type="text" + maxlength="255" + data-recurly="postal_code" + name="postalCode" + ng-model="data.postal_code" + required + ) + span.input-feedback-message(ng-if="simpleCCForm.postalCode.$error.required") #{translate('this_field_is_required')} + + .col-xs-8 + .form-group(ng-class="validation.errorFields.country || inputHasError(simpleCCForm.country) ? 'has-error' : ''") + label(for="country") #{translate('country')} + select#country.form-control( + data-recurly="country" + ng-model="data.country" + name="country" + ng-change="updateCountry()" + ng-selected="{{country.code == data.country}}" + ng-model-options="{ debounce: 200 }" + required + ) + option(value='', disabled) #{translate("country")} + option(value='-', disabled) -------------- + option(ng-repeat="country in countries" ng-bind-html="country.name" value="{{country.code}}") + span.input-feedback-message(ng-if="simpleCCForm.country.$error.required") #{translate('this_field_is_required')} + + .form-group + .checkbox + label + input( + type="checkbox" + ng-model="ui.addCompanyDetails" + ) + | + | #{translate("add_company_details")} + + .form-group(ng-show="ui.addCompanyDetails") + label(for="company-name") #{translate("company_name")} + input#company-name.form-control( + type="text" + name="companyName" + ng-model="data.company" + ) + + .form-group(ng-show="ui.addCompanyDetails && taxes.length") + label(for="vat-number") #{translate("vat_number")} + input#vat-number.form-control( + type="text" + name="vatNumber" + ng-model="data.vat_number" + ng-blur="applyVatNumber()" + ) + + if (showCouponField) + .form-group + label(for="coupon-code") #{translate('coupon_code')} + input#coupon-code.form-control( + type="text" + ng-blur="applyCoupon()" + ng-model="data.coupon" + ) + + p(ng-if="paymentMethod.value === 'paypal'") #{translate("paypal_upgrade")} + + div.price-breakdown( + ng-show="taxes.length" + ) + - var priceBreakdownVars = { total: "{{ availableCurrencies[currencyCode]['symbol'] }}{{ price.total }}", subtotal: "{{availableCurrencies[currencyCode]['symbol']}}{{ price.subtotal }}", tax: "{{availableCurrencies[currencyCode]['symbol']}}{{ price.tax }}" }; + hr.thin + span + | Total: + | + span(ng-if="!coupon.singleUse && monthlyBilling") + | !{translate("x_price_per_month_tax", priceBreakdownVars, ['strong'] )} + span(ng-if="!coupon.singleUse && !monthlyBilling") + | !{translate("x_price_per_year_tax", priceBreakdownVars, ['strong'] )} + span(ng-if="coupon.singleUse && monthlyBilling") + | !{translate("x_price_for_first_month_tax", priceBreakdownVars, ['strong'] )} + span(ng-if="coupon.singleUse && !monthlyBilling") + | !{translate("x_price_for_first_year_tax", priceBreakdownVars, ['strong'] )} + hr.thin + + div.payment-submit + button.btn.btn-success.btn-block( + ng-click="submit()" + ng-disabled="processing || !isFormValid(simpleCCForm);" + ) + span(ng-show="processing") + i.fa.fa-spinner.fa-spin(aria-hidden="true") + span.sr-only #{translate('processing')} + |   + span(ng-if="paymentMethod.value === 'credit_card'") + | {{ monthlyBilling ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_now")}'}} + span(ng-if="paymentMethod.value !== 'credit_card'") #{translate("upgrade_paypal_btn")} + + p.tos-agreement-notice !{translate("by_subscribing_you_agree_to_our_terms_of_service", {}, [{name: 'a', attrs: {href: '/legal#Terms', target:'_blank', rel:'noopener noreferrer'}}])} + + div.three-d-secure-container.card.card-highlighted.card-border(ng-show="threeDSecureFlow") + .alert.alert-info.small(aria-live="assertive") + strong #{translate('card_must_be_authenticated_by_3dsecure')} + div.three-d-secure-recurly-container + + + + .col-md-3.col-md-pull-4 + if showStudentPlan + a.btn-primary.btn.plansPageStudentLink( + href, + ng-click="switchToStudent()" + ) #{translate("special_price_student")} + + .card.card-first + .paymentPageFeatures + h3 #{translate("unlimited_projects")} + p #{translate("create_unlimited_projects")} + + h3 + if plan.features.collaborators == -1 + - var collaboratorCount = 'Unlimited' + else + - var collaboratorCount = plan.features.collaborators + if plan.features.collaborators == 1 + | #{translate("collabs_per_proj_single", {collabcount:collaboratorCount})} + else + | #{translate("collabs_per_proj", {collabcount:collaboratorCount})} + p #{translate("work_on_single_version")}. #{translate("view_collab_edits_in_real_time")} + + h3 #{translate("full_doc_history")} + p.track-changes-example + | #{translate("see_what_has_been")} #[span.added #{translate("added")}] + |  #{translate("and")} #[span.removed #{translate("removed")}]. + p + | #{translate("restore_to_any_older_version")}. + + h3 #{translate("sync_to_dropbox")} + p + | #{translate("acces_work_from_anywhere")}. + | #{translate("work_offline_and_sync_with_dropbox")}. + + hr + + p.small.text-center(ng-non-bindable) !{translate("cancel_anytime", { appName:'{{settings.appName}}' })} + + script(type="text/javascript", nonce=scriptNonce). + ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}") + + script( + type="text/ng-template" + id="cvv-tooltip-tpl.html" + ) + p !{translate("for_visa_mastercard_and_discover", {}, ['strong', 'strong', 'strong'])} + p !{translate("for_american_express", {}, ['strong', 'strong', 'strong'])} diff --git a/services/web/app/views/subscriptions/plans.pug b/services/web/app/views/subscriptions/plans.pug new file mode 100644 index 0000000000..1db7c400ec --- /dev/null +++ b/services/web/app/views/subscriptions/plans.pug @@ -0,0 +1,100 @@ +extends ../layout + +include _plans_page_mixins +include _plans_page_tables + +block vars + - metadata = { viewport: true } + +block append meta + meta(name="ol-recomendedCurrency" content=recomendedCurrency) + meta(name="ol-groupPlans" data-type="json" content=groupPlans) + +block content + main.content.content-alt#main-content + .container + .user-notifications + ul.list-unstyled(ng-cloak) + li.notification-entry + .alert.alert-info + .notification-body + span To help you work from home throughout 2021, we're providing discounted plans and special initiatives. + .notification-action + a.btn.btn-sm.btn-info(href="https://www.overleaf.com/events/wfh2021" event-tracking="Event-Pages" event-tracking-trigger="click" event-tracking-ga="WFH-Offer-Click" event-tracking-label="Plans-Banner") Upgrade + .content-page + .plans(ng-controller="PlansController") + .container(ng-cloak) + .row + .col-md-12 + .page-header.centered.plans-header.text-centered + h1.text-capitalize(ng-non-bindable) #{translate('get_instant_access_to')} #{settings.appName} + .row + .col-md-8.col-md-offset-2 + p.text-centered #{translate("sl_benefits_plans")} + + +allCardsAndControls() + + .row.row-spaced-large.text-centered + .col-xs-12 + p.text-centered !{translate('also_provides_free_plan', { appName:'{{settings.appName}}' }, [{ name: 'a', attrs: { href: '/register' }}])} + i.fa.fa-cc-mastercard.fa-2x(aria-hidden="true")   + span.sr-only Mastercard accepted + i.fa.fa-cc-visa.fa-2x(aria-hidden="true")   + span.sr-only Visa accepted + i.fa.fa-cc-amex.fa-2x(aria-hidden="true")   + span.sr-only Amex accepted + i.fa.fa-cc-paypal.fa-2x(aria-hidden="true")   + span.sr-only Paypal accepted + div.text-centered #{translate('change_plans_any_time')}
#{translate('billed_after_x_days', {len:'{{trial_len}}'})} + br + div.text-centered #{translate('subject_to_additional_vat')}
#{translate('select_country_vat')} + + .row.row-spaced-large + .col-md-8.col-md-offset-2 + .card.text-centered + .card-header + h2 #{translate('looking_multiple_licenses')} + span #{translate('reduce_costs_group_licenses')} + br + br + a.btn.btn-default( + href="#groups" + ng-click="openGroupPlanModal()" + ) #{translate('find_out_more')} + + .row.row-spaced-large + .col-sm-12 + .page-header.plans-header.plans-subheader.text-centered + h2 #{translate('compare_plan_features')} + .row + .col-md-6.col-md-offset-3 + +plan_switch('table') + .col-md-3.text-right + +currency_dropdown + .row(event-tracking="features-table-viewed" event-tracking-ga="subscription-funnel" event-tracking-trigger="scroll" event-tracking-send-once="true" event-tracking-label=`exp-{{plansVariant}}`) + .col-sm-12(ng-if="ui.view != 'student'") + +table_premium + .col-sm-12(ng-if="ui.view == 'student'") + +table_student + + include _plans_quotes + + include _plans_faq + + #bottom-cards.row.row-spaced(style="display: none;") + .col-sm-12 + +allCardsAndControls(true, 'bottom') + + .row.row-spaced-large + .col-md-12 + .plans-header.plans-subheader.text-centered + h2.header-with-btn #{translate('still_have_questions')} + button.btn.btn-default.btn-header.text-capitalize( + ng-controller="ContactGeneralModal" + ng-click="openModal()" + ) #{translate('get_in_touch')} + != moduleIncludes("contactModalGeneral", locals) + + .row.row-spaced + + include _modal_group_purchase diff --git a/services/web/app/views/subscriptions/successful_subscription.pug b/services/web/app/views/subscriptions/successful_subscription.pug new file mode 100644 index 0000000000..384358a30f --- /dev/null +++ b/services/web/app/views/subscriptions/successful_subscription.pug @@ -0,0 +1,30 @@ +extends ../layout + +block content + main.content.content-alt#main-content + .container + .row + .col-md-8.col-md-offset-2 + .card(ng-cloak) + .page-header + h2 #{translate("thanks_for_subscribing")} + + .alert.alert-success + if (personalSubscription.recurly.trial_ends_at) + p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount: personalSubscription.recurly.price, collectionDate: personalSubscription.recurly.nextPaymentDueAt}, ['strong', 'strong'])} + include ./_price_exceptions + p #{translate("to_modify_your_subscription_go_to")} + a(href="/user/subscription") #{translate("manage_subscription")}. + p + if (personalSubscription.groupPlan == true) + a.btn.btn-success.btn-large(href=`/manage/groups/${personalSubscription._id}/members`) #{translate("add_your_first_group_member_now")} + p.letter-from-founders + p #{translate("thanks_for_subscribing_you_help_sl", {planName:personalSubscription.plan.name})} + p #{translate("need_anything_contact_us_at")} + a(href=`mailto:${settings.adminEmail}`, ng-non-bindable) #{settings.adminEmail} + | . + p #{translate("regards")}, + br(ng-non-bindable) + | The #{settings.appName} Team + p + a.btn.btn-primary(href="/project") < #{translate("back_to_your_projects")} diff --git a/services/web/app/views/subscriptions/team/invite.pug b/services/web/app/views/subscriptions/team/invite.pug new file mode 100644 index 0000000000..40f19a8703 --- /dev/null +++ b/services/web/app/views/subscriptions/team/invite.pug @@ -0,0 +1,39 @@ +extends ../../layout + +block append meta + meta(name="ol-hasIndividualRecurlySubscription" data-type="boolean" content=hasIndividualRecurlySubscription) + meta(name="ol-inviteToken" content=inviteToken) + +block content + main.content.content-alt.team-invite#main-content + .container + .row + .col-md-8.col-md-offset-2 + if (expired) + .alert.alert-warning #{translate("email_link_expired")} + + .row.row-spaced + .col-md-8.col-md-offset-2.text-center(ng-cloak) + .card(ng-controller="TeamInviteController") + .page-header + h1.text-centered(ng-non-bindable) #{translate("invited_to_group", {inviterName: inviterName, appName: appName})} + + div(ng-show="view =='hasIndividualRecurlySubscription'") + p #{translate("cancel_personal_subscription_first")} + .alert.alert-danger(ng-show="cancel_error" ng-cloak) #{translate("something_went_wrong_canceling_your_subscription")} + p + a.btn.btn.btn-default(ng-click="keepPersonalSubscription()", ng-disabled="inflight") #{translate("not_now")} + |   + a.btn.btn.btn-primary(ng-click="cancelPersonalSubscription()", ng-disabled="inflight") #{translate("cancel_your_subscription")} + + div(ng-show="view =='teamInvite'") + p #{translate("join_team_explanation", {appName: appName})} + p + a.btn.btn-default(href="/project") #{translate("not_now")} + |   + a.btn.btn.btn-primary(ng-click="joinTeam()", ng-disabled="inflight") #{translate("accept_invitation")} + + div(ng-show="view =='inviteAccepted'") + p(ng-non-bindable) #{translate("joined_team", {inviterName: inviterName})} + p + a.btn.btn.btn-primary(href="/project") #{translate("done")} diff --git a/services/web/app/views/subscriptions/upgradeToAnnual.pug b/services/web/app/views/subscriptions/upgradeToAnnual.pug new file mode 100644 index 0000000000..cb13bcc535 --- /dev/null +++ b/services/web/app/views/subscriptions/upgradeToAnnual.pug @@ -0,0 +1,24 @@ +extends ../layout + +block content + + main.content.content-alt#main-content + .container(ng-controller="AnnualUpgradeController") + .row(ng-cloak) + .col-md-6.col-md-offset-3 + .card(ng-init="planName = "+JSON.stringify(planName)) + .page-header + h1.text-centered #{translate("move_to_annual_billing")} + div(ng-hide="upgradeComplete") + .row + div.col-md-12 !{translate("change_to_annual_billing_and_save", {percentage:'20%', yearlySaving:'${{yearlySaving}}'}, ['strong', 'strong'])} + .row   + .row + div.col-md-12 + center + button.btn.btn-success(ng-click="completeAnnualUpgrade()", ng-disabled="inflight") + span(ng-show="inflight") #{translate("processing")} + span(ng-hide="inflight") #{translate("move_to_annual_billing")} now + + div(ng-show="upgradeComplete") + h3 #{translate("annual_billing_enabled")}, #{translate("thank_you")}. diff --git a/services/web/app/views/translations/translation_message.pug b/services/web/app/views/translations/translation_message.pug new file mode 100644 index 0000000000..a5b57320cb --- /dev/null +++ b/services/web/app/views/translations/translation_message.pug @@ -0,0 +1,8 @@ +if (typeof(suggestedLanguageSubdomainConfig) != "undefined") + span(ng-controller="TranslationsPopupController", ng-cloak) + .translations-message(ng-hide="hidei18nNotification") + a(href=suggestedLanguageSubdomainConfig.url+currentUrl) !{translate("click_here_to_view_sl_in_lng", {lngName: translate(suggestedLanguageSubdomainConfig.lngCode)}, ['strong'])} + img(src=buildImgPath("flags/24/" + suggestedLanguageSubdomainConfig.lngCode + ".png")) + button(ng-click="dismiss()").close.pull-right + span(aria-hidden="true") × + span.sr-only #{translate("close")} diff --git a/services/web/app/views/university/case_study.pug b/services/web/app/views/university/case_study.pug new file mode 100644 index 0000000000..ad25fd43b6 --- /dev/null +++ b/services/web/app/views/university/case_study.pug @@ -0,0 +1,19 @@ +extends ../layout + +block content + .masthead + .container + .row + .col-md-12 + h1(ng-non-bindable) !{viewData.title} + .row.row-spaced + + .pattern-container + .content + .container + .row + .col-md-10.col-md-offset-1 + .card + .row(ng-non-bindable) + | !{viewData.html} + diff --git a/services/web/app/views/university/university_holder.pug b/services/web/app/views/university/university_holder.pug new file mode 100644 index 0000000000..babf2ebad9 --- /dev/null +++ b/services/web/app/views/university/university_holder.pug @@ -0,0 +1,4 @@ +extends ../layout + +block content + | !{content} \ No newline at end of file diff --git a/services/web/app/views/user/confirm_email.pug b/services/web/app/views/user/confirm_email.pug new file mode 100644 index 0000000000..6b0453a7ab --- /dev/null +++ b/services/web/app/views/user/confirm_email.pug @@ -0,0 +1,28 @@ +extends ../layout + +block content + main.content.content-alt#main-content + .container + .row + .col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4 + .card + .page-header + h1 #{translate("confirm_email")} + form( + async-form="confirm-email", + name="confirmEmailForm" + action="/user/emails/confirm", + method="POST", + id="confirmEmailForm", + auto-submit="true", + ng-cloak + ) + input(type="hidden", name="_csrf", value=csrfToken) + input(type="hidden", name="token", value=token ng-non-bindable) + form-messages(for="confirmEmailForm") + .alert.alert-success(ng-show="confirmEmailForm.response.success") + | Thank you, your email is now confirmed + p.text-center(ng-show="!confirmEmailForm.response.success && !confirmEmailForm.response.error") + i.fa.fa-fw.fa-spin.fa-spinner(aria-hidden="true") + | + | Confirming your email… diff --git a/services/web/app/views/user/login.pug b/services/web/app/views/user/login.pug new file mode 100644 index 0000000000..f4a4b4f69b --- /dev/null +++ b/services/web/app/views/user/login.pug @@ -0,0 +1,46 @@ +extends ../layout + +block vars + - metadata = { viewport: true } + +block content + main.content.content-alt#main-content + .container + .row + .col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4 + .card + .page-header + h1 #{translate("log_in")} + form(async-form="login", name="loginForm", action='/login', method="POST", ng-cloak) + input(name='_csrf', type='hidden', value=csrfToken) + form-messages(for="loginForm") + .form-group + input.form-control( + type='email', + name='email', + required, + placeholder='email@example.com', + ng-model="email", + ng-model-options="{ updateOn: 'blur' }", + focus="true" + ) + span.small.text-primary(ng-show="loginForm.email.$invalid && loginForm.email.$dirty") + | #{translate("must_be_email_address")} + .form-group + input.form-control( + type='password', + name='password', + required, + placeholder='********', + ng-model="password" + ) + span.small.text-primary(ng-show="loginForm.password.$invalid && loginForm.password.$dirty") + | #{translate("required")} + .actions + button.btn-primary.btn( + type='submit', + ng-disabled="loginForm.inflight" + ) + span(ng-show="!loginForm.inflight") #{translate("login")} + span(ng-show="loginForm.inflight") #{translate("logging_in")}… + a.pull-right(href='/user/password/reset') #{translate("forgot_your_password")}? diff --git a/services/web/app/views/user/logout.pug b/services/web/app/views/user/logout.pug new file mode 100644 index 0000000000..a22c833983 --- /dev/null +++ b/services/web/app/views/user/logout.pug @@ -0,0 +1,20 @@ +extends ../layout + +block vars + - metadata = { viewport: true } + +block content + .content.content-alt + main.login-register-container#main-content + .card.login-register-card + .login-register-header + h1.login-register-header-heading #{translate("log_out")} + form.login-register-form(name="logoutForm", action='/logout', method="POST" ng-init="$scope.inflight=true" auto-submit-form) + input(name='_csrf', type='hidden', value=csrfToken) + .actions + button#submit-logout.btn-primary.btn.btn-block( + type='submit', + ng-disabled="$scope.inflight" + ) + span(ng-show="!$scope.inflight") #{translate("log_out")} + span(ng-show="$scope.inflight" ng-cloak) #{translate("logging_out")}… diff --git a/services/web/app/views/user/one_time_login.pug b/services/web/app/views/user/one_time_login.pug new file mode 100644 index 0000000000..c57d7cddc4 --- /dev/null +++ b/services/web/app/views/user/one_time_login.pug @@ -0,0 +1,20 @@ +extends ../layout + +block vars + - metadata = { viewport: true } + +block content + main.content.content-alt#main-content + .container + .row + .col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4 + .card + .page-header + h1 We're back! + p Overleaf is now running normally. + p + | Please + | + a(href="/login") log in + | + | to continue working on your projects. diff --git a/services/web/app/views/user/passwordReset.pug b/services/web/app/views/user/passwordReset.pug new file mode 100644 index 0000000000..12c09f1cc7 --- /dev/null +++ b/services/web/app/views/user/passwordReset.pug @@ -0,0 +1,58 @@ +extends ../layout + +block vars + - metadata = { viewport: true } + +block content + - var showCaptcha = settings.recaptcha && settings.recaptcha.siteKey && !(settings.recaptcha.disabled && settings.recaptcha.disabled.passwordReset) + + if showCaptcha + script(type="text/javascript", nonce=scriptNonce, src="https://www.recaptcha.net/recaptcha/api.js?render=explicit") + div( + id="recaptcha" + class="g-recaptcha" + data-sitekey=settings.recaptcha.siteKey + data-size="invisible" + data-badge="inline" + ) + + main.content.content-alt#main-content + .container + .row + .col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4 + .card + .page-header + h1 #{translate("password_reset")} + .messageArea + form( + async-form="password-reset-request", + name="passwordResetForm" + action="/user/password/reset", + method="POST", + captcha=(showCaptcha ? '' : false), + captcha-action-name=(showCaptcha ? "passwordReset" : false), + ng-cloak + ) + input(type="hidden", name="_csrf", value=csrfToken) + form-messages(for="passwordResetForm" role="alert") + .form-group + label(for='email') #{translate("please_enter_email")} + input.form-control#email( + type='email', + name='email', + placeholder='email@example.com', + required, + autocomplete="username", + ng-model="email", + autofocus + ) + span.small.text-primary( + ng-show="passwordResetForm.email.$invalid && passwordResetForm.email.$dirty" + ) #{translate("must_be_email_address")} + .actions + button.btn.btn-primary( + type='submit', + ng-disabled="passwordResetForm.$invalid || passwordResetForm.inflight" + ) + span(ng-hide="passwordResetForm.inflight") #{translate("request_password_reset")} + span(ng-show="passwordResetForm.inflight") #{translate("requesting_password_reset")}… diff --git a/services/web/app/views/user/reconfirm.pug b/services/web/app/views/user/reconfirm.pug new file mode 100644 index 0000000000..551ef09dfc --- /dev/null +++ b/services/web/app/views/user/reconfirm.pug @@ -0,0 +1,58 @@ +extends ../layout + +block content + - var email = reconfirm_email ? reconfirm_email : "" + - var showCaptcha = settings.recaptcha && settings.recaptcha.siteKey && !(settings.recaptcha.disabled && settings.recaptcha.disabled.passwordReset) + + if showCaptcha + script(type="text/javascript", nonce=scriptNonce, src="https://www.recaptcha.net/recaptcha/api.js?render=explicit") + div( + id="recaptcha" + class="g-recaptcha" + data-sitekey=settings.recaptcha.siteKey + data-size="invisible" + data-badge="inline" + ) + + main.content.content-alt#main-content + .container + .row + .col-sm-12.col-md-6.col-md-offset-3 + .card + h1.card-header.text-capitalize #{translate("reconfirm")} #{translate("Account")} + p #{translate('reconfirm_explained')}  + a(href=`mailto:${settings.adminEmail}`, ng-non-bindable) #{settings.adminEmail} + | . + form( + async-form="reconfirm-account-request", + name="reconfirmAccountForm" + action="/user/reconfirm", + method="POST", + ng-cloak + ng-init="email='"+email+"'" + aria-label=translate('request_reconfirmation_email') + captcha=(showCaptcha ? '' : false), + captcha-action-name=(showCaptcha ? "passwordReset" : false), + ) + input(type="hidden", name="_csrf", value=csrfToken) + form-messages(for="reconfirmAccountForm" role="alert") + .form-group + label(for='email') #{translate("please_enter_email")} + input.form-control( + aria-label="email" + type='email', + name='email', + placeholder='email@example.com', + required, + ng-model="email", + autofocus + ) + span.small.text-primary( + ng-show="reconfirmAccountForm.email.$invalid && reconfirmAccountForm.email.$dirty" + ) #{translate("must_be_email_address")} + .actions + button.btn.btn-primary( + type='submit', + ng-disabled="reconfirmAccountForm.$invalid" + aria-label=translate('request_password_reset_to_reconfirm') + ) #{translate('request_password_reset_to_reconfirm')} diff --git a/services/web/app/views/user/register.pug b/services/web/app/views/user/register.pug new file mode 100644 index 0000000000..38c4e5fd4a --- /dev/null +++ b/services/web/app/views/user/register.pug @@ -0,0 +1,32 @@ +extends ../layout + +block content + main.content.content-alt#main-content + .container + .row + .registration_message + if sharedProjectData.user_first_name !== undefined + h1(ng-non-bindable) #{translate("user_wants_you_to_see_project", {username:sharedProjectData.user_first_name, projectname:""})} + em(ng-non-bindable) #{sharedProjectData.project_name} + div + | #{translate("join_sl_to_view_project")}. + div + | #{translate("if_you_are_registered")}, + a(href="/login") #{translate("login_here")} + else if newTemplateData.templateName !== undefined + h1(ng-non-bindable) #{translate("register_to_edit_template", {templateName:newTemplateData.templateName})} + + div #{translate("already_have_sl_account")} + a(href="/login") #{translate("login_here")} + + .row + .col-md-8.col-md-offset-2.col-lg-6.col-lg-offset-3 + .card + .page-header + h1 #{translate("register")} + p + | Please contact + | + strong(ng-non-bindable) #{settings.adminEmail} + | + | to create an account. diff --git a/services/web/app/views/user/restricted.pug b/services/web/app/views/user/restricted.pug new file mode 100644 index 0000000000..1e87a835a8 --- /dev/null +++ b/services/web/app/views/user/restricted.pug @@ -0,0 +1,13 @@ +extends ../layout + +block content + main.content#main-content + .container + .row + .col-md-8.col-md-offset-2.text-center + .page-header + h2 #{translate("restricted_no_permission")} + p + a(href="/") + i.fa.fa-arrow-circle-o-left(aria-hidden="true") + | #{translate("take_me_home")} diff --git a/services/web/app/views/user/sessions.pug b/services/web/app/views/user/sessions.pug new file mode 100644 index 0000000000..315aa06ae1 --- /dev/null +++ b/services/web/app/views/user/sessions.pug @@ -0,0 +1,48 @@ +extends ../layout + + +block append meta + meta(name="ol-otherSessions" data-type="json" content=sessions) + + +block content + main.content.content-alt#main-content + .container + .row + .col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 + .card.clear-user-sessions(ng-controller="ClearSessionsController", ng-cloak) + .page-header + h1 #{translate("your_sessions")} + + div + p.small + | !{translate("clear_sessions_description")} + + div + div(ng-if="state.otherSessions.length == 0") + p.text-center + | #{translate("no_other_sessions")} + + div(ng-if="state.success == true") + p.text-success.text-center + | #{translate('clear_sessions_success')} + + div(ng-if="state.otherSessions.length != 0") + table.table.table-striped + thead + tr + th #{translate("ip_address")} + th #{translate("session_created_at")} + tr(ng-repeat="session in state.otherSessions") + td {{session.ip_address}} + td {{session.session_created | formatDate}} + + p.actions + .text-center + button.btn.btn-lg.btn-primary( + ng-click="clearSessions()" + ) #{translate('clear_sessions')} + + div(ng-if="state.error == true") + p.text-danger.error + | #{translate('generic_something_went_wrong')} diff --git a/services/web/app/views/user/setPassword.pug b/services/web/app/views/user/setPassword.pug new file mode 100644 index 0000000000..2e5c10a455 --- /dev/null +++ b/services/web/app/views/user/setPassword.pug @@ -0,0 +1,63 @@ +extends ../layout + +block append meta + meta(name="ol-passwordStrengthOptions" data-type="json" content=settings.passwordStrengthOptions) + +block content + main.content.content-alt#main-content + .container + .row + .col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4 + .card + .page-header + h1 #{translate("reset_your_password")} + form( + async-form="password-reset", + name="passwordResetForm", + action="/user/password/set", + method="POST", + ng-cloak + ) + input(type="hidden", name="_csrf", value=csrfToken) + .alert.alert-success(ng-show="passwordResetForm.response.success") + | #{translate("password_has_been_reset")}. + br + a(href='/login') #{translate("login_here")} + div(ng-show="passwordResetForm.response.error == true") + div(ng-switch="passwordResetForm.response.status") + .alert.alert-danger(ng-switch-when="404") + | #{translate('password_reset_token_expired')} + br + a(href="/user/password/reset") + | Request a new password reset email + .alert.alert-danger(ng-switch-when="400") + | #{translate('invalid_password')} + .alert.alert-danger(ng-switch-when="429") + | #{translate('rate_limit_hit_wait')} + .alert.alert-danger(ng-switch-default) + | #{translate('error_performing_request')} + + + .form-group + input.form-control#passwordField( + type='password', + name='password', + placeholder='new password', + required, + autocomplete="new-password", + ng-model="password", + autofocus, + complex-password + ) + span.small.text-primary(ng-show="passwordResetForm.password.$error.complexPassword", ng-bind-html="complexPasswordErrorMessage") + input( + type="hidden", + name="passwordResetToken", + value=passwordResetToken + ng-non-bindable + ) + .actions + button.btn.btn-primary( + type='submit', + ng-disabled="passwordResetForm.$invalid" + ) #{translate("set_new_password")} diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug new file mode 100644 index 0000000000..0b501ad824 --- /dev/null +++ b/services/web/app/views/user/settings.pug @@ -0,0 +1,287 @@ +extends ../layout + +block append meta + meta(name="ol-reconfirmationRemoveEmail", content=reconfirmationRemoveEmail) + meta(name="ol-reconfirmedViaSAML", content=reconfirmedViaSAML) + meta(name="ol-passwordStrengthOptions", data-type="json", content=settings.passwordStrengthOptions || {}) + meta(name="ol-oauthProviders", data-type="json", content=oauthProviders) + meta(name="ol-thirdPartyIds", data-type="json", content=thirdPartyIds) + +block content + main.content.content-alt#main-content + .container + .row + .col-md-12.col-lg-10.col-lg-offset-1 + if ssoError + .alert.alert-danger + | #{translate('sso_link_error')}: #{translate(ssoError)} + .card + .page-header + h1 #{translate("account_settings")} + .account-settings(ng-controller="AccountSettingsController", ng-cloak) + + if hasFeature('affiliations') + include settings/user-affiliations + + .row + .col-md-5 + h3 #{translate("update_account_info")} + form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate) + input(type="hidden", name="_csrf", value=csrfToken) + if !hasFeature('affiliations') + if !externalAuthenticationSystemUsed() + .form-group + label(for='email') #{translate("email")} + input.form-control( + id="email" + type='email', + name='email', + placeholder="email@example.com" + required, + ng-model="email", + ng-model-options="{ updateOn: 'blur' }" + ) + span.small.text-danger(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty") + | #{translate("must_be_email_address")} + else + // show the email, non-editable + .form-group + label.control-label #{translate("email")} + div.form-control( + readonly="true", + ng-non-bindable + ) #{user.email} + + if shouldAllowEditingDetails + .form-group + label(for='firstName').control-label #{translate("first_name")} + input.form-control( + id="firstName" + type='text', + name='first_name', + value=user.first_name + ng-non-bindable + ) + .form-group + label(for='lastName').control-label #{translate("last_name")} + input.form-control( + id="lastName" + type='text', + name='last_name', + value=user.last_name + ng-non-bindable + ) + .form-group + form-messages(aria-live="polite" for="settingsForm") + .alert.alert-success(ng-show="settingsForm.response.success") + | #{translate("thanks_settings_updated")} + .actions + button.btn.btn-primary( + type='submit', + ng-disabled="settingsForm.$invalid" + ) #{translate("update")} + else + .form-group + label.control-label #{translate("first_name")} + div.form-control( + readonly="true", + ng-non-bindable + ) #{user.first_name} + .form-group + label.control-label #{translate("last_name")} + div.form-control( + readonly="true", + ng-non-bindable + ) #{user.last_name} + + .col-md-5.col-md-offset-1 + h3 #{translate("change_password")} + if externalAuthenticationSystemUsed() && !settings.overleaf + p + Password settings are managed externally + else if !hasPassword + p + | #[a(href="/user/password/reset", target='_blank') #{translate("no_existing_password")}] + else + - var submitAction + - submitAction = '/user/password/update' + form( + async-form="changepassword" + name="changePasswordForm" + action=submitAction + method="POST" + novalidate + ) + input(type="hidden", name="_csrf", value=csrfToken) + .form-group + label(for='currentPassword') #{translate("current_password")} + input.form-control( + id="currentPassword" + type='password', + name='currentPassword', + placeholder='*********', + ng-model="currentPassword", + required + ) + span.small.text-danger(ng-show="changePasswordForm.currentPassword.$invalid && changePasswordForm.currentPassword.$dirty" aria-live="polite") + | #{translate("required")} + .form-group + label(for='passwordField') #{translate("new_password")} + input.form-control( + id='passwordField', + type='password', + name='newPassword1', + placeholder='*********', + ng-model="newPassword1", + required, + complex-password + ) + span.small.text-danger(ng-show="changePasswordForm.newPassword1.$error.complexPassword && changePasswordForm.newPassword1.$dirty", ng-bind-html="complexPasswordErrorMessage" aria-live="polite") + .form-group + label(for='newPassword2') #{translate("confirm_new_password")} + input.form-control( + id="newPassword2" + type='password', + name='newPassword2', + placeholder='*********', + ng-model="newPassword2", + equals="passwordField" + ) + span.small.text-danger(ng-show="changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$dirty" aria-live="polite") + | #{translate("doesnt_match")} + span.small.text-danger(ng-show="!changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$invalid && changePasswordForm.newPassword2.$dirty" aria-live="polite") + | #{translate("invalid_password")} + .form-group + form-messages(aria-live="polite" for="changePasswordForm") + .actions + button.btn.btn-primary( + type='submit', + ng-disabled="changePasswordForm.$invalid" + ) #{translate("change")} + + | !{moduleIncludes("userSettings", locals)} + hr + + h3 + | #{translate("sharelatex_beta_program")} + + if (user.betaProgram) + p.small + | #{translate("beta_program_already_participating")} + + div + a(id="beta-program-participate-link" href="/beta/participate") #{translate("manage_beta_program_membership")} + + hr + + h3 + | #{translate("sessions")} + + div + a(id="sessions-link", href="/user/sessions") #{translate("manage_sessions")} + + if hasFeature('oauth') + hr + include settings/user-oauth + + if hasFeature('saas') && (!externalAuthenticationSystemUsed() || (settings.createV1AccountOnLogin && settings.overleaf)) + hr + p.small + | #{translate("newsletter_info_and_unsubscribe")} + a( + href, + ng-click="unsubscribe()", + ng-show="subscribed && !unsubscribing" + ) #{translate("unsubscribe")} + span( + ng-show="unsubscribing" + ) + i.fa.fa-spin.fa-refresh(aria-hidden="true") + | #{translate("unsubscribing")} + span.text-success( + ng-show="!subscribed" + ) + i.fa.fa-check(aria-hidden="true") + | #{translate("unsubscribed")} + + if !settings.overleaf && user.overleaf + p + | Please note: If you have linked your account with Overleaf + | v2, then deleting your ShareLaTeX account will also delete + | account and all of it's associated projects and data. + p #{translate("need_to_leave")} + a(href, ng-click="deleteAccount()") #{translate("delete_your_account")} + + + script(type='text/ng-template', id='deleteAccountModalTemplate') + .modal-header + h3 #{translate("delete_account")} + div.modal-body#delete-account-modal + p !{translate("delete_account_warning_message_3")} + if settings.createV1AccountOnLogin && settings.overleaf + p + strong + | Your Overleaf v2 projects will be deleted if you delete your account. + | If you want to remove any remaining Overleaf v1 projects in your account, + | please first make sure they are imported to Overleaf v2. + + if settings.overleaf && !hasPassword + p + b + | #[a(href="/user/password/reset", target='_blank') #{translate("delete_acct_no_existing_pw")}]. + else + form(novalidate, name="deleteAccountForm") + label #{translate('email')} + input.form-control( + type="text", + autocomplete="off", + placeholder="", + ng-model="state.deleteText", + focus-on="open", + ng-keyup="checkValidation()" + ) + + label #{translate('password')} + input.form-control( + type="password", + autocomplete="off", + placeholder="", + ng-model="state.password", + ng-keyup="checkValidation()" + ) + + div.confirmation-checkbox-wrapper + input( + type="checkbox" + ng-model="state.confirmSharelatexDelete" + ng-change="checkValidation()" + ).pull-left + label(style="display: inline")  I understand this will delete all projects in my Overleaf account with email address #[em {{ userDefaultEmail }}] + + div(ng-if="state.error") + div.alert.alert-danger(ng-switch="state.error.code") + span(ng-switch-when="InvalidCredentialsError") + | #{translate('email_or_password_wrong_try_again')} + span(ng-switch-when="SubscriptionAdminDeletionError") + | #{translate('subscription_admins_cannot_be_deleted')} + span(ng-switch-when="UserDeletionError") + | #{translate('user_deletion_error')} + span(ng-switch-default) + | #{translate('generic_something_went_wrong')} + if settings.createV1AccountOnLogin && settings.overleaf + div(ng-if="state.error && state.error.code == 'InvalidCredentialsError'") + div.alert.alert-info + | If you can't remember your password, or if you are using Single-Sign-On with another provider + | to sign in (such as Twitter or Google), please + | #[a(href="/user/password/reset", target='_blank') reset your password], + | and try again. + .modal-footer + button.btn.btn-default( + ng-click="cancel()" + ) #{translate("cancel")} + button.btn.btn-danger( + ng-disabled="!state.isValid || state.inflight" + ng-click="delete()" + ) + span(ng-hide="state.inflight") #{translate("delete")} + span(ng-show="state.inflight") #{translate("deleting")}… diff --git a/services/web/app/views/user/settings/user-affiliations.pug b/services/web/app/views/user/settings/user-affiliations.pug new file mode 100644 index 0000000000..ebeb6ade14 --- /dev/null +++ b/services/web/app/views/user/settings/user-affiliations.pug @@ -0,0 +1,386 @@ +include ../../_mixins/reconfirm_affiliation + +mixin aboutInstitutionLink() + a(href="/learn/how-to/Institutional_Login") #{translate("find_out_more_about_institution_login")}. + +mixin btnMakePrimaryDisabled(tooltip) + div( + tooltip=tooltip + tooltip-enable="!ui.isMakingRequest" + ) + button.btn.btn-sm.btn-success.affiliations-table-inline-action( + disabled + type="button" + ) #{translate("make_primary")} + +mixin btnRemoveEmail() + .affiliations-table-inline-action-disabled-wrapper(ng-if="userEmail.default") + div( + tooltip=translate("please_change_primary_to_remove") + tooltip-enable="!ui.isMakingRequest" + tooltip-placement="left" + ) + button.btn.btn-sm.btn-danger(disabled) + i.fa.fa-fw.fa-trash(aria-hidden="true") + span.sr-only #{translate("please_change_primary_to_remove")} + button.btn.btn-sm.btn-danger.affiliations-table-inline-action( + ng-if="!userEmail.default" + ng-click="removeUserEmail(userEmail)" + ng-disabled="ui.isMakingRequest" + tooltip=translate("remove") + type="button" + ) + i.fa.fa-fw.fa-trash(aria-hidden="true") + span.sr-only #{translate("remove")} + +form.row( + ng-controller="UserAffiliationsController" + name="affiliationsForm" +) + .col-md-12 + h3 #{translate("emails_and_affiliations_title")} + p.small #{translate("emails_and_affiliations_explanation")} + table.table.affiliations-table + thead + tr + th.affiliations-table-email #{translate("email")} + th.affiliations-table-institution #{translate("institution_and_role")} + th.affiliations-table-inline-actions + tbody + tr( + ng-repeat-start="userEmail in userEmails" + ) + td + | {{ userEmail.email + (userEmail.default ? ' (primary)' : '') }} + div(ng-if="!userEmail.confirmedAt").small + strong #{translate('unconfirmed')}. + span(ng-if="!userEmail.ssoAvailable")  #{translate('please_check_your_inbox')}. + br + a( + href, + ng-click="resendConfirmationEmail(userEmail)", + ng-if="!userEmail.ssoAvailable" + ) #{translate('resend_confirmation_email')} + div(ng-if="userEmail.confirmedAt && userEmail.affiliation.institution && userEmail.affiliation.institution.confirmed && userEmail.affiliation.licence && userEmail.affiliation.licence != 'free'").small + span.label.label-primary #{translate("professional")} + td + div(ng-if="userEmail.affiliation.institution") + div {{ userEmail.affiliation.institution.name }} + span.small + a( + href + ng-if="!isChangingAffiliation(userEmail.email) && !userEmail.affiliation.role && !userEmail.affiliation.department" + ng-click="changeAffiliation(userEmail);" + ) #{translate("add_role_and_department")} + div.small( + ng-if="!isChangingAffiliation(userEmail.email) && (userEmail.affiliation.role || userEmail.affiliation.department)" + ) + span(ng-if="userEmail.affiliation.role") {{ userEmail.affiliation.role }} + span(ng-if="userEmail.affiliation.role && userEmail.affiliation.department") ,  + span(ng-if="userEmail.affiliation.department") {{ userEmail.affiliation.department }} + br + a( + href + ng-click="changeAffiliation(userEmail);" + ) #{translate("change")} + .affiliation-change-container( + ng-if="isChangingAffiliation(userEmail.email)" + ) + affiliation-form( + affiliation-data="affiliationToChange" + show-university-and-country="false" + show-role-and-department="true" + ) + .affiliation-change-actions.small + button.btn.btn-sm.btn-success( + ng-click="saveAffiliationChange(userEmail);" + ng-disabled="!(affiliationToChange.role && affiliationToChange.department)" + type="button" + ) #{translate("save_or_cancel-save")} + |  #{translate("save_or_cancel-or" )}  + a( + href + ng-click="cancelAffiliationChange();" + ) #{translate("save_or_cancel-cancel")} + td.affiliations-table-inline-actions + // Disabled buttons don't work with tooltips, due to pointer-events: none, + // so create a wrapper for the tooltip + span(ng-if="!userEmail.default && (!userEmail.confirmedAt || ui.isMakingRequest) && !institutionAlreadyLinked(userEmail) && !inReconfirmNotificationPeriod(userEmail)") + .affiliations-table-inline-action-disabled-wrapper(ng-if="userEmail.ssoAvailable") + +btnMakePrimaryDisabled(translate("please_link_before_making_primary")) + .affiliations-table-inline-action-disabled-wrapper(ng-if="!userEmail.ssoAvailable") + +btnMakePrimaryDisabled(translate("please_confirm_your_email_before_making_it_default")) + .affiliations-table-inline-action-disabled-wrapper(ng-if="!userEmail.default && inReconfirmNotificationPeriod(userEmail)") + +btnMakePrimaryDisabled(translate("please_reconfirm_your_affiliation_before_making_this_primary")) + button.btn.btn-sm.btn-success.affiliations-table-inline-action( + tooltip=translate("make_email_primary_description") + ng-if="!userEmail.default && (userEmail.confirmedAt && !ui.isMakingRequest) && !inReconfirmNotificationPeriod(userEmail)" + ng-click="setDefaultUserEmail(userEmail)" + type="button" + ) #{translate("make_primary")} + |   + +btnRemoveEmail() + tr.affiliations-table-saml-row(ng-if="userEmail.affiliation && userEmail.affiliation && userEmail.ssoAvailable") + td + td(ng-attr-colspan="{{userEmail.samlProviderId ? '2' : '1'}}" ng-class="institutionAlreadyLinked(userEmail) ? '' : 'with-border'") + p.small(ng-if="userEmail.samlProviderId") + | !{translate("acct_linked_to_institution_acct", {institutionName: '{{userEmail.affiliation.institution.name}}'})} + div(ng-if="!userEmail.samlProviderId && !institutionAlreadyLinked(userEmail)") + //- this email is not linked to institution login but + //- cannot have multiple emails at same institution linked for "institution login" + //- so check if institution is already linked + p.small + | !{translate("can_link_your_institution_acct", {institutionName: '{{userEmail.affiliation.institution.name}}'})} + p.small + | !{translate("doing_this_allow_log_in_through_institution")}  + +aboutInstitutionLink() + td.with-border.affiliations-table-inline-actions(ng-if="!userEmail.samlProviderId && !institutionAlreadyLinked(userEmail)") + button.btn-sm.btn.btn-info( + ng-click="linkInstitutionAcct(userEmail.email, userEmail.affiliation.institution.id)" + ng-disabled="ui.isMakingRequest" + type="button" + ) + | #{translate("link_accounts")} + tr( + class="reconfirm-row" + ng-if="userEmail.samlIdentifier && userEmail.samlIdentifier.providerId === reconfirmedViaSAML" + ) + td(colspan="3") + +reconfirmedAffiliationNotification() + tr( + class="reconfirm-row" + ng-repeat-end + ) + td( + colspan="3" + ng-if="userEmail.affiliation && userEmail.affiliation.inReconfirmNotificationPeriod" + ) + div(ng-class="{'alert alert-info': reconfirmationRemoveEmail === userEmail.email}") + +reconfirmAffiliationNotification('/user/settings') + + tr.affiliations-table-highlighted-row( + ng-if="!ui.showAddEmailUI && !ui.isMakingRequest" + ) + td(colspan="3") + a( + href + ng-click="showAddEmailForm()" + ) #{translate("add_another_email")} + + tr.affiliations-table-highlighted-row( + ng-if="ui.showAddEmailUI && !ui.isLoadingEmails" + ) + td + .affiliations-form-group + input-suggestions( + ng-model="newAffiliation.email" + ng-model-options="{ allowInvalid: true }" + get-suggestion="getEmailSuggestion(userInput)" + on-blur="handleEmailInputBlur()" + input-id="affilitations-email" + input-name="affilitationsEmail" + input-placeholder="e.g. johndoe@mit.edu" + input-type="email" + input-required="true" + ) + td( + colspan="2" + ng-if="newAffiliation.ssoAvailable" + ) + p.affiliations-table-label {{ newAffiliation.university.name }} + p !{translate("to_add_email_accounts_need_to_be_linked", {institutionName: "{{newAffiliation.university.name}}"})} + p !{translate("doing_this_will_verify_affiliation_and_allow_log_in", {institutionName: "{{newAffiliation.university.name}}"})}  + +aboutInstitutionLink() + button.btn-sm.btn.btn-primary.btn-link-accounts( + ng-click="linkInstitutionAcct(newAffiliation.email, newAffiliation.university.id)" + ng-disabled="ui.isMakingRequest" + type="button" + ) + | #{translate("link_accounts_and_add_email")} + td( + ng-if="!newAffiliation.ssoAvailable" + ) + p.affiliations-table-label( + ng-if="newAffiliation.university && !ui.showManualUniversitySelectionUI" + ) + | {{ newAffiliation.university.name }} + span.small + | ( + a( + href + ng-click="selectUniversityManually();" + ) #{translate("change")} + | ) + .affiliations-table-label( + ng-if="!newAffiliation.university && !ui.isValidEmail && !ui.showManualUniversitySelectionUI" + ) #{translate("start_by_adding_your_email")} + .affiliations-table-label( + ng-if="!newAffiliation.university && ui.isValidEmail && !ui.isBlacklistedEmail && !ui.showManualUniversitySelectionUI" + ) + | #{translate("is_email_affiliated")} + br + a( + href + ng-click="selectUniversityManually();" + ) #{translate("let_us_know")} + affiliation-form( + affiliation-data="newAffiliation" + show-university-and-country="ui.showManualUniversitySelectionUI" + show-role-and-department="ui.isValidEmail && newAffiliation.university" + ) + td( + ng-if="!newAffiliation.ssoAvailable" + ) + button.btn.btn-sm.btn-primary( + ng-disabled="affiliationsForm.$invalid || ui.isMakingRequest" + ng-click="addNewEmail()" + ) + | #{translate("add_new_email")} + tr.affiliations-table-highlighted-row( + ng-if="ui.isMakingRequest" + ) + td.text-center(colspan="3", ng-if="ui.isLoadingEmails") + i.fa.fa-fw.fa-spin.fa-refresh(aria-hidden="true") + |  #{translate("loading")}… + td.text-center(colspan="3", ng-if="ui.isResendingConfirmation") + i.fa.fa-fw.fa-spin.fa-refresh(aria-hidden="true") + |  #{translate("sending")}… + td.text-center.text-capitalize(colspan="3", ng-if="ui.isProcessing") + i.fa.fa-fw.fa-spin.fa-refresh(aria-hidden="true") + |  #{translate("processing")} + td.text-center(colspan="3", ng-if="!ui.isLoadingEmails && !ui.isResendingConfirmation && !ui.isProcessing") + i.fa.fa-fw.fa-spin.fa-refresh(aria-hidden="true") + |  #{translate("saving")} + tr.affiliations-table-error-row( + ng-if="ui.hasError" + ) + td.text-center(colspan="3") + div + i.fa.fa-fw.fa-exclamation-triangle(aria-hidden="true") + span(ng-if="!ui.errorMessage")  #{translate("error_performing_request")} + span(ng-if="ui.errorMessage")  {{ui.errorMessage}} + if institutionLinked + tr.affiliations-table-info-row(ng-if="!hideInstitutionNotifications.info") + td.text-center(aria-live="assertive" colspan="3") + button.close( + type="button" + data-dismiss="modal" + ng-click="closeInstitutionNotification('info')" + aria-label=translate("close") + ) + span(aria-hidden="true") × + .small(ng-non-bindable) !{translate("institution_acct_successfully_linked", {institutionName: institutionLinked.universityName})} + if institutionLinked.hasEntitlement + .small !{translate("this_grants_access_to_features", {featureType: translate("professional")})} + if institutionEmailNonCanonical + tr.affiliations-table-warning-row(ng-if="!hideInstitutionNotifications.warning") + td.text-center(aria-live="assertive" colspan="3") + button.close( + type="button" + data-dismiss="modal" + ng-click="closeInstitutionNotification('warning')" + aria-label=translate("close") + ) + span(aria-hidden="true") × + .small + i.fa.fa-exclamation-triangle(aria-hidden="true") + |   + | !{translate("in_order_to_match_institutional_metadata", {email: institutionEmailNonCanonical})} + + if samlError + tr.affiliations-table-error-row(ng-if="!hideInstitutionNotifications.linkError") + td.text-center(aria-live="assertive" colspan="3") + button.close( + type="button" + data-dismiss="modal" + ng-click="closeInstitutionNotification('linkError')" + aria-label=translate("close") + ) + span(aria-hidden="true") × + .small + i.fa.fa-fw.fa-exclamation-triangle(aria-hidden="true") + |  #{translate("generic_something_went_wrong")}. + br + if samlError.translatedMessage + span(ng-non-bindable) !{samlError.translatedMessage} + else if samlError.message + span(ng-non-bindable) #{samlError.message} + if samlError.tryAgain + br + |  #{translate("try_again")}. + +script(type="text/ng-template", id="affiliationFormTpl") + .affiliations-form-group( + ng-if="$ctrl.showUniversityAndCountry" + ) + ui-select( + ng-model="$ctrl.affiliationData.country" + ) + ui-select-match( + placeholder="Country" + ) {{ $select.selected.name }} + ui-select-choices( + repeat="country in $ctrl.countries | filter: $select.search" + ) + span( + ng-bind="country.name" + ) + .affiliations-form-group( + ng-if="$ctrl.showUniversityAndCountry" + ) + ui-select( + ng-model="$ctrl.affiliationData.university" + ng-disabled="!$ctrl.affiliationData.country" + tagging="$ctrl.addUniversityToSelection" + tagging-label="false" + ) + ui-select-match( + placeholder="Institution" + ) {{ $select.selected.name }} + ui-select-choices( + repeat="university in $ctrl.universities | filter: $select.search" + refresh="$ctrl.handleFreeformInputChange($select, 'name');" + refresh-delay="10" + ) + span( + ng-bind="university.name" + ) + .affiliations-form-group( + ng-if="$ctrl.showRoleAndDepartment" + ) + ui-select( + ng-model="$ctrl.affiliationData.role" + tagging + tagging-label="false" + ) + ui-select-match( + placeholder="Role" + ) {{ $select.selected }} + ui-select-choices( + repeat="role in $ctrl.roles | filter: $select.search" + refresh="$ctrl.handleFreeformInputChange($select);" + refresh-delay="10" + ) + span( + ng-bind="role" + ) + + .affiliations-form-group( + ng-if="$ctrl.showRoleAndDepartment" + ) + ui-select( + ng-model="$ctrl.affiliationData.department" + tagging + tagging-label="false" + ) + ui-select-match( + placeholder="Department" + ) {{ $select.selected }} + ui-select-choices( + repeat="department in $ctrl.departments | filter: $select.search" + refresh="$ctrl.handleFreeformInputChange($select);" + refresh-delay="10" + ) + span( + ng-bind="department" + ) diff --git a/services/web/app/views/user/settings/user-oauth.pug b/services/web/app/views/user/settings/user-oauth.pug new file mode 100644 index 0000000000..e384e16c38 --- /dev/null +++ b/services/web/app/views/user/settings/user-oauth.pug @@ -0,0 +1,41 @@ +mixin providerList() + ul.list-like-table + li(ng-repeat="(key, provider) in providers" ng-if="!provider.hideWhenNotLinked || (provider.hideWhenNotLinked && thirdPartyIds[key])") + .row + .col-xs-12.col-sm-8.col-md-10 + h4 {{provider.name}} + p.small(ng-bind-html="provider.description") + .col-xs-2.col-sm-4.col-md-2.text-right + //- Unlink + button.btn.btn-default( + ng-click="unlink(key)" + ng-disabled="providers[key].ui.isProcessing" + ng-if="thirdPartyIds[key]" + ) + span(ng-if="!providers[key].ui.isProcessing") #{translate("unlink")} + span(ng-if="providers[key].ui.isProcessing") #{translate("processing")} + //- Link + a.btn.btn-primary.text-capitalize( + ng-href="{{provider.linkPath}}?intent=link" + ng-if="!thirdPartyIds[key] && !provider.hideWhenNotLinked" + ) #{translate("link")} + //- unlink error + .row( + ng-if="providers[key].ui.hasError" + ) + .col-sm-12 + //- to do: fix CSS so that we don't need inline styling + .alert.alert-danger( + ng-if="providers[key].ui.hasError" + style="display: block; margin-bottom: 10px;" + ) + i.fa.fa-fw.fa-exclamation-triangle(aria-hidden="true")   + | {{providers[key].ui.errorMessage}} +.row( + ng-controller="UserOauthController" + ng-cloak +) + .col-xs-12 + h3.text-capitalize#linked-accounts #{translate("linked_accounts")} + p.small #{translate("linked_accounts_explained", {appName:'{{settings.appName}}'})} + +providerList() diff --git a/services/web/app/views/user_membership/index.pug b/services/web/app/views/user_membership/index.pug new file mode 100644 index 0000000000..ee0583e52b --- /dev/null +++ b/services/web/app/views/user_membership/index.pug @@ -0,0 +1,113 @@ +extends ../layout + +block append meta + meta(name="ol-users", data-type="json", content=users) + meta(name="ol-paths", data-type="json", content=paths) + meta(name="ol-groupSize", data-type="json", content=groupSize) + +block content + main.content.content-alt#main-content + .container + .row + .col-md-10.col-md-offset-1 + h1(ng-non-bindable) #{name || translate(translations.title)} + .card(ng-controller="UserMembershipController") + .page-header + .pull-right(ng-cloak) + small(ng-show="groupSize && selectedUsers.length == 0") !{translate("you_have_added_x_of_group_size_y", {addedUsersSize:'{{ users.length }}', groupSize: '{{ groupSize }}'}, ['strong', 'strong'])} + a.btn.btn-danger( + href, + ng-show="selectedUsers.length > 0" + ng-click="removeMembers()" + ) #{translate(translations.remove)} + h3 #{translate(translations.subtitle)} + + .row-spaced-small + div(ng-if="inputs.removeMembers.error", ng-cloak) + div.alert.alert-danger(ng-if="inputs.removeMembers.errorMessage") + | #{translate('error')}: + | {{ inputs.removeMembers.errorMessage }} + div.alert.alert-danger(ng-if="!inputs.removeMembers.errorMessage") + | #{translate('generic_something_went_wrong')} + ul.list-unstyled.structured-list( + select-all-list, + ng-cloak + ) + li.container-fluid + .row + .col-md-4 + input.select-all( + select-all, + type="checkbox" + ) + span.header #{translate("email")} + .col-md-4 + span.header #{translate("name")} + .col-md-2 + span.header #{translate("last_login")} + .col-md-2 + span.header #{translate("accepted_invite")} + li.container-fluid( + ng-repeat="user in users | orderBy:'email':true", + ng-controller="UserMembershipListItemController" + ) + .row + .col-md-4 + input.select-item( + select-individual, + type="checkbox", + ng-model="user.selected" + ) + span.email {{ user.email }} + .col-md-4 + span.name {{ user.first_name }} {{ user.last_name }} + .col-md-2 + span.lastLogin {{ user.last_logged_in_at | formatDate:'Do MMM YYYY' }} + .col-md-2 + span.registered + i.fa.fa-check.text-success(ng-show="!user.invite" aria-hidden="true") + span.sr-only(ng-show="!user.invite") #{translate('accepted_invite')} + i.fa.fa-times(ng-show="user.invite" aria-hidden="true") + span.sr-only(ng-show="user.invite") #{translate('invite_not_accepted')} + li( + ng-if="users.length == 0", + ng-cloak + ) + .row + .col-md-12.text-centered + small #{translate("no_members")} + + hr + div(ng-if="!groupSize || users.length < groupSize", ng-cloak) + p.small #{translate("add_more_members")} + div(ng-if="inputs.addMembers.error", ng-cloak) + div.alert.alert-danger(ng-if="inputs.addMembers.errorMessage") + | #{translate('error')}: + | {{ inputs.addMembers.errorMessage }} + div.alert.alert-danger(ng-if="!inputs.addMembers.errorMessage") + | #{translate('generic_something_went_wrong')} + form.form + .row + .col-xs-6 + input.form-control( + name="email", + type="text", + placeholder="jane@example.com, joe@example.com", + ng-model="inputs.addMembers.content", + on-enter="addMembers()" + aria-describedby="add-members-description" + ) + .col-xs-4 + button.btn.btn-primary(ng-click="addMembers()", ng-disabled="inputs.addMembers.inflightCount > 0") + span(ng-show="inputs.addMembers.inflightCount === 0") #{translate("add")} + span(ng-show="inputs.addMembers.inflightCount > 0") #{translate("adding")}… + .col-xs-2(ng-if="paths.exportMembers", ng-cloak) + a(href=paths.exportMembers) #{translate('export_csv')} + .row + .col-xs-8 + span.help-block #{translate('add_comma_separated_emails_help')} + + div(ng-if="groupSize && users.length >= groupSize && users.length > 0", ng-cloak) + .row + .col-xs-2.col-xs-offset-10(ng-if="paths.exportMembers", ng-cloak) + a(href=paths.exportMembers) #{translate('export_csv')} diff --git a/services/web/app/views/user_membership/new.pug b/services/web/app/views/user_membership/new.pug new file mode 100644 index 0000000000..30543da2e9 --- /dev/null +++ b/services/web/app/views/user_membership/new.pug @@ -0,0 +1,18 @@ +extends ../layout + +block content + main.content.content-alt#main-content + .container + .row + .col-md-10.col-md-offset-1 + h3(ng-non-bindable) #{entityName} "#{entityId}" does not exists in v2 + form( + enctype='application/json', + method='post', + action='' + ) + input(name="_csrf", type="hidden", value=csrfToken) + button.btn.btn-primary.text-capitalize( + type="submit", + ng-non-bindable + ) Create #{entityName} in v2 diff --git a/services/web/app/views/view_templates/bonus_templates.pug b/services/web/app/views/view_templates/bonus_templates.pug new file mode 100644 index 0000000000..58b4fc4973 --- /dev/null +++ b/services/web/app/views/view_templates/bonus_templates.pug @@ -0,0 +1,47 @@ +script(type="text/ng-template", id="BonusLinkToUsModal") + .modal-header + button.close( + type="button" + data-dismiss="modal" + ng-click="cancel()" + aria-label="Close" + ) + span(aria-hidden="true") × + h3 Dropbox link + .modal-body.modal-body-share + + div(ng-show="dbState.gotLinkStatus") + div(ng-hide="dbState.userIsLinkedToDropbox || !dbState.hasDropboxFeature") + + span(ng-hide="dbState.startedLinkProcess") Your account is not linked to dropbox + |     + a(ng-click="linkToDropbox()").btn.btn-info Update Dropbox Settings + + p.small.text-center(ng-show="dbState.startedLinkProcess") + | Please refresh this page after starting your free trial. + + + div(ng-show="dbState.hasDropboxFeature && dbState.userIsLinkedToDropbox") + progressbar.progress-striped.active(value='dbState.percentageLeftTillNextPoll', type="info") + span + strong {{dbState.minsTillNextPoll}} minutes + span until dropbox is next checked for changes. + + div.text-center(ng-hide="dbState.hasDropboxFeature") + p You need to upgrade your account to link to dropbox. + p + a.btn(ng-click="startFreeTrial('dropbox')", ng-class="buttonClass") Start Free Trial + p.small(ng-show="startedFreeTrial") + | Please refresh this page after starting your free trial. + + div(ng-hide="dbState.gotLinkStatus") + span.small   checking dropbox status   + i.fa.fa-refresh.fa-spin(aria-hidden="true") + + + + .modal-footer() + button.btn.btn-default( + ng-click="cancel()", + ) + span Dismiss diff --git a/services/web/babel.config.json b/services/web/babel.config.json new file mode 100644 index 0000000000..4a1da76e68 --- /dev/null +++ b/services/web/babel.config.json @@ -0,0 +1,40 @@ +{ + "presets": [ + [ + "@babel/react", + { + "runtime": "automatic" + } + ], + [ + "@babel/env", + { + "useBuiltIns": "usage", + "corejs": { "version": 3 } + } + ] + ], + "plugins": ["angularjs-annotate", "macros"], + // Target our current Node version in test environment, to transform and + // polyfill only what's necessary + "env": { + "test": { + "presets": [ + [ + "@babel/react", + { + "runtime": "automatic" + } + ], + [ + "@babel/env", + { + "targets": { "node": "12.21" }, + "useBuiltIns": "usage", + "corejs": { "version": 3 } + } + ] + ] + } + } +} diff --git a/services/web/bin/cdn_upload b/services/web/bin/cdn_upload new file mode 100755 index 0000000000..cdd3df2f50 --- /dev/null +++ b/services/web/bin/cdn_upload @@ -0,0 +1,61 @@ +#!/bin/bash +set -e + +function upload_with_content_type() { + content_type=$1 + bucket=$2 + shift 2 + content_type_options="" + if [[ "$content_type" != "-" ]]; then + content_type_options="-h Content-Type:${content_type};charset=utf-8" + fi + + # DOCS for gsutil -- it does not have long command line flags! + ## global flags + # -h NAME:VALUE add header, can occur multiples times + # -m upload with multiple threads + ## rsync flags + # -r traverse into directories recursively + # -x Python regex for excluding files from the sync + gsutil \ + -h "Cache-Control:public, max-age=31536000" \ + ${content_type_options} \ + -m \ + rsync \ + -r \ + "$@" \ + "/tmp/public/" \ + "${bucket}/public/" +} +function upload_into_bucket() { + bucket=$1 + + # stylesheets + upload_with_content_type 'text/css' "$bucket" \ + -x '.+(? $KNOWN_HOSTS + ssh-keygen -lf $KNOWN_HOSTS | grep "SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8 github.com" + git config --global user.name Copybot + git config --global user.email copybot@overleaf.com + set +e + copybara --git-committer-email=copybot@overleaf.com --git-committer-name=Copybot + COPYBARA_EXIT_CODE=$? + # Exit codes are documented in java/com/google/copybara/util/ExitCode.java + # 0 is success, 4 is no-op (i.e. no change), anything else is an error + if [[ $COPYBARA_EXIT_CODE -eq 0 || $COPYBARA_EXIT_CODE -eq 4 ]]; then + exit 0 + else + exit $COPYBARA_EXIT_CODE + fi +fi diff --git a/services/web/bin/lint_pug_templates b/services/web/bin/lint_pug_templates new file mode 100755 index 0000000000..6f255d9745 --- /dev/null +++ b/services/web/bin/lint_pug_templates @@ -0,0 +1,31 @@ +#!/bin/sh + +set -e + +TEMPLATES_EXTENDING_META_BLOCK=$(\ + grep \ + --files-with-matches \ + --recursive app/views modules/*/app/views \ + --regex 'block append meta' \ + --regex 'block prepend meta' \ + --regex 'append meta' \ + --regex 'prepend meta' \ +) + +for file in ${TEMPLATES_EXTENDING_META_BLOCK}; do + if ! grep "$file" --quiet --extended-regexp -e 'extends .+layout'; then + cat <&2 + +ERROR: $file is a partial template and extends 'block meta'. + +Using block append/prepend in a partial will duplicate the block contents into + the due to a bug in pug. +Putting meta tags in the can lead to Angular XSS. + +You will need to refactor the partial and move the block into the top level + page template that extends the global layout.pug. + +MSG + exit 1 + fi +done diff --git a/services/web/bin/push-translations-changes.sh b/services/web/bin/push-translations-changes.sh new file mode 100755 index 0000000000..6e0df59cd3 --- /dev/null +++ b/services/web/bin/push-translations-changes.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e + +if [[ -z "$BRANCH_NAME" ]]; then + BRANCH_NAME=master +fi + +if [[ `git status --porcelain=2 locales/` ]]; then + git add locales/* + git commit -m "auto update translation" + git push "$UPSTREAM_REPO" "HEAD:$BRANCH_NAME" +else + echo 'No changes' +fi diff --git a/services/web/bin/routes b/services/web/bin/routes new file mode 100755 index 0000000000..33bdeb713d --- /dev/null +++ b/services/web/bin/routes @@ -0,0 +1,129 @@ +#! /usr/bin/env node + +const acorn = require('acorn') +const acornWalk = require('acorn-walk') +const fs = require('fs') +const _ = require('lodash') +const glob = require('glob') +print = console.log + +const Methods = new Set([ + 'get', + 'head', + 'post', + 'put', + 'delete', + 'connect', + 'options', + 'trace', + 'patch' +]) + +const isMethod = str => { + return Methods.has(str) +} + +// Check if the expression is a call on a router, return data about it, or null +const routerCall = callExpression => { + const callee = callExpression.callee + const property = callee.property + const args = callExpression.arguments + if (!callee.object || !callee.object.name) { + return false + } + const routerName = callee.object.name + if ( // Match known names for the Express routers: app, webRouter, whateverRouter, etc... + isMethod(property.name) && + (routerName === 'app' || routerName.match('^.*[rR]outer$')) + ) { + return { + routerName: routerName, + method: property.name, + args: args + } + } else { + return null + } +} + +const formatMethodCall = expression => { + if (!expression.object || !expression.property) { + return '????' + } + return `${expression.object.name}.${expression.property.name}` +} + +const parseAndPrintRoutesSync = path => { + const content = fs.readFileSync(path) + // Walk the AST (Abstract Syntax Tree) + acornWalk.simple(acorn.parse(content), { + // We only care about call expression ( like `a.b()` ) + CallExpression(node) { + const call = routerCall(node) + if (call) { + const firstArg = _.first(call.args) + const lastArg = _.last(call.args) + try { + print( + ` ${formatRouterName(call.routerName)}\t .${call.method} \t: ${ + firstArg.value + } => ${formatMethodCall(lastArg)}` + ) + } catch (e) { + print('>> Error') + print(e) + print(JSON.stringify(call)) + process.exit(1) + } + } + } + }) +} + +const routerNameMapping = { + 'privateApiRouter': 'privateApi', + 'publicApiRouter': 'publicApi' +} +const formatRouterName = (name) => { + return routerNameMapping[name] || name +} + +const main = () => { + // Take an optional filter to apply to file names + const filter = process.argv[2] || null + + if (filter && (filter === '--help' || filter == 'help')) { + print('') + print(' Usage: bin/routes [filter]') + print(' Examples:') + print(' bin/routes') + print(' bin/routes GitBridge') + print('') + process.exit(0) + } + + // Find all routers + glob('*[rR]outer.js', { matchBase: true }, (err, files) => { + for (file of files) { + if (file.match('^node_modules.*$') || file.match('.*/public/.*')) { + continue + } + // Restrict to the filter (if filter is present) + if (filter && !file.match(`.*${filter}.*`)) { + continue + } + print(`[${file}]`) + try { + parseAndPrintRoutesSync(file) + } catch (_e) { + print('>> Error parsing file') + continue + } + } + process.exit(0) + }) +} + +if (require.main === module) { + main() +} diff --git a/services/web/bin/run b/services/web/bin/run new file mode 100755 index 0000000000..699e3f68c6 --- /dev/null +++ b/services/web/bin/run @@ -0,0 +1,8 @@ +#!/bin/bash + +pushd .. +bin/run $* +RV=$? +popd + +exit $RV diff --git a/services/web/bin/sentry_upload b/services/web/bin/sentry_upload new file mode 100755 index 0000000000..cf7b3cddb8 --- /dev/null +++ b/services/web/bin/sentry_upload @@ -0,0 +1,18 @@ +#!/bin/sh +set -e + +if [[ "$BRANCH_NAME" == "master" || "$BRANCH_NAME" == "main" ]]; then + rm -rf sentry_upload + mkdir sentry_upload + tar --directory sentry_upload -xf build.tar + cd sentry_upload/public + + SENTRY_RELEASE=${COMMIT_SHA} + OPTS="--no-rewrite --url-prefix ~" + sentry-cli releases new "$SENTRY_RELEASE" + sentry-cli releases files "$SENTRY_RELEASE" upload-sourcemaps ${OPTS} . + sentry-cli releases set-commits --auto --ignore-missing "$SENTRY_RELEASE" + sentry-cli releases finalize "$SENTRY_RELEASE" + + rm -rf sentry_upload +fi diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js new file mode 100644 index 0000000000..cc3f3438b8 --- /dev/null +++ b/services/web/config/settings.defaults.js @@ -0,0 +1,726 @@ +const { merge } = require('@overleaf/settings/merge') + +let defaultFeatures, siteUrl + +// Make time interval config easier. +const seconds = 1000 +const minutes = 60 * seconds + +// These credentials are used for authenticating api requests +// between services that may need to go over public channels +const httpAuthUser = process.env.WEB_API_USER +const httpAuthPass = process.env.WEB_API_PASSWORD +const httpAuthUsers = {} +if (httpAuthUser && httpAuthPass) { + httpAuthUsers[httpAuthUser] = httpAuthPass +} + +const sessionSecret = process.env.SESSION_SECRET || 'secret-please-change' + +const intFromEnv = function (name, defaultValue) { + if ( + [null, undefined].includes(defaultValue) || + typeof defaultValue !== 'number' + ) { + throw new Error( + `Bad default integer value for setting: ${name}, ${defaultValue}` + ) + } + return parseInt(process.env[name], 10) || defaultValue +} + +const defaultTextExtensions = [ + 'tex', + 'latex', + 'sty', + 'cls', + 'bst', + 'bib', + 'bibtex', + 'txt', + 'tikz', + 'mtx', + 'rtex', + 'md', + 'asy', + 'latexmkrc', + 'lbx', + 'bbx', + 'cbx', + 'm', + 'lco', + 'dtx', + 'ins', + 'ist', + 'def', + 'clo', + 'ldf', + 'rmd', + 'lua', + 'gv', + 'mf', + 'yml', + 'yaml', +] + +const parseTextExtensions = function (extensions) { + if (extensions) { + return extensions.split(',').map(ext => ext.trim()) + } else { + return [] + } +} + +module.exports = { + env: 'server-ce', + + limits: { + httpGlobalAgentMaxSockets: 300, + httpsGlobalAgentMaxSockets: 300, + }, + + allowAnonymousReadAndWriteSharing: + process.env.SHARELATEX_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING === 'true', + + // Databases + // --------- + mongo: { + options: { + appname: 'web', + useUnifiedTopology: + (process.env.MONGO_USE_UNIFIED_TOPOLOGY || 'true') === 'true', + poolSize: parseInt(process.env.MONGO_POOL_SIZE, 10) || 10, + serverSelectionTimeoutMS: + parseInt(process.env.MONGO_SERVER_SELECTION_TIMEOUT, 10) || 60000, + socketTimeoutMS: parseInt(process.env.MONGO_SOCKET_TIMEOUT, 10) || 30000, + }, + url: + process.env.MONGO_CONNECTION_STRING || + process.env.MONGO_URL || + `mongodb://${process.env.MONGO_HOST || '127.0.0.1'}/sharelatex`, + }, + + redis: { + web: { + host: process.env.REDIS_HOST || 'localhost', + port: process.env.REDIS_PORT || '6379', + password: process.env.REDIS_PASSWORD || '', + maxRetriesPerRequest: parseInt( + process.env.REDIS_MAX_RETRIES_PER_REQUEST || '20' + ), + }, + + // websessions: + // cluster: [ + // {host: 'localhost', port: 7000} + // {host: 'localhost', port: 7001} + // {host: 'localhost', port: 7002} + // {host: 'localhost', port: 7003} + // {host: 'localhost', port: 7004} + // {host: 'localhost', port: 7005} + // ] + + // ratelimiter: + // cluster: [ + // {host: 'localhost', port: 7000} + // {host: 'localhost', port: 7001} + // {host: 'localhost', port: 7002} + // {host: 'localhost', port: 7003} + // {host: 'localhost', port: 7004} + // {host: 'localhost', port: 7005} + // ] + + // cooldown: + // cluster: [ + // {host: 'localhost', port: 7000} + // {host: 'localhost', port: 7001} + // {host: 'localhost', port: 7002} + // {host: 'localhost', port: 7003} + // {host: 'localhost', port: 7004} + // {host: 'localhost', port: 7005} + // ] + + api: { + host: process.env.REDIS_HOST || 'localhost', + port: process.env.REDIS_PORT || '6379', + password: process.env.REDIS_PASSWORD || '', + maxRetriesPerRequest: parseInt( + process.env.REDIS_MAX_RETRIES_PER_REQUEST || '20' + ), + }, + }, + + // Service locations + // ----------------- + + // Configure which ports to run each service on. Generally you + // can leave these as they are unless you have some other services + // running which conflict, or want to run the web process on port 80. + internal: { + web: { + port: process.env.WEB_PORT || 3000, + host: process.env.LISTEN_ADDRESS || 'localhost', + }, + }, + + // Tell each service where to find the other services. If everything + // is running locally then this is easy, but they exist as separate config + // options incase you want to run some services on remote hosts. + apis: { + web: { + url: `http://${ + process.env.WEB_API_HOST || process.env.WEB_HOST || 'localhost' + }:${process.env.WEB_API_PORT || process.env.WEB_PORT || 3000}`, + user: httpAuthUser, + pass: httpAuthPass, + }, + documentupdater: { + url: `http://${ + process.env.DOCUPDATER_HOST || + process.env.DOCUMENT_UPDATER_HOST || + 'localhost' + }:3003`, + }, + spelling: { + url: `http://${process.env.SPELLING_HOST || 'localhost'}:3005`, + host: process.env.SPELLING_HOST, + }, + trackchanges: { + url: `http://${process.env.TRACK_CHANGES_HOST || 'localhost'}:3015`, + }, + docstore: { + url: `http://${process.env.DOCSTORE_HOST || 'localhost'}:3016`, + pubUrl: `http://${process.env.DOCSTORE_HOST || 'localhost'}:3016`, + }, + chat: { + internal_url: `http://${process.env.CHAT_HOST || 'localhost'}:3010`, + }, + filestore: { + url: `http://${process.env.FILESTORE_HOST || 'localhost'}:3009`, + }, + clsi: { + url: `http://${process.env.CLSI_HOST || 'localhost'}:3013`, + // url: "http://#{process.env['CLSI_LB_HOST']}:3014" + backendGroupName: undefined, + }, + realTime: { + url: `http://${process.env.REALTIME_HOST || 'localhost'}:3026`, + }, + contacts: { + url: `http://${process.env.CONTACTS_HOST || 'localhost'}:3036`, + }, + notifications: { + url: `http://${process.env.NOTIFICATIONS_HOST || 'localhost'}:3042`, + }, + + // For legacy reasons, we need to populate the below objects. + v1: {}, + recurly: {}, + }, + + splitTests: [], + + // Where your instance of ShareLaTeX can be found publically. Used in emails + // that are sent out, generated links, etc. + siteUrl: (siteUrl = process.env.PUBLIC_URL || 'http://localhost:3000'), + + lockManager: { + lockTestInterval: intFromEnv('LOCK_MANAGER_LOCK_TEST_INTERVAL', 50), + maxTestInterval: intFromEnv('LOCK_MANAGER_MAX_TEST_INTERVAL', 1000), + maxLockWaitTime: intFromEnv('LOCK_MANAGER_MAX_LOCK_WAIT_TIME', 10000), + redisLockExpiry: intFromEnv('LOCK_MANAGER_REDIS_LOCK_EXPIRY', 30), + slowExecutionThreshold: intFromEnv( + 'LOCK_MANAGER_SLOW_EXECUTION_THRESHOLD', + 5000 + ), + }, + + // Optional separate location for websocket connections, if unset defaults to siteUrl. + wsUrl: process.env.WEBSOCKET_URL, + wsUrlV2: process.env.WEBSOCKET_URL_V2, + wsUrlBeta: process.env.WEBSOCKET_URL_BETA, + + wsUrlV2Percentage: parseInt( + process.env.WEBSOCKET_URL_V2_PERCENTAGE || '0', + 10 + ), + wsRetryHandshake: parseInt(process.env.WEBSOCKET_RETRY_HANDSHAKE || '5', 10), + + // cookie domain + // use full domain for cookies to only be accessible from that domain, + // replace subdomain with dot to have them accessible on all subdomains + cookieDomain: process.env.COOKIE_DOMAIN, + cookieName: process.env.COOKIE_NAME || 'sharelatex.sid', + + // this is only used if cookies are used for clsi backend + // clsiCookieKey: "clsiserver" + + robotsNoindex: process.env.ROBOTS_NOINDEX === 'true' || false, + + maxEntitiesPerProject: 2000, + + maxUploadSize: 50 * 1024 * 1024, // 50 MB + + // start failing the health check if active handles exceeds this limit + maxActiveHandles: process.env.MAX_ACTIVE_HANDLES + ? parseInt(process.env.MAX_ACTIVE_HANDLES, 10) + : undefined, + + // Security + // -------- + security: { + sessionSecret, + bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS, 10) || 12, + }, // number of rounds used to hash user passwords (raised to power 2) + + httpAuthUsers, + + // Default features + // ---------------- + // + // You can select the features that are enabled by default for new + // new users. + defaultFeatures: (defaultFeatures = { + collaborators: -1, + dropbox: true, + github: true, + gitBridge: true, + versioning: true, + compileTimeout: 180, + compileGroup: 'standard', + references: true, + templates: true, + trackChanges: true, + }), + + features: { + personal: defaultFeatures, + }, + + plans: [ + { + planCode: 'personal', + name: 'Personal', + price: 0, + features: defaultFeatures, + }, + ], + + enableSubscriptions: false, + + enabledLinkedFileTypes: (process.env.ENABLED_LINKED_FILE_TYPES || '').split( + ',' + ), + + // i18n + // ------ + // + i18n: { + checkForHTMLInVars: process.env.I18N_CHECK_FOR_HTML_IN_VARS === 'true', + escapeHTMLInVars: process.env.I18N_ESCAPE_HTML_IN_VARS === 'true', + subdomainLang: { + www: { lngCode: 'en', url: siteUrl }, + }, + defaultLng: 'en', + }, + + // Spelling languages + // ------------------ + // + // You must have the corresponding aspell package installed to + // be able to use a language. + languages: [ + { code: 'en', name: 'English' }, + { code: 'en_US', name: 'English (American)' }, + { code: 'en_GB', name: 'English (British)' }, + { code: 'en_CA', name: 'English (Canadian)' }, + { code: 'af', name: 'Afrikaans' }, + { code: 'ar', name: 'Arabic' }, + { code: 'gl', name: 'Galician' }, + { code: 'eu', name: 'Basque' }, + { code: 'br', name: 'Breton' }, + { code: 'bg', name: 'Bulgarian' }, + { code: 'ca', name: 'Catalan' }, + { code: 'hr', name: 'Croatian' }, + { code: 'cs', name: 'Czech' }, + { code: 'da', name: 'Danish' }, + { code: 'nl', name: 'Dutch' }, + { code: 'eo', name: 'Esperanto' }, + { code: 'et', name: 'Estonian' }, + { code: 'fo', name: 'Faroese' }, + { code: 'fr', name: 'French' }, + { code: 'de', name: 'German' }, + { code: 'el', name: 'Greek' }, + { code: 'id', name: 'Indonesian' }, + { code: 'ga', name: 'Irish' }, + { code: 'it', name: 'Italian' }, + { code: 'kk', name: 'Kazakh' }, + { code: 'ku', name: 'Kurdish' }, + { code: 'lv', name: 'Latvian' }, + { code: 'lt', name: 'Lithuanian' }, + { code: 'nr', name: 'Ndebele' }, + { code: 'ns', name: 'Northern Sotho' }, + { code: 'no', name: 'Norwegian' }, + { code: 'fa', name: 'Persian' }, + { code: 'pl', name: 'Polish' }, + { code: 'pt_BR', name: 'Portuguese (Brazilian)' }, + { code: 'pt_PT', name: 'Portuguese (European)' }, + { code: 'pa', name: 'Punjabi' }, + { code: 'ro', name: 'Romanian' }, + { code: 'ru', name: 'Russian' }, + { code: 'sk', name: 'Slovak' }, + { code: 'sl', name: 'Slovenian' }, + { code: 'st', name: 'Southern Sotho' }, + { code: 'es', name: 'Spanish' }, + { code: 'sv', name: 'Swedish' }, + { code: 'tl', name: 'Tagalog' }, + { code: 'ts', name: 'Tsonga' }, + { code: 'tn', name: 'Tswana' }, + { code: 'hsb', name: 'Upper Sorbian' }, + { code: 'cy', name: 'Welsh' }, + { code: 'xh', name: 'Xhosa' }, + ], + + // Password Settings + // ----------- + // These restrict the passwords users can use when registering + // opts are from http://antelle.github.io/passfield + // passwordStrengthOptions: + // pattern: "aA$3" + // length: + // min: 6 + // max: 128 + + // Email support + // ------------- + // + // ShareLaTeX uses nodemailer (http://www.nodemailer.com/) to send transactional emails. + // To see the range of transport and options they support, see http://www.nodemailer.com/docs/transports + // email: + // fromAddress: "" + // replyTo: "" + // lifecycle: false + // # Example transport and parameter settings for Amazon SES + // transport: "SES" + // parameters: + // AWSAccessKeyID: "" + // AWSSecretKey: "" + + // For legacy reasons, we need to populate this object. + sentry: {}, + + // Production Settings + // ------------------- + debugPugTemplates: process.env.DEBUG_PUG_TEMPLATES === 'true', + precompilePugTemplatesAtBootTime: process.env + .PRECOMPILE_PUG_TEMPLATES_AT_BOOT_TIME + ? process.env.PRECOMPILE_PUG_TEMPLATES_AT_BOOT_TIME === 'true' + : process.env.NODE_ENV === 'production', + + // Should javascript assets be served minified or not. Note that you will + // need to run `grunt compile:minify` within the web-sharelatex directory + // to generate these. + useMinifiedJs: process.env.MINIFIED_JS === 'true' || false, + + // Should static assets be sent with a header to tell the browser to cache + // them. + cacheStaticAssets: false, + + // If you are running ShareLaTeX over https, set this to true to send the + // cookie with a secure flag (recommended). + secureCookie: false, + + // 'SameSite' cookie setting. Can be set to 'lax', 'none' or 'strict' + // 'lax' is recommended, as 'strict' will prevent people linking to projects + // https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7 + sameSiteCookie: 'lax', + + // If you are running ShareLaTeX behind a proxy (like Apache, Nginx, etc) + // then set this to true to allow it to correctly detect the forwarded IP + // address and http/https protocol information. + behindProxy: false, + + // Expose the hostname in the `X-Served-By` response header + exposeHostname: process.env.EXPOSE_HOSTNAME === 'true', + + // Cookie max age (in milliseconds). Set to false for a browser session. + cookieSessionLength: 5 * 24 * 60 * 60 * 1000, // 5 days + + // When true, only allow invites to be sent to email addresses that + // already have user accounts + restrictInvitesToExistingAccounts: false, + + // Should we allow access to any page without logging in? This includes + // public projects, /learn, /templates, about pages, etc. + allowPublicAccess: process.env.SHARELATEX_ALLOW_PUBLIC_ACCESS === 'true', + + // editor should be open by default + editorIsOpen: process.env.EDITOR_OPEN !== 'false', + + // site should be open by default + siteIsOpen: process.env.SITE_OPEN !== 'false', + + // Use a single compile directory for all users in a project + // (otherwise each user has their own directory) + // disablePerUserCompiles: true + + // Domain the client (pdfjs) should download the compiled pdf from + pdfDownloadDomain: process.env.PDF_DOWNLOAD_DOMAIN, // "http://clsi-lb:3014" + + // By default turn on feature flag, can be overridden per request. + enablePdfCaching: process.env.ENABLE_PDF_CACHING === 'true', + + // Whether to disable any existing service worker on the next load of the editor + resetServiceWorker: process.env.RESET_SERVICE_WORKER === 'true', + + // Maximum size of text documents in the real-time editing system. + max_doc_length: 2 * 1024 * 1024, // 2mb + + // Maximum JSON size in HTTP requests + // We should be able to process twice the max doc length, to allow for + // - the doc content + // - text ranges spanning the whole doc + // + // There's also overhead required for the JSON encoding and the UTF-8 encoding, + // theoretically up to 3 times the max doc length. On the other hand, we don't + // want to block the event loop with JSON parsing, so we try to find a + // practical compromise. + max_json_request_size: + parseInt(process.env.MAX_JSON_REQUEST_SIZE) || 6 * 1024 * 1024, // 6 MB + + // Internal configs + // ---------------- + path: { + // If we ever need to write something to disk (e.g. incoming requests + // that need processing but may be too big for memory, then write + // them to disk here). + dumpFolder: './data/dumpFolder', + uploadFolder: './data/uploads', + }, + + // Automatic Snapshots + // ------------------- + automaticSnapshots: { + // How long should we wait after the user last edited to + // take a snapshot? + waitTimeAfterLastEdit: 5 * minutes, + // Even if edits are still taking place, this is maximum + // time to wait before taking another snapshot. + maxTimeBetweenSnapshots: 30 * minutes, + }, + + // Smoke test + // ---------- + // Provide log in credentials and a project to be able to run + // some basic smoke tests to check the core functionality. + // + smokeTest: { + user: process.env.SMOKE_TEST_USER, + userId: process.env.SMOKE_TEST_USER_ID, + password: process.env.SMOKE_TEST_PASSWORD, + projectId: process.env.SMOKE_TEST_PROJECT_ID, + rateLimitSubject: process.env.SMOKE_TEST_RATE_LIMIT_SUBJECT || '127.0.0.1', + stepTimeout: parseInt(process.env.SMOKE_TEST_STEP_TIMEOUT || '10000', 10), + }, + + appName: process.env.APP_NAME || 'ShareLaTeX (Community Edition)', + + adminEmail: process.env.ADMIN_EMAIL || 'placeholder@example.com', + adminDomains: process.env.ADMIN_DOMAINS + ? JSON.parse(process.env.ADMIN_DOMAINS) + : undefined, + + nav: { + title: 'ShareLaTeX Community Edition', + + left_footer: [ + { + text: + "Powered by ShareLaTeX © 2016", + }, + ], + + right_footer: [ + { + text: " Fork on Github!", + url: 'https://github.com/sharelatex/sharelatex', + }, + ], + + showSubscriptionLink: false, + + header_extras: [], + }, + // Example: + // header_extras: [{text: "Some Page", url: "http://example.com/some/page", class: "subdued"}] + + recaptcha: { + disabled: { + invite: true, + login: true, + passwordReset: true, + register: true, + }, + }, + + customisation: {}, + + redirects: { + '/templates/index': '/templates/', + }, + + reloadModuleViewsOnEachRequest: process.env.NODE_ENV === 'development', + + rateLimit: { + autoCompile: { + everyone: process.env.RATE_LIMIT_AUTO_COMPILE_EVERYONE || 100, + standard: process.env.RATE_LIMIT_AUTO_COMPILE_STANDARD || 25, + }, + }, + + analytics: { + enabled: false, + }, + + compileBodySizeLimitMb: process.env.COMPILE_BODY_SIZE_LIMIT_MB || 5, + + textExtensions: defaultTextExtensions.concat( + parseTextExtensions(process.env.ADDITIONAL_TEXT_EXTENSIONS) + ), + + validRootDocExtensions: ['tex', 'Rtex', 'ltx'], + + emailConfirmationDisabled: + process.env.EMAIL_CONFIRMATION_DISABLED === 'true' || false, + + enabledServices: (process.env.ENABLED_SERVICES || 'web,api') + .split(',') + .map(s => s.trim()), + + // module options + // ---------- + modules: { + sanitize: { + options: { + allowedTags: [ + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'blockquote', + 'p', + 'a', + 'ul', + 'ol', + 'nl', + 'li', + 'b', + 'i', + 'strong', + 'em', + 'strike', + 'code', + 'hr', + 'br', + 'div', + 'table', + 'thead', + 'col', + 'caption', + 'tbody', + 'tr', + 'th', + 'td', + 'tfoot', + 'pre', + 'iframe', + 'img', + 'figure', + 'figcaption', + 'span', + 'source', + 'video', + 'del', + ], + allowedAttributes: { + a: [ + 'href', + 'name', + 'target', + 'class', + 'event-tracking', + 'event-tracking-ga', + 'event-tracking-label', + 'event-tracking-trigger', + ], + div: ['class', 'id', 'style'], + h1: ['class', 'id'], + h2: ['class', 'id'], + h3: ['class', 'id'], + h4: ['class', 'id'], + h5: ['class', 'id'], + h6: ['class', 'id'], + col: ['width'], + figure: ['class', 'id', 'style'], + figcaption: ['class', 'id', 'style'], + i: ['aria-hidden', 'aria-label', 'class', 'id'], + iframe: [ + 'allowfullscreen', + 'frameborder', + 'height', + 'src', + 'style', + 'width', + ], + img: ['alt', 'class', 'src', 'style'], + source: ['src', 'type'], + span: ['class', 'id', 'style'], + strong: ['style'], + table: ['border', 'class', 'id', 'style'], + td: ['colspan', 'rowspan', 'headers', 'style'], + th: [ + 'abbr', + 'headers', + 'colspan', + 'rowspan', + 'scope', + 'sorted', + 'style', + ], + tr: ['class'], + video: ['alt', 'class', 'controls', 'height', 'width'], + }, + }, + }, + }, + + overleafModuleImports: { + // modules to import (an empty array for each set of modules) + createFileModes: [], + gitBridge: [], + publishModal: [], + tprLinkedFileInfo: [], + tprLinkedFileRefreshError: [], + }, + + moduleImportSequence: ['launchpad', 'server-ce-scripts', 'user-activate'], + + csp: { + percentage: parseFloat(process.env.CSP_PERCENTAGE) || 0, + enabled: process.env.CSP_ENABLED === 'true', + reportOnly: process.env.CSP_REPORT_ONLY === 'true', + reportPercentage: parseFloat(process.env.CSP_REPORT_PERCENTAGE) || 0, + reportUri: process.env.CSP_REPORT_URI, + exclude: ['app/views/project/editor', 'app/views/project/list'], + }, + + unsupportedBrowsers: { + ie: '<=11', + }, +} + +module.exports.mergeWith = function (overrides) { + return merge(overrides, module.exports) +} diff --git a/services/web/data/.gitignore b/services/web/data/.gitignore new file mode 100644 index 0000000000..0fa27a178d --- /dev/null +++ b/services/web/data/.gitignore @@ -0,0 +1,4 @@ +gnore everything in this directory +* +# Except this file +!.gitignore diff --git a/services/web/data/dumpFolder/.gitignore b/services/web/data/dumpFolder/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/web/data/logs/.gitignore b/services/web/data/logs/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/web/data/pdfs/.gitignore b/services/web/data/pdfs/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/web/data/uploads/.gitignore b/services/web/data/uploads/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/web/data/zippedProjects/.gitignore b/services/web/data/zippedProjects/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/web/docker-compose.ci.yml b/services/web/docker-compose.ci.yml new file mode 100644 index 0000000000..c0fb7f58db --- /dev/null +++ b/services/web/docker-compose.ci.yml @@ -0,0 +1,81 @@ +version: "2.3" + +volumes: + data: + +services: + + test_unit: + build: . + image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + user: node + command: npm run test:unit:app + environment: + BASE_CONFIG: + SHARELATEX_CONFIG: + NODE_OPTIONS: "--unhandled-rejections=strict" + + test_acceptance: + build: . + image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + working_dir: /app + env_file: docker-compose.common.env + environment: + BASE_CONFIG: + SHARELATEX_CONFIG: + extra_hosts: + - 'www.overleaf.test:127.0.0.1' + command: npm run test:acceptance:app + user: root + depends_on: + - redis + - mongo + - saml + - ldap + + test_karma: + build: + context: . + dockerfile: Dockerfile.frontend.ci + args: + PROJECT_NAME: $PROJECT_NAME + BRANCH_NAME: $BRANCH_NAME + BUILD_NUMBER: $BUILD_NUMBER + working_dir: /app + command: npm run test:karma:single + user: node + environment: + NODE_OPTIONS: "--unhandled-rejections=strict" + + test_frontend: + build: . + image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + user: node + command: npm run test:frontend + environment: + NODE_OPTIONS: "--unhandled-rejections=strict" + + tar: + image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER-webpack + volumes: + - ./:/tmp/build/ + command: tar -cf /tmp/build/build.tar public/ + user: root + + redis: + image: redis + + mongo: + image: mongo:4.0.19 + + ldap: + restart: always + image: rroemhild/test-openldap:1.1 + + saml: + restart: always + image: gcr.io/overleaf-ops/saml-test + environment: + SAML_BASE_URL_PATH: 'http://saml/simplesaml/' + SAML_TEST_SP_ENTITY_ID: 'sharelatex-test-saml' + SAML_TEST_SP_LOCATION: 'http://www.overleaf.test:3000/saml/callback' diff --git a/services/web/docker-compose.common.env b/services/web/docker-compose.common.env new file mode 100644 index 0000000000..5281dee83a --- /dev/null +++ b/services/web/docker-compose.common.env @@ -0,0 +1,36 @@ +BCRYPT_ROUNDS=1 +REDIS_HOST=redis +QUEUES_REDIS_HOST=redis +MONGO_URL=mongodb://mongo/sharelatex +SHARELATEX_ALLOW_PUBLIC_ACCESS=true +PROJECT_HISTORY_ENABLED=true +LINKED_URL_PROXY=http://localhost:6543 +ENABLED_LINKED_FILE_TYPES=url,project_file,project_output_file,mendeley,zotero +NODE_ENV=test +NODE_OPTIONS=--unhandled-rejections=strict +LOCK_MANAGER_MAX_LOCK_WAIT_TIME=30000 +COOKIE_DOMAIN=.overleaf.test +PUBLIC_URL=http://www.overleaf.test:3000 +HTTP_TEST_HOST=www.overleaf.test +OT_JWT_AUTH_KEY=very secret key +# Server-Pro LDAP +SHARELATEX_LDAP_URL=ldap://ldap:389 +SHARELATEX_LDAP_SEARCH_BASE=ou=people,dc=planetexpress,dc=com +SHARELATEX_LDAP_SEARCH_FILTER=(uid={{username}}) +SHARELATEX_LDAP_BIND_DN=cn=admin,dc=planetexpress,dc=com +SHARELATEX_LDAP_BIND_CREDENTIALS=GoodNewsEveryone +SHARELATEX_LDAP_EMAIL_ATT=mail +SHARELATEX_LDAP_NAME_ATT=cn +SHARELATEX_LDAP_LAST_NAME_ATT=sn +SHARELATEX_LDAP_UPDATE_USER_DETAILS_ON_LOGIN=true +# Server-Pro SAML +SHARELATEX_SAML_ENTRYPOINT=http://saml/simplesaml/saml2/idp/SSOService.php +SHARELATEX_SAML_CALLBACK_URL=http://saml/saml/callback +SHARELATEX_SAML_ISSUER=sharelatex-test-saml +SHARELATEX_SAML_IDENTITY_SERVICE_NAME=SAML Test Server +SHARELATEX_SAML_EMAIL_FIELD=email +SHARELATEX_SAML_FIRST_NAME_FIELD=givenName +SHARELATEX_SAML_LAST_NAME_FIELD=sn +SHARELATEX_SAML_UPDATE_USER_DETAILS_ON_LOGIN=true +# simplesaml cert from https://github.com/overleaf/google-ops/tree/master/docker-images/saml-test/var-simplesamlphp/cert +SHARELATEX_SAML_CERT=MIIDXTCCAkWgAwIBAgIJAOvOeQ4xFTzsMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMTE1MTQxMjU5WhcNMjYxMTE1MTQxMjU5WjBFMQswCQYDVQQGEwJHQjETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCT6MBe5G9VoLU8MfztOEbUhnwLp17ak8eFUqxqeXkkqtWB0b/cmIBU3xoQoO3dIF8PBzfqehqfYVhrNt/TFgcmDfmJnPJRL1RJWMW3VmiP5odJ3LwlkKbZpkeT3wZ8HEJIR1+zbpxiBNkbd2GbdR1iumcsHzMYX1A2CBj+ZMV5VijC+K4P0e9c05VsDEUtLmfeAasJAiumQoVVgAe/BpiXjICGGewa6EPFI7mKkifIRKOGxdRESwZZjxP30bI31oDN0cgKqIgSJtJ9nfCn9jgBMBkQHu42WMuaWD4jrGd7+vYdX+oIfArs9aKgAH5kUGhGdew2R9SpBefrhbNxG8QIDAQABo1AwTjAdBgNVHQ4EFgQU+aSojSyyLChP/IpZcafvSdhj7KkwHwYDVR0jBBgwFoAU+aSojSyyLChP/IpZcafvSdhj7KkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEABl3+OOVLBWMKs6PjA8lPuloWDNzSr3v76oUcHqAb+cfbucjXrOVsS9RJ0X9yxvCQyfM9FfY43DbspnN3izYhdvbJD8kKLNf0LA5st+ZxLfy0ACyL2iyAwICaqndqxAjQYplFAHmpUiu1DiHckyBPekokDJd+ze95urHMOsaGS5RWPoKJVE0bkaAeZCmEu0NNpXRSBiuxXSTeSAJfv6kyE/rkdhzUKyUl/cGQFrsVYfAFQVA+W6CKOh74ErSEzSHQQYndl7nD33snD/YqdU1ROxV6aJzLKCg+sdj+wRXSP2u/UHnM4jW9TGJfhO42jzL6WVuEvr9q4l7zWzUQKKKhtQ== diff --git a/services/web/docker-compose.yml b/services/web/docker-compose.yml new file mode 100644 index 0000000000..95a6242e96 --- /dev/null +++ b/services/web/docker-compose.yml @@ -0,0 +1,86 @@ +version: "2.3" + +volumes: + data: + +services: + + test_unit: + build: + context: . + target: base + volumes: + - .:/app + working_dir: /app + environment: + BASE_CONFIG: + SHARELATEX_CONFIG: + MOCHA_GREP: ${MOCHA_GREP} + NODE_OPTIONS: "--unhandled-rejections=strict" + command: npm run --silent test:unit:app + user: node + + test_acceptance: + image: node:12.22.3 + volumes: + - .:/app + working_dir: /app + env_file: docker-compose.common.env + environment: + BASE_CONFIG: + SHARELATEX_CONFIG: + MOCHA_GREP: ${MOCHA_GREP} + MONGO_SERVER_SELECTION_TIMEOUT: 600000 + MONGO_SOCKET_TIMEOUT: 300000 + # SHARELATEX_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true' + + extra_hosts: + - 'www.overleaf.test:127.0.0.1' + depends_on: + - redis + - mongo + - saml + - ldap + command: npm run --silent test:acceptance:app + + test_karma: + build: + context: . + dockerfile: Dockerfile.frontend + volumes: + - .:/app + environment: + NODE_OPTIONS: "--unhandled-rejections=strict" + working_dir: /app + command: npm run --silent test:karma:single + + test_frontend: + build: + context: . + target: base + volumes: + - .:/app + working_dir: /app + environment: + MOCHA_GREP: ${MOCHA_GREP} + NODE_OPTIONS: "--unhandled-rejections=strict" + command: npm run --silent test:frontend + user: node + + redis: + image: redis + + mongo: + image: mongo:4.0.19 + + ldap: + restart: always + image: rroemhild/test-openldap:1.1 + + saml: + restart: always + image: gcr.io/overleaf-ops/saml-test + environment: + SAML_BASE_URL_PATH: 'http://saml/simplesaml/' + SAML_TEST_SP_ENTITY_ID: 'sharelatex-test-saml' + SAML_TEST_SP_LOCATION: 'http://www.overleaf.test:3000/saml/callback' diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json new file mode 100644 index 0000000000..21f5993997 --- /dev/null +++ b/services/web/frontend/extracted-translations.json @@ -0,0 +1,353 @@ +{ + "access_your_projects_with_git": "", + "account_not_linked_to_dropbox": "", + "account_settings": "", + "add_files": "", + "also": "", + "anyone_with_link_can_edit": "", + "anyone_with_link_can_view": "", + "ask_proj_owner_to_upgrade_for_git_bridge": "", + "ask_proj_owner_to_upgrade_for_longer_compiles": "", + "auto_compile": "", + "autocompile_disabled": "", + "autocompile_disabled_reason": "", + "autocomplete": "", + "autocomplete_references": "", + "back_to_your_projects": "", + "beta_badge_tooltip": "", + "blocked_filename": "", + "can_edit": "", + "cancel": "", + "cannot_invite_non_user": "", + "cannot_invite_self": "", + "cannot_verify_user_not_robot": "", + "category_arrows": "", + "category_greek": "", + "category_misc": "", + "category_operators": "", + "category_relations": "", + "change_or_cancel-cancel": "", + "change_or_cancel-change": "", + "change_or_cancel-or": "", + "change_owner": "", + "change_project_owner": "", + "chat": "", + "chat_error": "", + "checking_dropbox_status": "", + "checking_project_github_status": "", + "clear_cached_files": "", + "clone_with_git": "", + "close": "", + "clsi_maintenance": "", + "clsi_unavailable": "", + "code_check_failed": "", + "code_check_failed_explanation": "", + "collaborate_online_and_offline": "", + "collabs_per_proj": "", + "collapse": "", + "commit": "", + "common": "", + "compile_error_description": "", + "compile_error_entry_description": "", + "compile_larger_projects": "", + "compile_mode": "", + "compile_terminated_by_user": "", + "compiling": "", + "conflicting_paths_found": "", + "connected_users": "", + "continue_github_merge": "", + "copy": "", + "copy_project": "", + "copying": "", + "create": "", + "create_project_in_github": "", + "creating": "", + "delete": "", + "deleting": "", + "demonstrating_git_integration": "", + "description": "", + "dismiss": "", + "dismiss_error_popup": "", + "done": "", + "download": "", + "download_pdf": "", + "drag_here": "", + "dropbox_for_link_share_projs": "", + "dropbox_sync": "", + "duplicate_file": "", + "easily_manage_your_project_files_everywhere": "", + "editing": "", + "error": "", + "expand": "", + "export_project_to_github": "", + "fast": "", + "file_already_exists": "", + "file_already_exists_in_this_location": "", + "file_name": "", + "file_name_in_this_project": "", + "file_outline": "", + "files_cannot_include_invalid_characters": "", + "find_out_more_about_latex_symbols": "", + "find_out_more_about_the_file_outline": "", + "first_error_popup_label": "", + "following_paths_conflict": "", + "free_accounts_have_timeout_upgrade_to_increase": "", + "from_another_project": "", + "from_external_url": "", + "from_provider": "", + "full_doc_history": "", + "full_screen": "", + "generic_linked_file_compile_error": "", + "generic_something_went_wrong": "", + "get_collaborative_benefits": "", + "git_bridge_modal_description": "", + "github_commit_message_placeholder": "", + "github_credentials_expired": "", + "github_file_name_error": "", + "github_for_link_shared_projects": "", + "github_large_files_error": "", + "github_merge_failed": "", + "github_private_description": "", + "github_public_description": "", + "github_repository_diverged": "", + "github_symlink_error": "", + "github_sync": "", + "github_sync_error": "", + "github_sync_repository_not_found_description": "", + "github_timeout_error": "", + "github_too_many_files_error": "", + "github_validation_check": "", + "give_feedback": "", + "go_next_page": "", + "go_page": "", + "go_prev_page": "", + "go_to_error_location": "", + "have_an_extra_backup": "", + "headers": "", + "hide_outline": "", + "history": "", + "hotkey_add_a_comment": "", + "hotkey_autocomplete_menu": "", + "hotkey_beginning_of_document": "", + "hotkey_bold_text": "", + "hotkey_compile": "", + "hotkey_delete_current_line": "", + "hotkey_end_of_document": "", + "hotkey_find_and_replace": "", + "hotkey_go_to_line": "", + "hotkey_indent_selection": "", + "hotkey_insert_candidate": "", + "hotkey_italic_text": "", + "hotkey_redo": "", + "hotkey_search_references": "", + "hotkey_select_all": "", + "hotkey_select_candidate": "", + "hotkey_to_lowercase": "", + "hotkey_to_uppercase": "", + "hotkey_toggle_comment": "", + "hotkey_toggle_review_panel": "", + "hotkey_toggle_track_changes": "", + "hotkey_undo": "", + "hotkeys": "", + "if_error_persists_try_relinking_provider": "", + "ignore_validation_errors": "", + "imported_from_another_project_at_date": "", + "imported_from_external_provider_at_date": "", + "imported_from_mendeley_at_date": "", + "imported_from_the_output_of_another_project_at_date": "", + "imported_from_zotero_at_date": "", + "importing_and_merging_changes_in_github": "", + "invalid_email": "", + "invalid_file_name": "", + "invalid_filename": "", + "invalid_request": "", + "invite_not_accepted": "", + "learn_how_to_make_documents_compile_quickly": "", + "learn_more_about_link_sharing": "", + "link_sharing_is_off": "", + "link_sharing_is_on": "", + "link_to_github": "", + "link_to_github_description": "", + "link_to_mendeley": "", + "link_to_zotero": "", + "linked_file": "", + "loading": "", + "loading_recent_github_commits": "", + "log_entry_description": "", + "log_hint_extra_info": "", + "logs_pane_info_message": "", + "logs_pane_info_message_popup": "", + "main_file_not_found": "", + "make_private": "", + "manage_files_from_your_dropbox_folder": "", + "math_display": "", + "math_inline": "", + "maximum_files_uploaded_together": "", + "mendeley_groups_loading_error": "", + "mendeley_is_premium": "", + "mendeley_reference_loading_error": "", + "mendeley_reference_loading_error_expired": "", + "mendeley_reference_loading_error_forbidden": "", + "mendeley_sync_description": "", + "menu": "", + "n_errors": "", + "n_errors_plural": "", + "n_items": "", + "n_items_plural": "", + "n_warnings": "", + "n_warnings_plural": "", + "navigate_log_source": "", + "navigation": "", + "need_to_upgrade_for_more_collabs": "", + "new_file": "", + "new_folder": "", + "new_name": "", + "no_messages": "", + "no_new_commits_in_github": "", + "no_other_projects_found": "", + "no_pdf_error_explanation": "", + "no_pdf_error_reason_no_content": "", + "no_pdf_error_reason_output_pdf_already_exists": "", + "no_pdf_error_reason_unrecoverable_error": "", + "no_pdf_error_title": "", + "no_preview_available": "", + "no_search_results": "", + "no_symbols_found": "", + "showing_symbol_search_results": "", + "normal": "", + "off": "", + "ok": "", + "on": "", + "optional": "", + "or": "", + "other_logs_and_files": "", + "other_output_files": "", + "owner": "", + "page_current": "", + "pagination_navigation": "", + "pdf_compile_in_progress_error": "", + "pdf_compile_rate_limit_hit": "", + "pdf_compile_try_again": "", + "pdf_rendering_error": "", + "please_compile_pdf_before_download": "", + "please_refresh": "", + "please_select_a_file": "", + "please_select_a_project": "", + "please_select_an_output_file": "", + "please_set_main_file": "", + "plus_upgraded_accounts_receive": "", + "private": "", + "processing": "", + "proj_timed_out_reason": "", + "project_approaching_file_limit": "", + "project_flagged_too_many_compiles": "", + "project_has_too_many_files": "", + "project_not_linked_to_github": "", + "project_ownership_transfer_confirmation_1": "", + "project_ownership_transfer_confirmation_2": "", + "project_synced_with_git_repo_at": "", + "project_too_large": "", + "project_too_large_please_reduce": "", + "project_too_much_editable_text": "", + "public": "", + "pull_github_changes_into_sharelatex": "", + "push_sharelatex_changes_to_github": "", + "raw_logs": "", + "raw_logs_description": "", + "read_only": "", + "reauthorize_github_account": "", + "recent_commits_in_github": "", + "recompile": "", + "recompile_from_scratch": "", + "reconnect": "", + "reference_error_relink_hint": "", + "refresh": "", + "refresh_page_after_linking_dropbox": "", + "refresh_page_after_starting_free_trial": "", + "refreshing": "", + "remote_service_error": "", + "remove": "", + "remove_collaborator": "", + "rename": "", + "repository_name": "", + "resend": "", + "review": "", + "revoke": "", + "revoke_invite": "", + "run_syntax_check_now": "", + "search": "", + "select_a_file": "", + "select_a_project": "", + "select_an_output_file": "", + "select_from_output_files": "", + "select_from_source_files": "", + "select_from_your_computer": "", + "send_first_message": "", + "server_error": "", + "session_error": "", + "session_expired_redirecting_to_login": "", + "share": "", + "share_project": "", + "share_with_your_collabs": "", + "show_outline": "", + "something_went_wrong_rendering_pdf": "", + "something_went_wrong_server": "", + "somthing_went_wrong_compiling": "", + "split_screen": "", + "start_free_trial": "", + "stop_compile": "", + "stop_on_validation_error": "", + "store_your_work": "", + "submit_title": "", + "sure_you_want_to_delete": "", + "sync_project_to_github_explanation": "", + "sync_to_dropbox": "", + "sync_to_github": "", + "terminated": "", + "this_project_is_public": "", + "this_project_is_public_read_only": "", + "this_project_will_appear_in_your_dropbox_folder_at": "", + "timedout": "", + "to_add_more_collaborators": "", + "to_change_access_permissions": "", + "toggle_compile_options_menu": "", + "toggle_output_files_list": "", + "too_many_attempts": "", + "too_many_files_uploaded_throttled_short_period": "", + "too_many_requests": "", + "too_recently_compiled": "", + "total_words": "", + "try_it_for_free": "", + "turn_off_link_sharing": "", + "turn_on_link_sharing": "", + "unlimited_projects": "", + "unlink_github_repository": "", + "unlinking": "", + "update_dropbox_settings": "", + "upgrade": "", + "upgrade_for_longer_compiles": "", + "upload": "", + "url_to_fetch_the_file_from": "", + "use_your_own_machine": "", + "validation_issue_description": "", + "validation_issue_entry_description": "", + "view_error": "", + "view_error_plural": "", + "view_logs": "", + "view_pdf": "", + "view_warning": "", + "view_warning_plural": "", + "we_cant_find_any_sections_or_subsections_in_this_file": "", + "word_count": "", + "work_offline": "", + "work_with_non_overleaf_users": "", + "your_message": "", + "your_project_has_an_error": "", + "your_project_has_an_error_plural": "", + "zotero_groups_loading_error": "", + "zotero_is_premium": "", + "zotero_reference_loading_error": "", + "zotero_reference_loading_error_expired": "", + "zotero_reference_loading_error_forbidden": "", + "zotero_sync_description": "" +} diff --git a/services/web/frontend/fonts/STIXTwoMath/LICENSE.txt b/services/web/frontend/fonts/STIXTwoMath/LICENSE.txt new file mode 100644 index 0000000000..11b7b8e889 --- /dev/null +++ b/services/web/frontend/fonts/STIXTwoMath/LICENSE.txt @@ -0,0 +1,92 @@ +Copyright 2001-2021 The STIX Fonts Project Authors (https://github.com/stipub/stixfonts), with Reserved Font Name "TM Math". STIX Fontsâ„¢ is a trademark of The Institute of Electrical and Electronics Engineers, Inc. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/services/web/frontend/fonts/STIXTwoMath/STIXTwoMath-Regular.woff2 b/services/web/frontend/fonts/STIXTwoMath/STIXTwoMath-Regular.woff2 new file mode 100644 index 0000000000..76f8db85a4 Binary files /dev/null and b/services/web/frontend/fonts/STIXTwoMath/STIXTwoMath-Regular.woff2 differ diff --git a/services/web/frontend/fonts/font-awesome-v470.woff b/services/web/frontend/fonts/font-awesome-v470.woff new file mode 100644 index 0000000000..400014a4b0 Binary files /dev/null and b/services/web/frontend/fonts/font-awesome-v470.woff differ diff --git a/services/web/frontend/fonts/font-awesome-v470.woff2 b/services/web/frontend/fonts/font-awesome-v470.woff2 new file mode 100644 index 0000000000..4d13fc6040 Binary files /dev/null and b/services/web/frontend/fonts/font-awesome-v470.woff2 differ diff --git a/services/web/frontend/fonts/font-awesome.css b/services/web/frontend/fonts/font-awesome.css new file mode 100644 index 0000000000..5cbae8e590 --- /dev/null +++ b/services/web/frontend/fonts/font-awesome.css @@ -0,0 +1,2335 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +@font-face { + font-family: 'FontAwesome'; + src: url('font-awesome-v470.woff2') format('woff2'), + url('font-awesome-v470.woff') format('woff'); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.33333333em; + line-height: 0.75em; + vertical-align: -15%; +} +.fa-2x { + font-size: 2em; +} +.fa-3x { + font-size: 3em; +} +.fa-4x { + font-size: 4em; +} +.fa-5x { + font-size: 5em; +} +.fa-fw { + width: 1.28571429em; + text-align: center; +} +.fa-ul { + padding-left: 0; + margin-left: 2.14285714em; + list-style-type: none; +} +.fa-ul > li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.85714286em; +} +.fa-border { + padding: 0.2em 0.25em 0.15em; + border: solid 0.08em #eeeeee; + border-radius: 0.1em; +} +.fa-pull-left { + float: left; +} +.fa-pull-right { + float: right; +} +.fa.fa-pull-left { + margin-right: 0.3em; +} +.fa.fa-pull-right { + margin-left: 0.3em; +} +/* Deprecated as of 4.4.0 */ +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: 0.3em; +} +.fa.pull-right { + margin-left: 0.3em; +} +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + -ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=1)'; + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + -ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=2)'; + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + -ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=3)'; + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + -ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)'; + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + -ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)'; + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none; +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: '\f000'; +} +.fa-music:before { + content: '\f001'; +} +.fa-search:before { + content: '\f002'; +} +.fa-envelope-o:before { + content: '\f003'; +} +.fa-heart:before { + content: '\f004'; +} +.fa-star:before { + content: '\f005'; +} +.fa-star-o:before { + content: '\f006'; +} +.fa-user:before { + content: '\f007'; +} +.fa-film:before { + content: '\f008'; +} +.fa-th-large:before { + content: '\f009'; +} +.fa-th:before { + content: '\f00a'; +} +.fa-th-list:before { + content: '\f00b'; +} +.fa-check:before { + content: '\f00c'; +} +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: '\f00d'; +} +.fa-search-plus:before { + content: '\f00e'; +} +.fa-search-minus:before { + content: '\f010'; +} +.fa-power-off:before { + content: '\f011'; +} +.fa-signal:before { + content: '\f012'; +} +.fa-gear:before, +.fa-cog:before { + content: '\f013'; +} +.fa-trash-o:before { + content: '\f014'; +} +.fa-home:before { + content: '\f015'; +} +.fa-file-o:before { + content: '\f016'; +} +.fa-clock-o:before { + content: '\f017'; +} +.fa-road:before { + content: '\f018'; +} +.fa-download:before { + content: '\f019'; +} +.fa-arrow-circle-o-down:before { + content: '\f01a'; +} +.fa-arrow-circle-o-up:before { + content: '\f01b'; +} +.fa-inbox:before { + content: '\f01c'; +} +.fa-play-circle-o:before { + content: '\f01d'; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: '\f01e'; +} +.fa-refresh:before { + content: '\f021'; +} +.fa-list-alt:before { + content: '\f022'; +} +.fa-lock:before { + content: '\f023'; +} +.fa-flag:before { + content: '\f024'; +} +.fa-headphones:before { + content: '\f025'; +} +.fa-volume-off:before { + content: '\f026'; +} +.fa-volume-down:before { + content: '\f027'; +} +.fa-volume-up:before { + content: '\f028'; +} +.fa-qrcode:before { + content: '\f029'; +} +.fa-barcode:before { + content: '\f02a'; +} +.fa-tag:before { + content: '\f02b'; +} +.fa-tags:before { + content: '\f02c'; +} +.fa-book:before { + content: '\f02d'; +} +.fa-bookmark:before { + content: '\f02e'; +} +.fa-print:before { + content: '\f02f'; +} +.fa-camera:before { + content: '\f030'; +} +.fa-font:before { + content: '\f031'; +} +.fa-bold:before { + content: '\f032'; +} +.fa-italic:before { + content: '\f033'; +} +.fa-text-height:before { + content: '\f034'; +} +.fa-text-width:before { + content: '\f035'; +} +.fa-align-left:before { + content: '\f036'; +} +.fa-align-center:before { + content: '\f037'; +} +.fa-align-right:before { + content: '\f038'; +} +.fa-align-justify:before { + content: '\f039'; +} +.fa-list:before { + content: '\f03a'; +} +.fa-dedent:before, +.fa-outdent:before { + content: '\f03b'; +} +.fa-indent:before { + content: '\f03c'; +} +.fa-video-camera:before { + content: '\f03d'; +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: '\f03e'; +} +.fa-pencil:before { + content: '\f040'; +} +.fa-map-marker:before { + content: '\f041'; +} +.fa-adjust:before { + content: '\f042'; +} +.fa-tint:before { + content: '\f043'; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: '\f044'; +} +.fa-share-square-o:before { + content: '\f045'; +} +.fa-check-square-o:before { + content: '\f046'; +} +.fa-arrows:before { + content: '\f047'; +} +.fa-step-backward:before { + content: '\f048'; +} +.fa-fast-backward:before { + content: '\f049'; +} +.fa-backward:before { + content: '\f04a'; +} +.fa-play:before { + content: '\f04b'; +} +.fa-pause:before { + content: '\f04c'; +} +.fa-stop:before { + content: '\f04d'; +} +.fa-forward:before { + content: '\f04e'; +} +.fa-fast-forward:before { + content: '\f050'; +} +.fa-step-forward:before { + content: '\f051'; +} +.fa-eject:before { + content: '\f052'; +} +.fa-chevron-left:before { + content: '\f053'; +} +.fa-chevron-right:before { + content: '\f054'; +} +.fa-plus-circle:before { + content: '\f055'; +} +.fa-minus-circle:before { + content: '\f056'; +} +.fa-times-circle:before { + content: '\f057'; +} +.fa-check-circle:before { + content: '\f058'; +} +.fa-question-circle:before { + content: '\f059'; +} +.fa-info-circle:before { + content: '\f05a'; +} +.fa-crosshairs:before { + content: '\f05b'; +} +.fa-times-circle-o:before { + content: '\f05c'; +} +.fa-check-circle-o:before { + content: '\f05d'; +} +.fa-ban:before { + content: '\f05e'; +} +.fa-arrow-left:before { + content: '\f060'; +} +.fa-arrow-right:before { + content: '\f061'; +} +.fa-arrow-up:before { + content: '\f062'; +} +.fa-arrow-down:before { + content: '\f063'; +} +.fa-mail-forward:before, +.fa-share:before { + content: '\f064'; +} +.fa-expand:before { + content: '\f065'; +} +.fa-compress:before { + content: '\f066'; +} +.fa-plus:before { + content: '\f067'; +} +.fa-minus:before { + content: '\f068'; +} +.fa-asterisk:before { + content: '\f069'; +} +.fa-exclamation-circle:before { + content: '\f06a'; +} +.fa-gift:before { + content: '\f06b'; +} +.fa-leaf:before { + content: '\f06c'; +} +.fa-fire:before { + content: '\f06d'; +} +.fa-eye:before { + content: '\f06e'; +} +.fa-eye-slash:before { + content: '\f070'; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: '\f071'; +} +.fa-plane:before { + content: '\f072'; +} +.fa-calendar:before { + content: '\f073'; +} +.fa-random:before { + content: '\f074'; +} +.fa-comment:before { + content: '\f075'; +} +.fa-magnet:before { + content: '\f076'; +} +.fa-chevron-up:before { + content: '\f077'; +} +.fa-chevron-down:before { + content: '\f078'; +} +.fa-retweet:before { + content: '\f079'; +} +.fa-shopping-cart:before { + content: '\f07a'; +} +.fa-folder:before { + content: '\f07b'; +} +.fa-folder-open:before { + content: '\f07c'; +} +.fa-arrows-v:before { + content: '\f07d'; +} +.fa-arrows-h:before { + content: '\f07e'; +} +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: '\f080'; +} +.fa-twitter-square:before { + content: '\f081'; +} +.fa-facebook-square:before { + content: '\f082'; +} +.fa-camera-retro:before { + content: '\f083'; +} +.fa-key:before { + content: '\f084'; +} +.fa-gears:before, +.fa-cogs:before { + content: '\f085'; +} +.fa-comments:before { + content: '\f086'; +} +.fa-thumbs-o-up:before { + content: '\f087'; +} +.fa-thumbs-o-down:before { + content: '\f088'; +} +.fa-star-half:before { + content: '\f089'; +} +.fa-heart-o:before { + content: '\f08a'; +} +.fa-sign-out:before { + content: '\f08b'; +} +.fa-linkedin-square:before { + content: '\f08c'; +} +.fa-thumb-tack:before { + content: '\f08d'; +} +.fa-external-link:before { + content: '\f08e'; +} +.fa-sign-in:before { + content: '\f090'; +} +.fa-trophy:before { + content: '\f091'; +} +.fa-github-square:before { + content: '\f092'; +} +.fa-upload:before { + content: '\f093'; +} +.fa-lemon-o:before { + content: '\f094'; +} +.fa-phone:before { + content: '\f095'; +} +.fa-square-o:before { + content: '\f096'; +} +.fa-bookmark-o:before { + content: '\f097'; +} +.fa-phone-square:before { + content: '\f098'; +} +.fa-twitter:before { + content: '\f099'; +} +.fa-facebook-f:before, +.fa-facebook:before { + content: '\f09a'; +} +.fa-github:before { + content: '\f09b'; +} +.fa-unlock:before { + content: '\f09c'; +} +.fa-credit-card:before { + content: '\f09d'; +} +.fa-feed:before, +.fa-rss:before { + content: '\f09e'; +} +.fa-hdd-o:before { + content: '\f0a0'; +} +.fa-bullhorn:before { + content: '\f0a1'; +} +.fa-bell:before { + content: '\f0f3'; +} +.fa-certificate:before { + content: '\f0a3'; +} +.fa-hand-o-right:before { + content: '\f0a4'; +} +.fa-hand-o-left:before { + content: '\f0a5'; +} +.fa-hand-o-up:before { + content: '\f0a6'; +} +.fa-hand-o-down:before { + content: '\f0a7'; +} +.fa-arrow-circle-left:before { + content: '\f0a8'; +} +.fa-arrow-circle-right:before { + content: '\f0a9'; +} +.fa-arrow-circle-up:before { + content: '\f0aa'; +} +.fa-arrow-circle-down:before { + content: '\f0ab'; +} +.fa-globe:before { + content: '\f0ac'; +} +.fa-wrench:before { + content: '\f0ad'; +} +.fa-tasks:before { + content: '\f0ae'; +} +.fa-filter:before { + content: '\f0b0'; +} +.fa-briefcase:before { + content: '\f0b1'; +} +.fa-arrows-alt:before { + content: '\f0b2'; +} +.fa-group:before, +.fa-users:before { + content: '\f0c0'; +} +.fa-chain:before, +.fa-link:before { + content: '\f0c1'; +} +.fa-cloud:before { + content: '\f0c2'; +} +.fa-flask:before { + content: '\f0c3'; +} +.fa-cut:before, +.fa-scissors:before { + content: '\f0c4'; +} +.fa-copy:before, +.fa-files-o:before { + content: '\f0c5'; +} +.fa-paperclip:before { + content: '\f0c6'; +} +.fa-save:before, +.fa-floppy-o:before { + content: '\f0c7'; +} +.fa-square:before { + content: '\f0c8'; +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: '\f0c9'; +} +.fa-list-ul:before { + content: '\f0ca'; +} +.fa-list-ol:before { + content: '\f0cb'; +} +.fa-strikethrough:before { + content: '\f0cc'; +} +.fa-underline:before { + content: '\f0cd'; +} +.fa-table:before { + content: '\f0ce'; +} +.fa-magic:before { + content: '\f0d0'; +} +.fa-truck:before { + content: '\f0d1'; +} +.fa-pinterest:before { + content: '\f0d2'; +} +.fa-pinterest-square:before { + content: '\f0d3'; +} +.fa-google-plus-square:before { + content: '\f0d4'; +} +.fa-google-plus:before { + content: '\f0d5'; +} +.fa-money:before { + content: '\f0d6'; +} +.fa-caret-down:before { + content: '\f0d7'; +} +.fa-caret-up:before { + content: '\f0d8'; +} +.fa-caret-left:before { + content: '\f0d9'; +} +.fa-caret-right:before { + content: '\f0da'; +} +.fa-columns:before { + content: '\f0db'; +} +.fa-unsorted:before, +.fa-sort:before { + content: '\f0dc'; +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: '\f0dd'; +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: '\f0de'; +} +.fa-envelope:before { + content: '\f0e0'; +} +.fa-linkedin:before { + content: '\f0e1'; +} +.fa-rotate-left:before, +.fa-undo:before { + content: '\f0e2'; +} +.fa-legal:before, +.fa-gavel:before { + content: '\f0e3'; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: '\f0e4'; +} +.fa-comment-o:before { + content: '\f0e5'; +} +.fa-comments-o:before { + content: '\f0e6'; +} +.fa-flash:before, +.fa-bolt:before { + content: '\f0e7'; +} +.fa-sitemap:before { + content: '\f0e8'; +} +.fa-umbrella:before { + content: '\f0e9'; +} +.fa-paste:before, +.fa-clipboard:before { + content: '\f0ea'; +} +.fa-lightbulb-o:before { + content: '\f0eb'; +} +.fa-exchange:before { + content: '\f0ec'; +} +.fa-cloud-download:before { + content: '\f0ed'; +} +.fa-cloud-upload:before { + content: '\f0ee'; +} +.fa-user-md:before { + content: '\f0f0'; +} +.fa-stethoscope:before { + content: '\f0f1'; +} +.fa-suitcase:before { + content: '\f0f2'; +} +.fa-bell-o:before { + content: '\f0a2'; +} +.fa-coffee:before { + content: '\f0f4'; +} +.fa-cutlery:before { + content: '\f0f5'; +} +.fa-file-text-o:before { + content: '\f0f6'; +} +.fa-building-o:before { + content: '\f0f7'; +} +.fa-hospital-o:before { + content: '\f0f8'; +} +.fa-ambulance:before { + content: '\f0f9'; +} +.fa-medkit:before { + content: '\f0fa'; +} +.fa-fighter-jet:before { + content: '\f0fb'; +} +.fa-beer:before { + content: '\f0fc'; +} +.fa-h-square:before { + content: '\f0fd'; +} +.fa-plus-square:before { + content: '\f0fe'; +} +.fa-angle-double-left:before { + content: '\f100'; +} +.fa-angle-double-right:before { + content: '\f101'; +} +.fa-angle-double-up:before { + content: '\f102'; +} +.fa-angle-double-down:before { + content: '\f103'; +} +.fa-angle-left:before { + content: '\f104'; +} +.fa-angle-right:before { + content: '\f105'; +} +.fa-angle-up:before { + content: '\f106'; +} +.fa-angle-down:before { + content: '\f107'; +} +.fa-desktop:before { + content: '\f108'; +} +.fa-laptop:before { + content: '\f109'; +} +.fa-tablet:before { + content: '\f10a'; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: '\f10b'; +} +.fa-circle-o:before { + content: '\f10c'; +} +.fa-quote-left:before { + content: '\f10d'; +} +.fa-quote-right:before { + content: '\f10e'; +} +.fa-spinner:before { + content: '\f110'; +} +.fa-circle:before { + content: '\f111'; +} +.fa-mail-reply:before, +.fa-reply:before { + content: '\f112'; +} +.fa-github-alt:before { + content: '\f113'; +} +.fa-folder-o:before { + content: '\f114'; +} +.fa-folder-open-o:before { + content: '\f115'; +} +.fa-smile-o:before { + content: '\f118'; +} +.fa-frown-o:before { + content: '\f119'; +} +.fa-meh-o:before { + content: '\f11a'; +} +.fa-gamepad:before { + content: '\f11b'; +} +.fa-keyboard-o:before { + content: '\f11c'; +} +.fa-flag-o:before { + content: '\f11d'; +} +.fa-flag-checkered:before { + content: '\f11e'; +} +.fa-terminal:before { + content: '\f120'; +} +.fa-code:before { + content: '\f121'; +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: '\f122'; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: '\f123'; +} +.fa-location-arrow:before { + content: '\f124'; +} +.fa-crop:before { + content: '\f125'; +} +.fa-code-fork:before { + content: '\f126'; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: '\f127'; +} +.fa-question:before { + content: '\f128'; +} +.fa-info:before { + content: '\f129'; +} +.fa-exclamation:before { + content: '\f12a'; +} +.fa-superscript:before { + content: '\f12b'; +} +.fa-subscript:before { + content: '\f12c'; +} +.fa-eraser:before { + content: '\f12d'; +} +.fa-puzzle-piece:before { + content: '\f12e'; +} +.fa-microphone:before { + content: '\f130'; +} +.fa-microphone-slash:before { + content: '\f131'; +} +.fa-shield:before { + content: '\f132'; +} +.fa-calendar-o:before { + content: '\f133'; +} +.fa-fire-extinguisher:before { + content: '\f134'; +} +.fa-rocket:before { + content: '\f135'; +} +.fa-maxcdn:before { + content: '\f136'; +} +.fa-chevron-circle-left:before { + content: '\f137'; +} +.fa-chevron-circle-right:before { + content: '\f138'; +} +.fa-chevron-circle-up:before { + content: '\f139'; +} +.fa-chevron-circle-down:before { + content: '\f13a'; +} +.fa-html5:before { + content: '\f13b'; +} +.fa-css3:before { + content: '\f13c'; +} +.fa-anchor:before { + content: '\f13d'; +} +.fa-unlock-alt:before { + content: '\f13e'; +} +.fa-bullseye:before { + content: '\f140'; +} +.fa-ellipsis-h:before { + content: '\f141'; +} +.fa-ellipsis-v:before { + content: '\f142'; +} +.fa-rss-square:before { + content: '\f143'; +} +.fa-play-circle:before { + content: '\f144'; +} +.fa-ticket:before { + content: '\f145'; +} +.fa-minus-square:before { + content: '\f146'; +} +.fa-minus-square-o:before { + content: '\f147'; +} +.fa-level-up:before { + content: '\f148'; +} +.fa-level-down:before { + content: '\f149'; +} +.fa-check-square:before { + content: '\f14a'; +} +.fa-pencil-square:before { + content: '\f14b'; +} +.fa-external-link-square:before { + content: '\f14c'; +} +.fa-share-square:before { + content: '\f14d'; +} +.fa-compass:before { + content: '\f14e'; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: '\f150'; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: '\f151'; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: '\f152'; +} +.fa-euro:before, +.fa-eur:before { + content: '\f153'; +} +.fa-gbp:before { + content: '\f154'; +} +.fa-dollar:before, +.fa-usd:before { + content: '\f155'; +} +.fa-rupee:before, +.fa-inr:before { + content: '\f156'; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: '\f157'; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: '\f158'; +} +.fa-won:before, +.fa-krw:before { + content: '\f159'; +} +.fa-bitcoin:before, +.fa-btc:before { + content: '\f15a'; +} +.fa-file:before { + content: '\f15b'; +} +.fa-file-text:before { + content: '\f15c'; +} +.fa-sort-alpha-asc:before { + content: '\f15d'; +} +.fa-sort-alpha-desc:before { + content: '\f15e'; +} +.fa-sort-amount-asc:before { + content: '\f160'; +} +.fa-sort-amount-desc:before { + content: '\f161'; +} +.fa-sort-numeric-asc:before { + content: '\f162'; +} +.fa-sort-numeric-desc:before { + content: '\f163'; +} +.fa-thumbs-up:before { + content: '\f164'; +} +.fa-thumbs-down:before { + content: '\f165'; +} +.fa-youtube-square:before { + content: '\f166'; +} +.fa-youtube:before { + content: '\f167'; +} +.fa-xing:before { + content: '\f168'; +} +.fa-xing-square:before { + content: '\f169'; +} +.fa-youtube-play:before { + content: '\f16a'; +} +.fa-dropbox:before { + content: '\f16b'; +} +.fa-stack-overflow:before { + content: '\f16c'; +} +.fa-instagram:before { + content: '\f16d'; +} +.fa-flickr:before { + content: '\f16e'; +} +.fa-adn:before { + content: '\f170'; +} +.fa-bitbucket:before { + content: '\f171'; +} +.fa-bitbucket-square:before { + content: '\f172'; +} +.fa-tumblr:before { + content: '\f173'; +} +.fa-tumblr-square:before { + content: '\f174'; +} +.fa-long-arrow-down:before { + content: '\f175'; +} +.fa-long-arrow-up:before { + content: '\f176'; +} +.fa-long-arrow-left:before { + content: '\f177'; +} +.fa-long-arrow-right:before { + content: '\f178'; +} +.fa-apple:before { + content: '\f179'; +} +.fa-windows:before { + content: '\f17a'; +} +.fa-android:before { + content: '\f17b'; +} +.fa-linux:before { + content: '\f17c'; +} +.fa-dribbble:before { + content: '\f17d'; +} +.fa-skype:before { + content: '\f17e'; +} +.fa-foursquare:before { + content: '\f180'; +} +.fa-trello:before { + content: '\f181'; +} +.fa-female:before { + content: '\f182'; +} +.fa-male:before { + content: '\f183'; +} +.fa-gittip:before, +.fa-gratipay:before { + content: '\f184'; +} +.fa-sun-o:before { + content: '\f185'; +} +.fa-moon-o:before { + content: '\f186'; +} +.fa-archive:before { + content: '\f187'; +} +.fa-bug:before { + content: '\f188'; +} +.fa-vk:before { + content: '\f189'; +} +.fa-weibo:before { + content: '\f18a'; +} +.fa-renren:before { + content: '\f18b'; +} +.fa-pagelines:before { + content: '\f18c'; +} +.fa-stack-exchange:before { + content: '\f18d'; +} +.fa-arrow-circle-o-right:before { + content: '\f18e'; +} +.fa-arrow-circle-o-left:before { + content: '\f190'; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: '\f191'; +} +.fa-dot-circle-o:before { + content: '\f192'; +} +.fa-wheelchair:before { + content: '\f193'; +} +.fa-vimeo-square:before { + content: '\f194'; +} +.fa-turkish-lira:before, +.fa-try:before { + content: '\f195'; +} +.fa-plus-square-o:before { + content: '\f196'; +} +.fa-space-shuttle:before { + content: '\f197'; +} +.fa-slack:before { + content: '\f198'; +} +.fa-envelope-square:before { + content: '\f199'; +} +.fa-wordpress:before { + content: '\f19a'; +} +.fa-openid:before { + content: '\f19b'; +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: '\f19c'; +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: '\f19d'; +} +.fa-yahoo:before { + content: '\f19e'; +} +.fa-google:before { + content: '\f1a0'; +} +.fa-reddit:before { + content: '\f1a1'; +} +.fa-reddit-square:before { + content: '\f1a2'; +} +.fa-stumbleupon-circle:before { + content: '\f1a3'; +} +.fa-stumbleupon:before { + content: '\f1a4'; +} +.fa-delicious:before { + content: '\f1a5'; +} +.fa-digg:before { + content: '\f1a6'; +} +.fa-pied-piper-pp:before { + content: '\f1a7'; +} +.fa-pied-piper-alt:before { + content: '\f1a8'; +} +.fa-drupal:before { + content: '\f1a9'; +} +.fa-joomla:before { + content: '\f1aa'; +} +.fa-language:before { + content: '\f1ab'; +} +.fa-fax:before { + content: '\f1ac'; +} +.fa-building:before { + content: '\f1ad'; +} +.fa-child:before { + content: '\f1ae'; +} +.fa-paw:before { + content: '\f1b0'; +} +.fa-spoon:before { + content: '\f1b1'; +} +.fa-cube:before { + content: '\f1b2'; +} +.fa-cubes:before { + content: '\f1b3'; +} +.fa-behance:before { + content: '\f1b4'; +} +.fa-behance-square:before { + content: '\f1b5'; +} +.fa-steam:before { + content: '\f1b6'; +} +.fa-steam-square:before { + content: '\f1b7'; +} +.fa-recycle:before { + content: '\f1b8'; +} +.fa-automobile:before, +.fa-car:before { + content: '\f1b9'; +} +.fa-cab:before, +.fa-taxi:before { + content: '\f1ba'; +} +.fa-tree:before { + content: '\f1bb'; +} +.fa-spotify:before { + content: '\f1bc'; +} +.fa-deviantart:before { + content: '\f1bd'; +} +.fa-soundcloud:before { + content: '\f1be'; +} +.fa-database:before { + content: '\f1c0'; +} +.fa-file-pdf-o:before { + content: '\f1c1'; +} +.fa-file-word-o:before { + content: '\f1c2'; +} +.fa-file-excel-o:before { + content: '\f1c3'; +} +.fa-file-powerpoint-o:before { + content: '\f1c4'; +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: '\f1c5'; +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: '\f1c6'; +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: '\f1c7'; +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: '\f1c8'; +} +.fa-file-code-o:before { + content: '\f1c9'; +} +.fa-vine:before { + content: '\f1ca'; +} +.fa-codepen:before { + content: '\f1cb'; +} +.fa-jsfiddle:before { + content: '\f1cc'; +} +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: '\f1cd'; +} +.fa-circle-o-notch:before { + content: '\f1ce'; +} +.fa-ra:before, +.fa-resistance:before, +.fa-rebel:before { + content: '\f1d0'; +} +.fa-ge:before, +.fa-empire:before { + content: '\f1d1'; +} +.fa-git-square:before { + content: '\f1d2'; +} +.fa-git:before { + content: '\f1d3'; +} +.fa-y-combinator-square:before, +.fa-yc-square:before, +.fa-hacker-news:before { + content: '\f1d4'; +} +.fa-tencent-weibo:before { + content: '\f1d5'; +} +.fa-qq:before { + content: '\f1d6'; +} +.fa-wechat:before, +.fa-weixin:before { + content: '\f1d7'; +} +.fa-send:before, +.fa-paper-plane:before { + content: '\f1d8'; +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: '\f1d9'; +} +.fa-history:before { + content: '\f1da'; +} +.fa-circle-thin:before { + content: '\f1db'; +} +.fa-header:before { + content: '\f1dc'; +} +.fa-paragraph:before { + content: '\f1dd'; +} +.fa-sliders:before { + content: '\f1de'; +} +.fa-share-alt:before { + content: '\f1e0'; +} +.fa-share-alt-square:before { + content: '\f1e1'; +} +.fa-bomb:before { + content: '\f1e2'; +} +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: '\f1e3'; +} +.fa-tty:before { + content: '\f1e4'; +} +.fa-binoculars:before { + content: '\f1e5'; +} +.fa-plug:before { + content: '\f1e6'; +} +.fa-slideshare:before { + content: '\f1e7'; +} +.fa-twitch:before { + content: '\f1e8'; +} +.fa-yelp:before { + content: '\f1e9'; +} +.fa-newspaper-o:before { + content: '\f1ea'; +} +.fa-wifi:before { + content: '\f1eb'; +} +.fa-calculator:before { + content: '\f1ec'; +} +.fa-paypal:before { + content: '\f1ed'; +} +.fa-google-wallet:before { + content: '\f1ee'; +} +.fa-cc-visa:before { + content: '\f1f0'; +} +.fa-cc-mastercard:before { + content: '\f1f1'; +} +.fa-cc-discover:before { + content: '\f1f2'; +} +.fa-cc-amex:before { + content: '\f1f3'; +} +.fa-cc-paypal:before { + content: '\f1f4'; +} +.fa-cc-stripe:before { + content: '\f1f5'; +} +.fa-bell-slash:before { + content: '\f1f6'; +} +.fa-bell-slash-o:before { + content: '\f1f7'; +} +.fa-trash:before { + content: '\f1f8'; +} +.fa-copyright:before { + content: '\f1f9'; +} +.fa-at:before { + content: '\f1fa'; +} +.fa-eyedropper:before { + content: '\f1fb'; +} +.fa-paint-brush:before { + content: '\f1fc'; +} +.fa-birthday-cake:before { + content: '\f1fd'; +} +.fa-area-chart:before { + content: '\f1fe'; +} +.fa-pie-chart:before { + content: '\f200'; +} +.fa-line-chart:before { + content: '\f201'; +} +.fa-lastfm:before { + content: '\f202'; +} +.fa-lastfm-square:before { + content: '\f203'; +} +.fa-toggle-off:before { + content: '\f204'; +} +.fa-toggle-on:before { + content: '\f205'; +} +.fa-bicycle:before { + content: '\f206'; +} +.fa-bus:before { + content: '\f207'; +} +.fa-ioxhost:before { + content: '\f208'; +} +.fa-angellist:before { + content: '\f209'; +} +.fa-cc:before { + content: '\f20a'; +} +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: '\f20b'; +} +.fa-meanpath:before { + content: '\f20c'; +} +.fa-buysellads:before { + content: '\f20d'; +} +.fa-connectdevelop:before { + content: '\f20e'; +} +.fa-dashcube:before { + content: '\f210'; +} +.fa-forumbee:before { + content: '\f211'; +} +.fa-leanpub:before { + content: '\f212'; +} +.fa-sellsy:before { + content: '\f213'; +} +.fa-shirtsinbulk:before { + content: '\f214'; +} +.fa-simplybuilt:before { + content: '\f215'; +} +.fa-skyatlas:before { + content: '\f216'; +} +.fa-cart-plus:before { + content: '\f217'; +} +.fa-cart-arrow-down:before { + content: '\f218'; +} +.fa-diamond:before { + content: '\f219'; +} +.fa-ship:before { + content: '\f21a'; +} +.fa-user-secret:before { + content: '\f21b'; +} +.fa-motorcycle:before { + content: '\f21c'; +} +.fa-street-view:before { + content: '\f21d'; +} +.fa-heartbeat:before { + content: '\f21e'; +} +.fa-venus:before { + content: '\f221'; +} +.fa-mars:before { + content: '\f222'; +} +.fa-mercury:before { + content: '\f223'; +} +.fa-intersex:before, +.fa-transgender:before { + content: '\f224'; +} +.fa-transgender-alt:before { + content: '\f225'; +} +.fa-venus-double:before { + content: '\f226'; +} +.fa-mars-double:before { + content: '\f227'; +} +.fa-venus-mars:before { + content: '\f228'; +} +.fa-mars-stroke:before { + content: '\f229'; +} +.fa-mars-stroke-v:before { + content: '\f22a'; +} +.fa-mars-stroke-h:before { + content: '\f22b'; +} +.fa-neuter:before { + content: '\f22c'; +} +.fa-genderless:before { + content: '\f22d'; +} +.fa-facebook-official:before { + content: '\f230'; +} +.fa-pinterest-p:before { + content: '\f231'; +} +.fa-whatsapp:before { + content: '\f232'; +} +.fa-server:before { + content: '\f233'; +} +.fa-user-plus:before { + content: '\f234'; +} +.fa-user-times:before { + content: '\f235'; +} +.fa-hotel:before, +.fa-bed:before { + content: '\f236'; +} +.fa-viacoin:before { + content: '\f237'; +} +.fa-train:before { + content: '\f238'; +} +.fa-subway:before { + content: '\f239'; +} +.fa-medium:before { + content: '\f23a'; +} +.fa-yc:before, +.fa-y-combinator:before { + content: '\f23b'; +} +.fa-optin-monster:before { + content: '\f23c'; +} +.fa-opencart:before { + content: '\f23d'; +} +.fa-expeditedssl:before { + content: '\f23e'; +} +.fa-battery-4:before, +.fa-battery:before, +.fa-battery-full:before { + content: '\f240'; +} +.fa-battery-3:before, +.fa-battery-three-quarters:before { + content: '\f241'; +} +.fa-battery-2:before, +.fa-battery-half:before { + content: '\f242'; +} +.fa-battery-1:before, +.fa-battery-quarter:before { + content: '\f243'; +} +.fa-battery-0:before, +.fa-battery-empty:before { + content: '\f244'; +} +.fa-mouse-pointer:before { + content: '\f245'; +} +.fa-i-cursor:before { + content: '\f246'; +} +.fa-object-group:before { + content: '\f247'; +} +.fa-object-ungroup:before { + content: '\f248'; +} +.fa-sticky-note:before { + content: '\f249'; +} +.fa-sticky-note-o:before { + content: '\f24a'; +} +.fa-cc-jcb:before { + content: '\f24b'; +} +.fa-cc-diners-club:before { + content: '\f24c'; +} +.fa-clone:before { + content: '\f24d'; +} +.fa-balance-scale:before { + content: '\f24e'; +} +.fa-hourglass-o:before { + content: '\f250'; +} +.fa-hourglass-1:before, +.fa-hourglass-start:before { + content: '\f251'; +} +.fa-hourglass-2:before, +.fa-hourglass-half:before { + content: '\f252'; +} +.fa-hourglass-3:before, +.fa-hourglass-end:before { + content: '\f253'; +} +.fa-hourglass:before { + content: '\f254'; +} +.fa-hand-grab-o:before, +.fa-hand-rock-o:before { + content: '\f255'; +} +.fa-hand-stop-o:before, +.fa-hand-paper-o:before { + content: '\f256'; +} +.fa-hand-scissors-o:before { + content: '\f257'; +} +.fa-hand-lizard-o:before { + content: '\f258'; +} +.fa-hand-spock-o:before { + content: '\f259'; +} +.fa-hand-pointer-o:before { + content: '\f25a'; +} +.fa-hand-peace-o:before { + content: '\f25b'; +} +.fa-trademark:before { + content: '\f25c'; +} +.fa-registered:before { + content: '\f25d'; +} +.fa-creative-commons:before { + content: '\f25e'; +} +.fa-gg:before { + content: '\f260'; +} +.fa-gg-circle:before { + content: '\f261'; +} +.fa-tripadvisor:before { + content: '\f262'; +} +.fa-odnoklassniki:before { + content: '\f263'; +} +.fa-odnoklassniki-square:before { + content: '\f264'; +} +.fa-get-pocket:before { + content: '\f265'; +} +.fa-wikipedia-w:before { + content: '\f266'; +} +.fa-safari:before { + content: '\f267'; +} +.fa-chrome:before { + content: '\f268'; +} +.fa-firefox:before { + content: '\f269'; +} +.fa-opera:before { + content: '\f26a'; +} +.fa-internet-explorer:before { + content: '\f26b'; +} +.fa-tv:before, +.fa-television:before { + content: '\f26c'; +} +.fa-contao:before { + content: '\f26d'; +} +.fa-500px:before { + content: '\f26e'; +} +.fa-amazon:before { + content: '\f270'; +} +.fa-calendar-plus-o:before { + content: '\f271'; +} +.fa-calendar-minus-o:before { + content: '\f272'; +} +.fa-calendar-times-o:before { + content: '\f273'; +} +.fa-calendar-check-o:before { + content: '\f274'; +} +.fa-industry:before { + content: '\f275'; +} +.fa-map-pin:before { + content: '\f276'; +} +.fa-map-signs:before { + content: '\f277'; +} +.fa-map-o:before { + content: '\f278'; +} +.fa-map:before { + content: '\f279'; +} +.fa-commenting:before { + content: '\f27a'; +} +.fa-commenting-o:before { + content: '\f27b'; +} +.fa-houzz:before { + content: '\f27c'; +} +.fa-vimeo:before { + content: '\f27d'; +} +.fa-black-tie:before { + content: '\f27e'; +} +.fa-fonticons:before { + content: '\f280'; +} +.fa-reddit-alien:before { + content: '\f281'; +} +.fa-edge:before { + content: '\f282'; +} +.fa-credit-card-alt:before { + content: '\f283'; +} +.fa-codiepie:before { + content: '\f284'; +} +.fa-modx:before { + content: '\f285'; +} +.fa-fort-awesome:before { + content: '\f286'; +} +.fa-usb:before { + content: '\f287'; +} +.fa-product-hunt:before { + content: '\f288'; +} +.fa-mixcloud:before { + content: '\f289'; +} +.fa-scribd:before { + content: '\f28a'; +} +.fa-pause-circle:before { + content: '\f28b'; +} +.fa-pause-circle-o:before { + content: '\f28c'; +} +.fa-stop-circle:before { + content: '\f28d'; +} +.fa-stop-circle-o:before { + content: '\f28e'; +} +.fa-shopping-bag:before { + content: '\f290'; +} +.fa-shopping-basket:before { + content: '\f291'; +} +.fa-hashtag:before { + content: '\f292'; +} +.fa-bluetooth:before { + content: '\f293'; +} +.fa-bluetooth-b:before { + content: '\f294'; +} +.fa-percent:before { + content: '\f295'; +} +.fa-gitlab:before { + content: '\f296'; +} +.fa-wpbeginner:before { + content: '\f297'; +} +.fa-wpforms:before { + content: '\f298'; +} +.fa-envira:before { + content: '\f299'; +} +.fa-universal-access:before { + content: '\f29a'; +} +.fa-wheelchair-alt:before { + content: '\f29b'; +} +.fa-question-circle-o:before { + content: '\f29c'; +} +.fa-blind:before { + content: '\f29d'; +} +.fa-audio-description:before { + content: '\f29e'; +} +.fa-volume-control-phone:before { + content: '\f2a0'; +} +.fa-braille:before { + content: '\f2a1'; +} +.fa-assistive-listening-systems:before { + content: '\f2a2'; +} +.fa-asl-interpreting:before, +.fa-american-sign-language-interpreting:before { + content: '\f2a3'; +} +.fa-deafness:before, +.fa-hard-of-hearing:before, +.fa-deaf:before { + content: '\f2a4'; +} +.fa-glide:before { + content: '\f2a5'; +} +.fa-glide-g:before { + content: '\f2a6'; +} +.fa-signing:before, +.fa-sign-language:before { + content: '\f2a7'; +} +.fa-low-vision:before { + content: '\f2a8'; +} +.fa-viadeo:before { + content: '\f2a9'; +} +.fa-viadeo-square:before { + content: '\f2aa'; +} +.fa-snapchat:before { + content: '\f2ab'; +} +.fa-snapchat-ghost:before { + content: '\f2ac'; +} +.fa-snapchat-square:before { + content: '\f2ad'; +} +.fa-pied-piper:before { + content: '\f2ae'; +} +.fa-first-order:before { + content: '\f2b0'; +} +.fa-yoast:before { + content: '\f2b1'; +} +.fa-themeisle:before { + content: '\f2b2'; +} +.fa-google-plus-circle:before, +.fa-google-plus-official:before { + content: '\f2b3'; +} +.fa-fa:before, +.fa-font-awesome:before { + content: '\f2b4'; +} +.fa-handshake-o:before { + content: '\f2b5'; +} +.fa-envelope-open:before { + content: '\f2b6'; +} +.fa-envelope-open-o:before { + content: '\f2b7'; +} +.fa-linode:before { + content: '\f2b8'; +} +.fa-address-book:before { + content: '\f2b9'; +} +.fa-address-book-o:before { + content: '\f2ba'; +} +.fa-vcard:before, +.fa-address-card:before { + content: '\f2bb'; +} +.fa-vcard-o:before, +.fa-address-card-o:before { + content: '\f2bc'; +} +.fa-user-circle:before { + content: '\f2bd'; +} +.fa-user-circle-o:before { + content: '\f2be'; +} +.fa-user-o:before { + content: '\f2c0'; +} +.fa-id-badge:before { + content: '\f2c1'; +} +.fa-drivers-license:before, +.fa-id-card:before { + content: '\f2c2'; +} +.fa-drivers-license-o:before, +.fa-id-card-o:before { + content: '\f2c3'; +} +.fa-quora:before { + content: '\f2c4'; +} +.fa-free-code-camp:before { + content: '\f2c5'; +} +.fa-telegram:before { + content: '\f2c6'; +} +.fa-thermometer-4:before, +.fa-thermometer:before, +.fa-thermometer-full:before { + content: '\f2c7'; +} +.fa-thermometer-3:before, +.fa-thermometer-three-quarters:before { + content: '\f2c8'; +} +.fa-thermometer-2:before, +.fa-thermometer-half:before { + content: '\f2c9'; +} +.fa-thermometer-1:before, +.fa-thermometer-quarter:before { + content: '\f2ca'; +} +.fa-thermometer-0:before, +.fa-thermometer-empty:before { + content: '\f2cb'; +} +.fa-shower:before { + content: '\f2cc'; +} +.fa-bathtub:before, +.fa-s15:before, +.fa-bath:before { + content: '\f2cd'; +} +.fa-podcast:before { + content: '\f2ce'; +} +.fa-window-maximize:before { + content: '\f2d0'; +} +.fa-window-minimize:before { + content: '\f2d1'; +} +.fa-window-restore:before { + content: '\f2d2'; +} +.fa-times-rectangle:before, +.fa-window-close:before { + content: '\f2d3'; +} +.fa-times-rectangle-o:before, +.fa-window-close-o:before { + content: '\f2d4'; +} +.fa-bandcamp:before { + content: '\f2d5'; +} +.fa-grav:before { + content: '\f2d6'; +} +.fa-etsy:before { + content: '\f2d7'; +} +.fa-imdb:before { + content: '\f2d8'; +} +.fa-ravelry:before { + content: '\f2d9'; +} +.fa-eercast:before { + content: '\f2da'; +} +.fa-microchip:before { + content: '\f2db'; +} +.fa-snowflake-o:before { + content: '\f2dc'; +} +.fa-superpowers:before { + content: '\f2dd'; +} +.fa-wpexplorer:before { + content: '\f2de'; +} +.fa-meetup:before { + content: '\f2e0'; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} diff --git a/services/web/frontend/fonts/lato-v16-latin-ext-300.woff b/services/web/frontend/fonts/lato-v16-latin-ext-300.woff new file mode 100644 index 0000000000..46d4244757 Binary files /dev/null and b/services/web/frontend/fonts/lato-v16-latin-ext-300.woff differ diff --git a/services/web/frontend/fonts/lato-v16-latin-ext-300.woff2 b/services/web/frontend/fonts/lato-v16-latin-ext-300.woff2 new file mode 100644 index 0000000000..f1c62e0996 Binary files /dev/null and b/services/web/frontend/fonts/lato-v16-latin-ext-300.woff2 differ diff --git a/services/web/frontend/fonts/lato-v16-latin-ext-700.woff b/services/web/frontend/fonts/lato-v16-latin-ext-700.woff new file mode 100644 index 0000000000..e8d62dd0a3 Binary files /dev/null and b/services/web/frontend/fonts/lato-v16-latin-ext-700.woff differ diff --git a/services/web/frontend/fonts/lato-v16-latin-ext-700.woff2 b/services/web/frontend/fonts/lato-v16-latin-ext-700.woff2 new file mode 100644 index 0000000000..5ba583e88b Binary files /dev/null and b/services/web/frontend/fonts/lato-v16-latin-ext-700.woff2 differ diff --git a/services/web/frontend/fonts/lato-v16-latin-ext-regular.woff b/services/web/frontend/fonts/lato-v16-latin-ext-regular.woff new file mode 100644 index 0000000000..c6d3d1d9de Binary files /dev/null and b/services/web/frontend/fonts/lato-v16-latin-ext-regular.woff differ diff --git a/services/web/frontend/fonts/lato-v16-latin-ext-regular.woff2 b/services/web/frontend/fonts/lato-v16-latin-ext-regular.woff2 new file mode 100644 index 0000000000..4153a8259f Binary files /dev/null and b/services/web/frontend/fonts/lato-v16-latin-ext-regular.woff2 differ diff --git a/services/web/frontend/fonts/lato.css b/services/web/frontend/fonts/lato.css new file mode 100644 index 0000000000..771a6ba5a1 --- /dev/null +++ b/services/web/frontend/fonts/lato.css @@ -0,0 +1,24 @@ +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 300; + src: local('Lato Light'), local('Lato-Light'), + url('lato-v16-latin-ext-300.woff2') format('woff2'), + url('lato-v16-latin-ext-300.woff') format('woff'); +} +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 400; + src: local('Lato Regular'), local('Lato-Regular'), + url('lato-v16-latin-ext-regular.woff2') format('woff2'), + url('lato-v16-latin-ext-regular.woff') format('woff'); +} +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 700; + src: local('Lato Bold'), local('Lato-Bold'), + url('lato-v16-latin-ext-700.woff2') format('woff2'), + url('lato-v16-latin-ext-700.woff') format('woff'); +} diff --git a/services/web/frontend/fonts/merriweather-v21-latin-700.woff b/services/web/frontend/fonts/merriweather-v21-latin-700.woff new file mode 100644 index 0000000000..afd6fecdcb Binary files /dev/null and b/services/web/frontend/fonts/merriweather-v21-latin-700.woff differ diff --git a/services/web/frontend/fonts/merriweather-v21-latin-700.woff2 b/services/web/frontend/fonts/merriweather-v21-latin-700.woff2 new file mode 100644 index 0000000000..ead7c09601 Binary files /dev/null and b/services/web/frontend/fonts/merriweather-v21-latin-700.woff2 differ diff --git a/services/web/frontend/fonts/merriweather-v21-latin-700italic.woff b/services/web/frontend/fonts/merriweather-v21-latin-700italic.woff new file mode 100644 index 0000000000..a572ccaac9 Binary files /dev/null and b/services/web/frontend/fonts/merriweather-v21-latin-700italic.woff differ diff --git a/services/web/frontend/fonts/merriweather-v21-latin-700italic.woff2 b/services/web/frontend/fonts/merriweather-v21-latin-700italic.woff2 new file mode 100644 index 0000000000..051450d3be Binary files /dev/null and b/services/web/frontend/fonts/merriweather-v21-latin-700italic.woff2 differ diff --git a/services/web/frontend/fonts/merriweather-v21-latin-italic.woff b/services/web/frontend/fonts/merriweather-v21-latin-italic.woff new file mode 100644 index 0000000000..fe2e3dc0bf Binary files /dev/null and b/services/web/frontend/fonts/merriweather-v21-latin-italic.woff differ diff --git a/services/web/frontend/fonts/merriweather-v21-latin-italic.woff2 b/services/web/frontend/fonts/merriweather-v21-latin-italic.woff2 new file mode 100644 index 0000000000..3c26191b83 Binary files /dev/null and b/services/web/frontend/fonts/merriweather-v21-latin-italic.woff2 differ diff --git a/services/web/frontend/fonts/merriweather-v21-latin-regular.woff b/services/web/frontend/fonts/merriweather-v21-latin-regular.woff new file mode 100644 index 0000000000..2f2ab42177 Binary files /dev/null and b/services/web/frontend/fonts/merriweather-v21-latin-regular.woff differ diff --git a/services/web/frontend/fonts/merriweather-v21-latin-regular.woff2 b/services/web/frontend/fonts/merriweather-v21-latin-regular.woff2 new file mode 100644 index 0000000000..099840930c Binary files /dev/null and b/services/web/frontend/fonts/merriweather-v21-latin-regular.woff2 differ diff --git a/services/web/frontend/fonts/merriweather.css b/services/web/frontend/fonts/merriweather.css new file mode 100644 index 0000000000..61787faee8 --- /dev/null +++ b/services/web/frontend/fonts/merriweather.css @@ -0,0 +1,32 @@ +@font-face { + font-family: 'Merriweather'; + font-style: normal; + font-weight: 400; + src: local('Merriweather Regular'), local('Merriweather-Regular'), + url('merriweather-v21-latin-regular.woff2') format('woff2'), + url('merriweather-v21-latin-regular.woff') format('woff'); +} +@font-face { + font-family: 'Merriweather'; + font-style: italic; + font-weight: 400; + src: local('Merriweather Italic'), local('Merriweather-Italic'), + url('merriweather-v21-latin-italic.woff2') format('woff2'), + url('merriweather-v21-latin-italic.woff') format('woff'); +} +@font-face { + font-family: 'Merriweather'; + font-style: normal; + font-weight: 700; + src: local('Merriweather Bold'), local('Merriweather-Bold'), + url('merriweather-v21-latin-700.woff2') format('woff2'), + url('merriweather-v21-latin-700.woff') format('woff'); +} +@font-face { + font-family: 'Merriweather'; + font-style: italic; + font-weight: 700; + src: local('Merriweather Bold Italic'), local('Merriweather-BoldItalic'), + url('merriweather-v21-latin-700italic.woff2') format('woff2'), + url('merriweather-v21-latin-700italic.woff') format('woff'); +} diff --git a/services/web/frontend/fonts/open-sans-v17-latin-300.woff b/services/web/frontend/fonts/open-sans-v17-latin-300.woff new file mode 100644 index 0000000000..26567ff259 Binary files /dev/null and b/services/web/frontend/fonts/open-sans-v17-latin-300.woff differ diff --git a/services/web/frontend/fonts/open-sans-v17-latin-300.woff2 b/services/web/frontend/fonts/open-sans-v17-latin-300.woff2 new file mode 100644 index 0000000000..7bf901c28e Binary files /dev/null and b/services/web/frontend/fonts/open-sans-v17-latin-300.woff2 differ diff --git a/services/web/frontend/fonts/open-sans-v17-latin-600.woff b/services/web/frontend/fonts/open-sans-v17-latin-600.woff new file mode 100644 index 0000000000..9d0eb42db0 Binary files /dev/null and b/services/web/frontend/fonts/open-sans-v17-latin-600.woff differ diff --git a/services/web/frontend/fonts/open-sans-v17-latin-600.woff2 b/services/web/frontend/fonts/open-sans-v17-latin-600.woff2 new file mode 100644 index 0000000000..5c5d54e2f2 Binary files /dev/null and b/services/web/frontend/fonts/open-sans-v17-latin-600.woff2 differ diff --git a/services/web/frontend/fonts/open-sans-v17-latin-700.woff b/services/web/frontend/fonts/open-sans-v17-latin-700.woff new file mode 100644 index 0000000000..b8b46d0b40 Binary files /dev/null and b/services/web/frontend/fonts/open-sans-v17-latin-700.woff differ diff --git a/services/web/frontend/fonts/open-sans-v17-latin-700.woff2 b/services/web/frontend/fonts/open-sans-v17-latin-700.woff2 new file mode 100644 index 0000000000..3a38286c67 Binary files /dev/null and b/services/web/frontend/fonts/open-sans-v17-latin-700.woff2 differ diff --git a/services/web/frontend/fonts/open-sans-v17-latin-regular.woff b/services/web/frontend/fonts/open-sans-v17-latin-regular.woff new file mode 100644 index 0000000000..39e88ed924 Binary files /dev/null and b/services/web/frontend/fonts/open-sans-v17-latin-regular.woff differ diff --git a/services/web/frontend/fonts/open-sans-v17-latin-regular.woff2 b/services/web/frontend/fonts/open-sans-v17-latin-regular.woff2 new file mode 100644 index 0000000000..e9f58b775e Binary files /dev/null and b/services/web/frontend/fonts/open-sans-v17-latin-regular.woff2 differ diff --git a/services/web/frontend/fonts/open-sans.css b/services/web/frontend/fonts/open-sans.css new file mode 100644 index 0000000000..ae38b38bb8 --- /dev/null +++ b/services/web/frontend/fonts/open-sans.css @@ -0,0 +1,32 @@ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + src: local('Open Sans Light'), local('OpenSans-Light'), + url('open-sans-v17-latin-300.woff2') format('woff2'), + url('open-sans-v17-latin-300.woff') format('woff'); +} +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + src: local('Open Sans Regular'), local('OpenSans-Regular'), + url('open-sans-v17-latin-regular.woff2') format('woff2'), + url('open-sans-v17-latin-regular.woff') format('woff'); +} +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 600; + src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'), + url('open-sans-v17-latin-600.woff2') format('woff2'), + url('open-sans-v17-latin-600.woff') format('woff'); +} +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 700; + src: local('Open Sans Bold'), local('OpenSans-Bold'), + url('open-sans-v17-latin-700.woff2') format('woff2'), + url('open-sans-v17-latin-700.woff') format('woff'); +} diff --git a/services/web/frontend/fonts/source-code-pro-v13-latin-regular.woff b/services/web/frontend/fonts/source-code-pro-v13-latin-regular.woff new file mode 100644 index 0000000000..9f393eb942 Binary files /dev/null and b/services/web/frontend/fonts/source-code-pro-v13-latin-regular.woff differ diff --git a/services/web/frontend/fonts/source-code-pro-v13-latin-regular.woff2 b/services/web/frontend/fonts/source-code-pro-v13-latin-regular.woff2 new file mode 100644 index 0000000000..90d1a423b4 Binary files /dev/null and b/services/web/frontend/fonts/source-code-pro-v13-latin-regular.woff2 differ diff --git a/services/web/frontend/fonts/source-code-pro.css b/services/web/frontend/fonts/source-code-pro.css new file mode 100644 index 0000000000..be0d2ff03b --- /dev/null +++ b/services/web/frontend/fonts/source-code-pro.css @@ -0,0 +1,8 @@ +@font-face { + font-family: 'Source Code Pro'; + font-style: normal; + font-weight: 400; + src: local('Source Code Pro Regular'), local('SourceCodePro-Regular'), + url('source-code-pro-v13-latin-regular.woff2') format('woff2'), + url('source-code-pro-v13-latin-regular.woff') format('woff'); +} diff --git a/services/web/frontend/fonts/stix-two-math.css b/services/web/frontend/fonts/stix-two-math.css new file mode 100644 index 0000000000..4fd5d2840c --- /dev/null +++ b/services/web/frontend/fonts/stix-two-math.css @@ -0,0 +1,5 @@ +@font-face { + font-family: "Stix Two Math"; + src: url("./STIXTwoMath/STIXTwoMath-Regular.woff2") + format("woff2"); +} diff --git a/services/web/frontend/js/base.js b/services/web/frontend/js/base.js new file mode 100644 index 0000000000..1059b69a55 --- /dev/null +++ b/services/web/frontend/js/base.js @@ -0,0 +1,105 @@ +/* global MathJax */ + +/* eslint-disable + camelcase, + max-len, + no-useless-escape, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS103: Rewrite code to no longer use __guard__ + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ + +import './utils/webpack-public-path' +import './libraries' +import './infrastructure/error-reporter' +import './modules/recursionHelper' +import './modules/errorCatcher' +import './modules/localStorage' +import './modules/sessionStorage' +import getMeta from './utils/meta' + +const App = angular + .module('SharelatexApp', [ + 'ui.bootstrap', + 'autocomplete', + 'RecursionHelper', + 'ng-context-menu', + 'ngSanitize', + 'ipCookie', + 'ErrorCatcher', + 'localStorage', + 'sessionStorage', + 'ui.select', + ]) + .config(function ($qProvider, $httpProvider, uiSelectConfig) { + $qProvider.errorOnUnhandledRejections(false) + uiSelectConfig.spinnerClass = 'fa fa-refresh ui-select-spin' + + return __guard__( + typeof MathJax !== 'undefined' && MathJax !== null + ? MathJax.Hub + : undefined, + x => + x.Config({ + messageStyle: 'none', + imageFont: null, + // Fast preview, introduced in 2.5, is unhelpful due to extra codemirror refresh + // and disabling it avoids issues with math processing errors + // github.com/overleaf/write_latex/pull/1375 + 'fast-preview': { disabled: true }, + 'HTML-CSS': { + availableFonts: ['TeX'], + // MathJax's automatic font scaling does not work well when we render math + // that isn't yet on the page, so we disable it and set a global font + // scale factor + scale: 110, + matchFontHeight: false, + }, + TeX: { + equationNumbers: { autoNumber: 'AMS' }, + useLabelIDs: false, + }, + skipStartupTypeset: true, + tex2jax: { + processEscapes: true, + // Dollar delimiters are added by the mathjax directive + inlineMath: [['\\(', '\\)']], + displayMath: [ + ['$$', '$$'], + ['\\[', '\\]'], + ], + }, + }) + ) + }) + +App.run(($rootScope, $templateCache) => { + $rootScope.usersEmail = getMeta('ol-usersEmail') + + // UI Select templates are hard-coded and use Glyphicon icons (which we don't import). + // The line below simply overrides the hard-coded template with our own, which is + // basically the same but using Font Awesome icons. + $templateCache.put( + 'bootstrap/match.tpl.html', + '
{{$select.placeholder}}
' + ) +}) + +const sl_debugging = window.location.search.match(/debug=true/) +window.sl_debugging = sl_debugging // make a global flag for debugging code +window.sl_console = sl_debugging ? console : { log() {} } + +export default App + +function __guard__(value, transform) { + return typeof value !== 'undefined' && value !== null + ? transform(value) + : undefined +} diff --git a/services/web/frontend/js/directives/asyncForm.js b/services/web/frontend/js/directives/asyncForm.js new file mode 100644 index 0000000000..43f6419307 --- /dev/null +++ b/services/web/frontend/js/directives/asyncForm.js @@ -0,0 +1,184 @@ +import App from '../base' +import 'libs/passfield' +App.directive('asyncForm', ($http, validateCaptcha, validateCaptchaV3) => ({ + controller: [ + '$scope', + '$location', + function ($scope, $location) { + this.getEmail = () => $scope.email + this.getEmailFromQuery = () => + $location.search().email || $location.search().new_email + return this + }, + ], + link(scope, element, attrs, ctrl) { + let response + const formName = attrs.asyncForm + + scope[attrs.name].response = response = {} + scope[attrs.name].inflight = false + scope.email = + scope.email || + scope.usersEmail || + ctrl.getEmailFromQuery() || + attrs.newEmail + + const validateCaptchaIfEnabled = function (callback) { + scope.$applyAsync(() => { + scope[attrs.name].inflight = true + }) + + if (attrs.captchaActionName) { + validateCaptchaV3(attrs.captchaActionName) + } + if (attrs.captcha != null) { + validateCaptcha(callback) + } else { + callback() + } + } + + const _submitRequest = function (grecaptchaResponse) { + const formData = {} + for (const data of Array.from(element.serializeArray())) { + formData[data.name] = data.value + } + + if (grecaptchaResponse) { + formData['g-recaptcha-response'] = grecaptchaResponse + } + + // clear the response object which may be referenced downstream + Object.keys(response).forEach(field => delete response[field]) + + // for asyncForm prevent automatic redirect to /login if + // authentication fails, we will handle it ourselves + const httpRequestFn = _httpRequestFn(element.attr('method')) + return httpRequestFn(element.attr('action'), formData, { + disableAutoLoginRedirect: true, + }) + .then(function (httpResponse) { + const { data, headers } = httpResponse + scope[attrs.name].inflight = false + response.success = true + response.error = false + + const onSuccessHandler = scope[attrs.onSuccess] + if (onSuccessHandler) { + onSuccessHandler(httpResponse) + return + } + + if (data.redir) { + ga('send', 'event', formName, 'success') + return (window.location = data.redir) + } else if (data.message) { + response.message = data.message + + if (data.message.type === 'error') { + response.success = false + response.error = true + return ga('send', 'event', formName, 'failure', data.message) + } else { + return ga('send', 'event', formName, 'success') + } + } else if (scope.$eval(attrs.asyncFormDownloadResponse)) { + const blob = new Blob([data], { + type: headers('Content-Type'), + }) + location.href = URL.createObjectURL(blob) // Trigger file save + } + }) + .catch(function (httpResponse) { + const { data, status } = httpResponse + scope[attrs.name].inflight = false + response.success = false + response.error = true + response.status = status + response.data = data + + const onErrorHandler = scope[attrs.onError] + if (onErrorHandler) { + onErrorHandler(httpResponse) + return + } + + let responseMessage + if (data.message && data.message.text) { + responseMessage = data.message.text + } else { + responseMessage = data.message + } + + if (status === 400) { + // Bad Request + response.message = { + text: + responseMessage || + 'Invalid Request. Please correct the data and try again.', + type: 'error', + } + } else if (status === 403) { + // Forbidden + response.message = { + text: + responseMessage || + 'Session error. Please check you have cookies enabled. If the problem persists, try clearing your cache and cookies.', + type: 'error', + } + } else if (status === 429) { + response.message = { + text: + responseMessage || + 'Too many attempts. Please wait for a while and try again.', + type: 'error', + } + } else { + response.message = { + text: + responseMessage || + 'Something went wrong talking to the server :(. Please try again.', + type: 'error', + } + } + ga('send', 'event', formName, 'failure', data.message) + }) + } + + const submit = () => + validateCaptchaIfEnabled(response => _submitRequest(response)) + + const _httpRequestFn = (method = 'post') => { + const $HTTP_FNS = { + post: $http.post, + get: $http.get, + } + return $HTTP_FNS[method.toLowerCase()] + } + + element.on('submit', function (e) { + e.preventDefault() + submit() + }) + + if (attrs.autoSubmit) { + submit() + } + }, +})) + +App.directive('formMessages', () => ({ + restrict: 'E', + template: `\ +
+
+
\ +`, + transclude: true, + scope: { + form: '=for', + }, +})) diff --git a/services/web/frontend/js/directives/autoSubmitForm.js b/services/web/frontend/js/directives/autoSubmitForm.js new file mode 100644 index 0000000000..b1df2792be --- /dev/null +++ b/services/web/frontend/js/directives/autoSubmitForm.js @@ -0,0 +1,8 @@ +import App from '../base' +App.directive('autoSubmitForm', function () { + return { + link(scope, element) { + element.submit() // Runs on load + }, + } +}) diff --git a/services/web/frontend/js/directives/bookmarkableTabset.js b/services/web/frontend/js/directives/bookmarkableTabset.js new file mode 100644 index 0000000000..1293f8ba16 --- /dev/null +++ b/services/web/frontend/js/directives/bookmarkableTabset.js @@ -0,0 +1,58 @@ +import _ from 'lodash' +import App from '../base' +App.directive('bookmarkableTabset', $location => ({ + restrict: 'A', + require: 'tabset', + link(scope, el, attrs, tabset) { + const _makeActive = function (hash) { + if (hash && hash !== '') { + const matchingTab = _.find( + tabset.tabs, + tab => tab.bookmarkableTabId === hash + ) + if (matchingTab) { + matchingTab.select() + return el.children()[0].scrollIntoView({ behavior: 'smooth' }) + } + } + } + + scope.$applyAsync(function () { + // for page load + const hash = $location.hash() + _makeActive(hash) + + // for links within page to a tab + // this needs to be within applyAsync because there could be a link + // within a tab to another tab + const linksToTabs = document.querySelectorAll('.link-to-tab') + const _clickLinkToTab = event => { + const hash = event.currentTarget.getAttribute('href').split('#').pop() + _makeActive(hash) + } + + if (linksToTabs) { + Array.from(linksToTabs).map(link => + link.addEventListener('click', _clickLinkToTab) + ) + } + }) + }, +})) + +App.directive('bookmarkableTab', $location => ({ + restrict: 'A', + require: 'tab', + link(scope, el, attrs, tab) { + const tabScope = el.isolateScope() + const tabId = attrs.bookmarkableTab + if (tabScope && tabId && tabId !== '') { + tabScope.bookmarkableTabId = tabId + tabScope.$watch('active', function (isActive, wasActive) { + if (isActive && !wasActive && $location.hash() !== tabId) { + return $location.hash(tabId) + } + }) + } + }, +})) diff --git a/services/web/frontend/js/directives/complexPassword.js b/services/web/frontend/js/directives/complexPassword.js new file mode 100644 index 0000000000..b1bc278fb1 --- /dev/null +++ b/services/web/frontend/js/directives/complexPassword.js @@ -0,0 +1,90 @@ +import _ from 'lodash' +/* global PassField */ + +/* eslint-disable + max-len +*/ +import App from '../base' +import 'libs/passfield' +App.directive('complexPassword', () => ({ + require: ['^asyncForm', 'ngModel'], + + link(scope, element, attrs, ctrl) { + PassField.Config.blackList = [] + const defaultPasswordOpts = { + pattern: '', + length: { + min: 6, + max: 72, + }, + allowEmpty: false, + allowAnyChars: false, + isMasked: true, + showToggle: false, + showGenerate: false, + showTip: false, + showWarn: false, + checkMode: PassField.CheckModes.STRICT, + chars: { + digits: '1234567890', + letters: 'abcdefghijklmnopqrstuvwxyz', + letters_up: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + symbols: '@#$%^&*()-_=+[]{};:<>/?!£€.,', + }, + } + + const opts = _.defaults( + window.passwordStrengthOptions || {}, + defaultPasswordOpts + ) + + if (opts.length.min === 1) { + // this allows basically anything to be a valid password + opts.acceptRate = 0 + } + + if (opts.length.max > 72) { + // there is a hard limit of 71 characters in the password at the backend + opts.length.max = 72 + } + + if (opts.length.max > 0) { + // PassField's notion of 'max' is non-inclusive + opts.length.max += 1 + } + + const passField = new PassField.Field('passwordField', opts) + const [asyncFormCtrl, ngModelCtrl] = Array.from(ctrl) + + ngModelCtrl.$parsers.unshift(function (modelValue) { + let isValid = passField.validatePass() + const email = asyncFormCtrl.getEmail() || window.usersEmail + + if (!isValid) { + scope.complexPasswordErrorMessage = passField.getPassValidationMessage() + } else if (typeof email === 'string' && email !== '') { + const startOfEmail = email.split('@')[0] + if ( + modelValue.indexOf(email) !== -1 || + modelValue.indexOf(startOfEmail) !== -1 + ) { + isValid = false + scope.complexPasswordErrorMessage = + 'Password can not contain email address' + } + } + if (opts.length.max != null && modelValue.length >= opts.length.max) { + isValid = false + scope.complexPasswordErrorMessage = `Maximum password length ${ + opts.length.max - 1 + } exceeded` + } + if (opts.length.min != null && modelValue.length < opts.length.min) { + isValid = false + scope.complexPasswordErrorMessage = `Password too short, minimum ${opts.length.min}` + } + ngModelCtrl.$setValidity('complexPassword', isValid) + return modelValue + }) + }, +})) diff --git a/services/web/frontend/js/directives/equals.js b/services/web/frontend/js/directives/equals.js new file mode 100644 index 0000000000..43b723432a --- /dev/null +++ b/services/web/frontend/js/directives/equals.js @@ -0,0 +1,21 @@ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import App from '../base' + +export default App.directive('equals', () => ({ + require: 'ngModel', + link(scope, elem, attrs, ctrl) { + const firstField = `#${attrs.equals}` + return elem.add(firstField).on('keyup', () => + scope.$apply(function () { + const equal = elem.val() === $(firstField).val() + return ctrl.$setValidity('areEqual', equal) + }) + ) + }, +})) diff --git a/services/web/frontend/js/directives/eventTracking.js b/services/web/frontend/js/directives/eventTracking.js new file mode 100644 index 0000000000..2836b9fb3e --- /dev/null +++ b/services/web/frontend/js/directives/eventTracking.js @@ -0,0 +1,125 @@ +import _ from 'lodash' +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +// For sending event data to metabase and google analytics +// --- +// by default, +// event not sent to MB. +// for MB, add event-tracking-mb='true' +// by default, event sent to MB via sendMB +// event not sent to GA. +// for GA, add event-tracking-ga attribute, where the value is the GA category +// Either GA or MB can use the attribute event-tracking-send-once='true' to +// send event just once +// MB will use the key and GA will use the action to determine if the event +// has been sent +// event-tracking-trigger attribute is required to send event + +/* eslint-disable + camelcase, + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +// For sending event data to metabase and google analytics +// --- +// by default, +// event not sent to MB. +// for MB, add event-tracking-mb='true' +// by default, event sent to MB via sendMB +// event not sent to GA. +// for GA, add event-tracking-ga attribute, where the value is the GA category +// Either GA or MB can use the attribute event-tracking-send-once='true' to +// send event just once +// MB will use the key and GA will use the action to determine if the event +// has been sent +// event-tracking-trigger attribute is required to send event + +import App from '../base' + +const isInViewport = function (element) { + const elTop = element.offset().top + const elBtm = elTop + element.outerHeight() + + const viewportTop = $(window).scrollTop() + const viewportBtm = viewportTop + $(window).height() + + return elBtm > viewportTop && elTop < viewportBtm +} + +export default App.directive('eventTracking', eventTracking => ({ + scope: { + eventTracking: '@', + eventSegmentation: '=?', + }, + link(scope, element, attrs) { + const sendGA = attrs.eventTrackingGa || false + const sendMB = attrs.eventTrackingMb || false + const sendMBFunction = attrs.eventTrackingSendOnce ? 'sendMBOnce' : 'sendMB' + const sendGAFunction = attrs.eventTrackingSendOnce ? 'sendGAOnce' : 'send' + const segmentation = scope.eventSegmentation || {} + segmentation.page = window.location.pathname + + const sendEvent = function (scrollEvent) { + /* + @param {boolean} scrollEvent Use to unbind scroll event + */ + if (sendMB) { + eventTracking[sendMBFunction](scope.eventTracking, segmentation) + } + if (sendGA) { + eventTracking[sendGAFunction]( + attrs.eventTrackingGa, + attrs.eventTrackingAction || scope.eventTracking, + attrs.eventTrackingLabel || '' + ) + } + if (scrollEvent) { + return $(window).unbind('resize scroll') + } + } + + if (attrs.eventTrackingTrigger === 'load') { + return sendEvent() + } else if (attrs.eventTrackingTrigger === 'click') { + return element.on('click', e => sendEvent()) + } else if (attrs.eventTrackingTrigger === 'hover') { + let timer = null + let timeoutAmt = 500 + if (attrs.eventHoverAmt) { + timeoutAmt = parseInt(attrs.eventHoverAmt, 10) + } + return element + .on('mouseenter', function () { + timer = setTimeout(() => sendEvent(), timeoutAmt) + }) + .on('mouseleave', () => clearTimeout(timer)) + } else if ( + attrs.eventTrackingTrigger === 'scroll' && + !eventTracking.eventInCache(scope.eventTracking) + ) { + $(window).on( + 'resize scroll', + _.throttle(() => { + if (isInViewport(element)) { + sendEvent(true) + } + }, 500) + ) + } + }, +})) diff --git a/services/web/frontend/js/directives/expandableTextArea.js b/services/web/frontend/js/directives/expandableTextArea.js new file mode 100644 index 0000000000..e134068404 --- /dev/null +++ b/services/web/frontend/js/directives/expandableTextArea.js @@ -0,0 +1,29 @@ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import App from '../base' + +export default App.directive('expandableTextArea', () => ({ + restrict: 'A', + link(scope, el) { + const resetHeight = function () { + const curHeight = el.outerHeight() + const fitHeight = el.prop('scrollHeight') + // clear height if text area is empty + if (el.val() === '') { + el.css('height', 'unset') + } + // otherwise expand to fit text + else if (fitHeight > curHeight) { + scope.$emit('expandable-text-area:resize') + el.css('height', fitHeight) + } + } + + return scope.$watch(() => el.val(), resetHeight) + }, +})) diff --git a/services/web/frontend/js/directives/fineUpload.js b/services/web/frontend/js/directives/fineUpload.js new file mode 100644 index 0000000000..0a60d89f5e --- /dev/null +++ b/services/web/frontend/js/directives/fineUpload.js @@ -0,0 +1,90 @@ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import App from '../base' +import qq from 'fineuploader' + +export default App.directive('fineUpload', $timeout => ({ + scope: { + multiple: '=', + endpoint: '@', + templateId: '@', + sizeLimit: '@', + allowedExtensions: '=', + onCompleteCallback: '=', + onUploadCallback: '=', + onValidateBatch: '=', + onErrorCallback: '=', + onSubmitCallback: '=', + onCancelCallback: '=', + autoUpload: '=', + params: '=', + control: '=', + }, + link(scope, element, attrs) { + let autoUpload, validation + const multiple = scope.multiple || false + const { endpoint } = scope + const { templateId } = scope + if (scope.allowedExtensions != null) { + validation = { allowedExtensions: scope.allowedExtensions } + } else { + validation = {} + } + if (scope.sizeLimit) { + validation.sizeLimit = scope.sizeLimit + } + const maxConnections = scope.maxConnections || 1 + const onComplete = scope.onCompleteCallback || function () {} + const onUpload = scope.onUploadCallback || function () {} + const onError = scope.onErrorCallback || function () {} + const onValidateBatch = scope.onValidateBatch || function () {} + const onSubmit = scope.onSubmitCallback || function () {} + const onCancel = scope.onCancelCallback || function () {} + if (scope.autoUpload == null) { + autoUpload = true + } else { + ;({ autoUpload } = scope) + } + const params = scope.params || {} + params._csrf = window.csrfToken + + const q = new qq.FineUploader({ + element: element[0], + multiple, + autoUpload, + disabledCancelForFormUploads: true, + validation, + maxConnections, + request: { + endpoint, + forceMultipart: true, + params, + paramsInBody: false, + }, + callbacks: { + onComplete, + onUpload, + onValidateBatch, + onError, + onSubmit, + onCancel, + }, + template: templateId, + failedUploadTextDisplay: { + mode: 'custom', + responseProperty: 'error', + }, + }) + window.q = q + if (scope.control != null) { + scope.control.q = q + } + return q + }, +})) diff --git a/services/web/frontend/js/directives/focus.js b/services/web/frontend/js/directives/focus.js new file mode 100644 index 0000000000..17ce160e7b --- /dev/null +++ b/services/web/frontend/js/directives/focus.js @@ -0,0 +1,92 @@ +/* eslint-disable + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import App from '../base' +App.directive('focusWhen', $timeout => ({ + restrict: 'A', + link(scope, element, attr) { + return scope.$watch(attr.focusWhen, function (value) { + if (value) { + return $timeout(() => element.focus()) + } + }) + }, +})) + +App.directive('focusOn', $timeout => ({ + restrict: 'A', + link(scope, element, attrs) { + return scope.$on(attrs.focusOn, () => element.focus()) + }, +})) + +App.directive('selectWhen', $timeout => ({ + restrict: 'A', + link(scope, element, attr) { + return scope.$watch(attr.selectWhen, function (value) { + if (value) { + return $timeout(() => element.select()) + } + }) + }, +})) + +App.directive('selectOn', $timeout => ({ + restrict: 'A', + link(scope, element, attrs) { + return scope.$on(attrs.selectOn, () => element.select()) + }, +})) + +App.directive('selectNameWhen', $timeout => ({ + restrict: 'A', + link(scope, element, attrs) { + return scope.$watch(attrs.selectNameWhen, function (value) { + if (value) { + return $timeout(() => selectName(element)) + } + }) + }, +})) + +App.directive('selectNameOn', () => ({ + restrict: 'A', + link(scope, element, attrs) { + return scope.$on(attrs.selectNameOn, () => selectName(element)) + }, +})) + +App.directive('focus', $timeout => ({ + scope: { + trigger: '@focus', + }, + + link(scope, element) { + return scope.$watch('trigger', function (value) { + if (value === 'true') { + return $timeout(() => element[0].focus()) + } + }) + }, +})) + +function selectName(element) { + // Select up to last '.'. I.e. everything except the file extension + element.focus() + const name = element.val() + if (element[0].setSelectionRange != null) { + let selectionEnd = name.lastIndexOf('.') + if (selectionEnd === -1) { + selectionEnd = name.length + } + return element[0].setSelectionRange(0, selectionEnd) + } +} diff --git a/services/web/frontend/js/directives/mathjax.js b/services/web/frontend/js/directives/mathjax.js new file mode 100644 index 0000000000..eac5422e00 --- /dev/null +++ b/services/web/frontend/js/directives/mathjax.js @@ -0,0 +1,33 @@ +import _ from 'lodash' +/* global MathJax */ + +import App from '../base' + +export default App.directive('mathjax', function ($compile, $parse) { + return { + link(scope, element, attrs) { + if (!(MathJax && MathJax.Hub)) return + + if (attrs.delimiter !== 'no-single-dollar') { + const inlineMathConfig = + MathJax.Hub.config && MathJax.Hub.config.tex2jax.inlineMath + const alreadyConfigured = _.find( + inlineMathConfig, + c => c[0] === '$' && c[1] === '$' + ) + + if (!alreadyConfigured) { + MathJax.Hub.Config({ + tex2jax: { + inlineMath: inlineMathConfig.concat([['$', '$']]), + }, + }) + } + } + + setTimeout(() => { + MathJax.Hub.Queue(['Typeset', MathJax.Hub, element.get(0)]) + }, 0) + }, + } +}) diff --git a/services/web/frontend/js/directives/maxHeight.js b/services/web/frontend/js/directives/maxHeight.js new file mode 100644 index 0000000000..91e916564d --- /dev/null +++ b/services/web/frontend/js/directives/maxHeight.js @@ -0,0 +1,20 @@ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import App from '../base' + +export default App.directive('maxHeight', () => ({ + restrict: 'A', + link(scope, element, attrs) { + return scope.$watch(attrs.maxHeight, function (value) { + if (value != null) { + return element.css({ 'max-height': value }) + } + }) + }, +})) diff --git a/services/web/frontend/js/directives/onEnter.js b/services/web/frontend/js/directives/onEnter.js new file mode 100644 index 0000000000..7c99867ffc --- /dev/null +++ b/services/web/frontend/js/directives/onEnter.js @@ -0,0 +1,17 @@ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import App from '../base' + +export default App.directive('onEnter', () => (scope, element, attrs) => + element.bind('keydown keypress', function (event) { + if (event.which === 13) { + scope.$apply(() => scope.$eval(attrs.onEnter, { event })) + return event.preventDefault() + } + }) +) diff --git a/services/web/frontend/js/directives/rightClick.js b/services/web/frontend/js/directives/rightClick.js new file mode 100644 index 0000000000..3d74de9db9 --- /dev/null +++ b/services/web/frontend/js/directives/rightClick.js @@ -0,0 +1,19 @@ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import App from '../base' + +export default App.directive('rightClick', () => ({ + restrict: 'A', + link(scope, element, attrs) { + return element.bind('contextmenu', function (e) { + e.preventDefault() + e.stopPropagation() + return scope.$eval(attrs.rightClick) + }) + }, +})) diff --git a/services/web/frontend/js/directives/scroll.js b/services/web/frontend/js/directives/scroll.js new file mode 100644 index 0000000000..82abe22d7e --- /dev/null +++ b/services/web/frontend/js/directives/scroll.js @@ -0,0 +1,56 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import App from '../base' + +export default App.directive('updateScrollBottomOn', $timeout => ({ + restrict: 'A', + link(scope, element, attrs, ctrls) { + // We keep the offset from the bottom fixed whenever the event fires + // + // ^ | ^ + // | | | scrollTop + // | | v + // | |----------- + // | | ^ + // | | | + // | | | clientHeight (viewable area) + // | | | + // | | | + // | | v + // | |----------- + // | | ^ + // | | | scrollBottom + // v | v + // \ + // scrollHeight + + let scrollBottom = 0 + element.on( + 'scroll', + e => + (scrollBottom = + element[0].scrollHeight - + element[0].scrollTop - + element[0].clientHeight) + ) + + return scope.$on(attrs.updateScrollBottomOn, () => + $timeout( + () => + element.scrollTop( + element[0].scrollHeight - element[0].clientHeight - scrollBottom + ), + 0 + ) + ) + }, +})) diff --git a/services/web/frontend/js/directives/selectAll.js b/services/web/frontend/js/directives/selectAll.js new file mode 100644 index 0000000000..db69d3d943 --- /dev/null +++ b/services/web/frontend/js/directives/selectAll.js @@ -0,0 +1,98 @@ +/* eslint-disable + max-len, + no-return-assign, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import App from '../base' +App.directive('selectAllList', () => ({ + controller: [ + '$scope', + function ($scope) { + // Selecting or deselecting all should apply to all projects + this.selectAll = () => $scope.$broadcast('select-all:select') + + this.deselectAll = () => $scope.$broadcast('select-all:deselect') + + this.clearSelectAllState = () => $scope.$broadcast('select-all:clear') + }, + ], + link(scope, element, attrs) {}, +})) + +App.directive('selectAll', () => ({ + require: '^selectAllList', + link(scope, element, attrs, selectAllListController) { + scope.$on('select-all:clear', () => element.prop('checked', false)) + + return element.change(function () { + if (element.is(':checked')) { + selectAllListController.selectAll() + } else { + selectAllListController.deselectAll() + } + return true + }) + }, +})) + +App.directive('selectIndividual', () => ({ + require: '^selectAllList', + scope: { + ngModel: '=', + }, + link(scope, element, attrs, selectAllListController) { + let ignoreChanges = false + + scope.$watch('ngModel', function (value) { + if (value != null && !ignoreChanges) { + return selectAllListController.clearSelectAllState() + } + }) + + scope.$on('select-all:select', function () { + if (element.prop('disabled')) { + return + } + ignoreChanges = true + scope.$apply(() => (scope.ngModel = true)) + return (ignoreChanges = false) + }) + + scope.$on('select-all:deselect', function () { + if (element.prop('disabled')) { + return + } + ignoreChanges = true + scope.$apply(() => (scope.ngModel = false)) + return (ignoreChanges = false) + }) + + return scope.$on('select-all:row-clicked', function () { + if (element.prop('disabled')) { + return + } + ignoreChanges = true + scope.$apply(function () { + scope.ngModel = !scope.ngModel + if (!scope.ngModel) { + return selectAllListController.clearSelectAllState() + } + }) + return (ignoreChanges = false) + }) + }, +})) + +export default App.directive('selectRow', () => ({ + scope: true, + link(scope, element, attrs) { + return element.on('click', e => scope.$broadcast('select-all:row-clicked')) + }, +})) diff --git a/services/web/frontend/js/directives/stopPropagation.js b/services/web/frontend/js/directives/stopPropagation.js new file mode 100644 index 0000000000..f310f7808a --- /dev/null +++ b/services/web/frontend/js/directives/stopPropagation.js @@ -0,0 +1,24 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import App from '../base' +App.directive('stopPropagation', $http => ({ + restrict: 'A', + link(scope, element, attrs) { + return element.bind(attrs.stopPropagation, e => e.stopPropagation()) + }, +})) + +export default App.directive('preventDefault', $http => ({ + restrict: 'A', + link(scope, element, attrs) { + return element.bind(attrs.preventDefault, e => e.preventDefault()) + }, +})) diff --git a/services/web/frontend/js/directives/videoPlayState.js b/services/web/frontend/js/directives/videoPlayState.js new file mode 100644 index 0000000000..c99b5131a2 --- /dev/null +++ b/services/web/frontend/js/directives/videoPlayState.js @@ -0,0 +1,29 @@ +/* eslint-disable + max-len, +*/ +// TODO: This file was created by bulk-decaffeinate. +// Fix any style issues and re-enable lint. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +import App from '../base' + +export default App.directive('videoPlayState', $parse => ({ + restrict: 'A', + link(scope, element, attrs) { + const videoDOMEl = element[0] + return scope.$watch( + () => $parse(attrs.videoPlayState)(scope), + function (shouldPlay) { + if (shouldPlay) { + videoDOMEl.currentTime = 0 + return videoDOMEl.play() + } else { + return videoDOMEl.pause() + } + } + ) + }, +})) diff --git a/services/web/frontend/js/features/chat/components/chat-fallback-error.js b/services/web/frontend/js/features/chat/components/chat-fallback-error.js new file mode 100644 index 0000000000..38273505b2 --- /dev/null +++ b/services/web/frontend/js/features/chat/components/chat-fallback-error.js @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' +import { Button, Alert } from 'react-bootstrap' + +function ChatFallbackError({ reconnect }) { + const { t } = useTranslation() + + return ( + + ) +} + +ChatFallbackError.propTypes = { + reconnect: PropTypes.any, +} + +export default ChatFallbackError diff --git a/services/web/frontend/js/features/chat/components/chat-pane.js b/services/web/frontend/js/features/chat/components/chat-pane.js new file mode 100644 index 0000000000..215af12117 --- /dev/null +++ b/services/web/frontend/js/features/chat/components/chat-pane.js @@ -0,0 +1,114 @@ +import React, { useEffect } from 'react' +import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' + +import MessageList from './message-list' +import MessageInput from './message-input' +import InfiniteScroll from './infinite-scroll' +import ChatFallbackError from './chat-fallback-error' +import Icon from '../../../shared/components/icon' +import { useLayoutContext } from '../../../shared/context/layout-context' +import { useUserContext } from '../../../shared/context/user-context' +import withErrorBoundary from '../../../infrastructure/error-boundary' +import { FetchError } from '../../../infrastructure/fetch-json' +import { useChatContext } from '../context/chat-context' + +const ChatPane = React.memo(function ChatPane() { + const { t } = useTranslation() + + const { chatIsOpen } = useLayoutContext({ chatIsOpen: PropTypes.bool }) + const user = useUserContext({ + id: PropTypes.string.isRequired, + }) + + const { + status, + messages, + initialMessagesLoaded, + atEnd, + loadInitialMessages, + loadMoreMessages, + reset, + sendMessage, + markMessagesAsRead, + error, + } = useChatContext() + + useEffect(() => { + if (chatIsOpen && !initialMessagesLoaded) { + loadInitialMessages() + } + }, [chatIsOpen, loadInitialMessages, initialMessagesLoaded]) + + const shouldDisplayPlaceholder = status !== 'pending' && messages.length === 0 + + const messageContentCount = messages.reduce( + (acc, { contents }) => acc + contents.length, + 0 + ) + + if (error) { + // let user try recover from fetch errors + if (error instanceof FetchError) { + return + } + throw error + } + + if (!user) { + return null + } + + return ( + + ) +}) + +function LoadingSpinner() { + const { t } = useTranslation() + return ( +
+ + {` ${t('loading')}…`} +
+ ) +} + +function Placeholder() { + const { t } = useTranslation() + return ( + <> +
{t('no_messages')}
+
+ {t('send_first_message')} +
+ +
+ + ) +} + +export default withErrorBoundary(ChatPane, ChatFallbackError) diff --git a/services/web/frontend/js/features/chat/components/infinite-scroll.js b/services/web/frontend/js/features/chat/components/infinite-scroll.js new file mode 100644 index 0000000000..361fec55d4 --- /dev/null +++ b/services/web/frontend/js/features/chat/components/infinite-scroll.js @@ -0,0 +1,88 @@ +import { useRef, useEffect, useLayoutEffect } from 'react' +import PropTypes from 'prop-types' +import _ from 'lodash' + +const SCROLL_END_OFFSET = 30 + +function InfiniteScroll({ + atEnd, + children, + className = '', + fetchData, + itemCount, + isLoading, +}) { + const root = useRef(null) + + // we keep the value in a Ref instead of state so it can be safely used in effects + const scrollBottomRef = useRef(0) + function setScrollBottom(value) { + scrollBottomRef.current = value + } + + function updateScrollPosition() { + root.current.scrollTop = + root.current.scrollHeight - + root.current.clientHeight - + scrollBottomRef.current + } + + // Repositions the scroll after new items are loaded + useLayoutEffect(updateScrollPosition, [itemCount]) + + // Repositions the scroll after a window resize + useEffect(() => { + const handleResize = _.debounce(updateScrollPosition, 400) + window.addEventListener('resize', handleResize) + return () => { + window.removeEventListener('resize', handleResize) + } + }, []) + + function onScrollHandler(event) { + setScrollBottom( + root.current.scrollHeight - + root.current.scrollTop - + root.current.clientHeight + ) + if (event.target !== event.currentTarget) { + // Ignore scroll events on nested divs + // (this check won't be necessary in React 17: https://github.com/facebook/react/issues/15723 + return + } + if (shouldFetchData()) { + fetchData() + } + } + + function shouldFetchData() { + const containerIsLargerThanContent = + root.current.children[0].clientHeight < root.current.clientHeight + if (atEnd || isLoading || containerIsLargerThanContent) { + return false + } else { + return root.current.scrollTop < SCROLL_END_OFFSET + } + } + + return ( +
+ {children} +
+ ) +} + +InfiniteScroll.propTypes = { + atEnd: PropTypes.bool, + children: PropTypes.element.isRequired, + className: PropTypes.string, + fetchData: PropTypes.func.isRequired, + itemCount: PropTypes.number.isRequired, + isLoading: PropTypes.bool, +} + +export default InfiniteScroll diff --git a/services/web/frontend/js/features/chat/components/message-content.js b/services/web/frontend/js/features/chat/components/message-content.js new file mode 100644 index 0000000000..109dc52f11 --- /dev/null +++ b/services/web/frontend/js/features/chat/components/message-content.js @@ -0,0 +1,56 @@ +import { useRef, useEffect } from 'react' +import PropTypes from 'prop-types' +import Linkify from 'react-linkify' + +function MessageContent({ content }) { + const root = useRef(null) + + useEffect(() => { + if (!(window.MathJax && window.MathJax.Hub)) { + return + } + const MJHub = window.MathJax.Hub + const inlineMathConfig = + (MJHub.config && + MJHub.config.tex2jax && + MJHub.config.tex2jax.inlineMath) || + [] + const alreadyConfigured = inlineMathConfig.some( + c => c[0] === '$' && c[1] === '$' + ) + if (!alreadyConfigured) { + MJHub.Config({ + tex2jax: { + inlineMath: inlineMathConfig.concat([['$', '$']]), + }, + }) + } + }, []) + + useEffect(() => { + // adds attributes to all the links generated by , required due to https://github.com/tasti/react-linkify/issues/99 + for (const a of root.current.getElementsByTagName('a')) { + a.setAttribute('target', '_blank') + a.setAttribute('rel', 'noreferrer noopener') + } + + // MathJax typesetting + const MJHub = window.MathJax.Hub + const timeoutHandler = setTimeout(() => { + MJHub.Queue(['Typeset', MJHub, root.current]) + }, 0) + return () => clearTimeout(timeoutHandler) + }, [content]) + + return ( +

+ {content} +

+ ) +} + +MessageContent.propTypes = { + content: PropTypes.string.isRequired, +} + +export default MessageContent diff --git a/services/web/frontend/js/features/chat/components/message-input.js b/services/web/frontend/js/features/chat/components/message-input.js new file mode 100644 index 0000000000..a80b091072 --- /dev/null +++ b/services/web/frontend/js/features/chat/components/message-input.js @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' + +function MessageInput({ resetUnreadMessages, sendMessage }) { + const { t } = useTranslation() + + function handleKeyDown(event) { + if (event.key === 'Enter') { + event.preventDefault() + sendMessage(event.target.value) + event.target.value = '' // clears the textarea content + } + } + + return ( +
+ +