From aa94cec0220e87f73d65bf287a9e5805ac2507d3 Mon Sep 17 00:00:00 2001 From: yu-i-i Date: Tue, 3 Dec 2024 01:14:41 +0100 Subject: [PATCH 001/250] Enable autocomplete of reference keys feature --- develop/README.md | 1 + develop/dev.env | 1 + develop/docker-compose.dev.yml | 11 + develop/docker-compose.yml | 8 + server-ce/config/env.sh | 1 + server-ce/runit/references-overleaf/run | 12 + server-ce/services.js | 3 + services/references/.eslintrc | 6 + services/references/.gitignore | 5 + services/references/.mocharc.json | 3 + services/references/.nvmrc | 1 + services/references/Dockerfile | 27 + services/references/LICENSE | 662 ++++++ services/references/Makefile | 156 ++ services/references/README.md | 10 + services/references/app.js | 40 + .../app/js/ReferencesAPIController.js | 42 + services/references/app/js/bib2json.js | 1967 +++++++++++++++++ services/references/buildscript.txt | 9 + .../references/config/settings.defaults.cjs | 9 + services/references/docker-compose.ci.yml | 52 + services/references/docker-compose.yml | 56 + services/references/package.json | 26 + services/references/tsconfig.json | 12 + services/web/config/settings.defaults.js | 3 + 25 files changed, 3123 insertions(+) create mode 100755 server-ce/runit/references-overleaf/run create mode 100644 services/references/.eslintrc create mode 100644 services/references/.gitignore create mode 100644 services/references/.mocharc.json create mode 100644 services/references/.nvmrc create mode 100644 services/references/Dockerfile create mode 100644 services/references/LICENSE create mode 100644 services/references/Makefile create mode 100644 services/references/README.md create mode 100644 services/references/app.js create mode 100644 services/references/app/js/ReferencesAPIController.js create mode 100644 services/references/app/js/bib2json.js create mode 100644 services/references/buildscript.txt create mode 100644 services/references/config/settings.defaults.cjs create mode 100644 services/references/docker-compose.ci.yml create mode 100644 services/references/docker-compose.yml create mode 100644 services/references/package.json create mode 100644 services/references/tsconfig.json diff --git a/develop/README.md b/develop/README.md index 568259c4e3..14f7354572 100644 --- a/develop/README.md +++ b/develop/README.md @@ -77,6 +77,7 @@ each service: | `filestore` | 9235 | | `notifications` | 9236 | | `real-time` | 9237 | +| `references` | 9238 | | `history-v1` | 9239 | | `project-history` | 9240 | diff --git a/develop/dev.env b/develop/dev.env index aae91497db..2ccfef7052 100644 --- a/develop/dev.env +++ b/develop/dev.env @@ -13,6 +13,7 @@ NOTIFICATIONS_HOST=notifications PROJECT_HISTORY_HOST=project-history REALTIME_HOST=real-time REDIS_HOST=redis +REFERENCES_HOST=references SESSION_SECRET=foo WEBPACK_HOST=webpack WEB_API_PASSWORD=overleaf diff --git a/develop/docker-compose.dev.yml b/develop/docker-compose.dev.yml index 4432a24162..05844136a0 100644 --- a/develop/docker-compose.dev.yml +++ b/develop/docker-compose.dev.yml @@ -112,6 +112,17 @@ services: - ../services/real-time/app.js:/overleaf/services/real-time/app.js - ../services/real-time/config:/overleaf/services/real-time/config + references: + command: ["node", "--watch", "app.js"] + environment: + - NODE_OPTIONS=--inspect=0.0.0.0:9229 + ports: + - "127.0.0.1:9238:9229" + volumes: + - ../services/references/app:/overleaf/services/references/app + - ../services/references/config:/overleaf/services/references/config + - ../services/references/app.js:/overleaf/services/references/app.js + web: command: ["node", "--watch", "app.js", "--watch-locales"] environment: diff --git a/develop/docker-compose.yml b/develop/docker-compose.yml index 7161e0686a..dfecb964bf 100644 --- a/develop/docker-compose.yml +++ b/develop/docker-compose.yml @@ -131,6 +131,13 @@ services: volumes: - redis-data:/data + references: + build: + context: .. + dockerfile: services/references/Dockerfile + env_file: + - dev.env + web: build: context: .. @@ -161,6 +168,7 @@ services: - notifications - project-history - real-time + - references webpack: build: diff --git a/server-ce/config/env.sh b/server-ce/config/env.sh index b12ca242d3..81cebe4caa 100644 --- a/server-ce/config/env.sh +++ b/server-ce/config/env.sh @@ -9,5 +9,6 @@ export HISTORY_V1_HOST=127.0.0.1 export NOTIFICATIONS_HOST=127.0.0.1 export PROJECT_HISTORY_HOST=127.0.0.1 export REALTIME_HOST=127.0.0.1 +export REFERENCES_HOST=127.0.0.1 export WEB_HOST=127.0.0.1 export WEB_API_HOST=127.0.0.1 diff --git a/server-ce/runit/references-overleaf/run b/server-ce/runit/references-overleaf/run new file mode 100755 index 0000000000..875023df9f --- /dev/null +++ b/server-ce/runit/references-overleaf/run @@ -0,0 +1,12 @@ +#!/bin/bash + +NODE_PARAMS="" +if [ "$DEBUG_NODE" == "true" ]; then + echo "running debug - references" + NODE_PARAMS="--inspect=0.0.0.0:30560" +fi + +source /etc/overleaf/env.sh +export LISTEN_ADDRESS=127.0.0.1 + +exec /sbin/setuser www-data /usr/bin/node $NODE_PARAMS /overleaf/services/references/app.js >> /var/log/overleaf/references.log 2>&1 diff --git a/server-ce/services.js b/server-ce/services.js index d0b0a9c076..e0282f3bad 100644 --- a/server-ce/services.js +++ b/server-ce/services.js @@ -29,6 +29,9 @@ module.exports = [ { name: 'project-history', }, + { + name: 'references', + }, { name: 'history-v1', }, diff --git a/services/references/.eslintrc b/services/references/.eslintrc new file mode 100644 index 0000000000..cc68024d9d --- /dev/null +++ b/services/references/.eslintrc @@ -0,0 +1,6 @@ +{ + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + } +} diff --git a/services/references/.gitignore b/services/references/.gitignore new file mode 100644 index 0000000000..80bac793a7 --- /dev/null +++ b/services/references/.gitignore @@ -0,0 +1,5 @@ +node_modules +forever + +# managed by dev-environment$ bin/update_build_scripts +.npmrc diff --git a/services/references/.mocharc.json b/services/references/.mocharc.json new file mode 100644 index 0000000000..dc3280aa96 --- /dev/null +++ b/services/references/.mocharc.json @@ -0,0 +1,3 @@ +{ + "require": "test/setup.js" +} diff --git a/services/references/.nvmrc b/services/references/.nvmrc new file mode 100644 index 0000000000..0254b1e633 --- /dev/null +++ b/services/references/.nvmrc @@ -0,0 +1 @@ +20.18.2 diff --git a/services/references/Dockerfile b/services/references/Dockerfile new file mode 100644 index 0000000000..caa6e2a31c --- /dev/null +++ b/services/references/Dockerfile @@ -0,0 +1,27 @@ +# This file was auto-generated, do not edit it directly. +# Instead run bin/update_build_scripts from +# https://github.com/overleaf/internal/ + +FROM node:20.18.2 AS base + +WORKDIR /overleaf/services/references + +# Google Cloud Storage needs a writable $HOME/.config for resumable uploads +# (see https://googleapis.dev/nodejs/storage/latest/File.html#createWriteStream) +RUN mkdir /home/node/.config && chown node:node /home/node/.config + +FROM base AS app + +COPY package.json package-lock.json /overleaf/ +COPY services/references/package.json /overleaf/services/references/ +COPY libraries/ /overleaf/libraries/ +COPY patches/ /overleaf/patches/ + +RUN cd /overleaf && npm ci --quiet + +COPY services/references/ /overleaf/services/references/ + +FROM app +USER node + +CMD ["node", "--expose-gc", "app.js"] diff --git a/services/references/LICENSE b/services/references/LICENSE new file mode 100644 index 0000000000..ac8619dcb9 --- /dev/null +++ b/services/references/LICENSE @@ -0,0 +1,662 @@ + + 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/references/Makefile b/services/references/Makefile new file mode 100644 index 0000000000..e5181b46f3 --- /dev/null +++ b/services/references/Makefile @@ -0,0 +1,156 @@ +# This file was auto-generated, do not edit it directly. +# Instead run bin/update_build_scripts from +# https://github.com/overleaf/internal/ + +BUILD_NUMBER ?= local +BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) +PROJECT_NAME = references +BUILD_DIR_NAME = $(shell pwd | xargs basename | tr -cd '[a-zA-Z0-9_.\-]') + +DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml +DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ + BRANCH_NAME=$(BRANCH_NAME) \ + PROJECT_NAME=$(PROJECT_NAME) \ + MOCHA_GREP=${MOCHA_GREP} \ + docker compose ${DOCKER_COMPOSE_FLAGS} + +COMPOSE_PROJECT_NAME_TEST_ACCEPTANCE ?= test_acceptance_$(BUILD_DIR_NAME) +DOCKER_COMPOSE_TEST_ACCEPTANCE = \ + COMPOSE_PROJECT_NAME=$(COMPOSE_PROJECT_NAME_TEST_ACCEPTANCE) $(DOCKER_COMPOSE) + +COMPOSE_PROJECT_NAME_TEST_UNIT ?= test_unit_$(BUILD_DIR_NAME) +DOCKER_COMPOSE_TEST_UNIT = \ + COMPOSE_PROJECT_NAME=$(COMPOSE_PROJECT_NAME_TEST_UNIT) $(DOCKER_COMPOSE) + +clean: + -docker rmi ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + -docker rmi us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + -$(DOCKER_COMPOSE_TEST_UNIT) down --rmi local + -$(DOCKER_COMPOSE_TEST_ACCEPTANCE) down --rmi local + +HERE=$(shell pwd) +MONOREPO=$(shell cd ../../ && pwd) +# Run the linting commands in the scope of the monorepo. +# Eslint and prettier (plus some configs) are on the root. +RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) node:20.18.2 npm run --silent + +RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) npm run --silent + +# Same but from the top of the monorepo +RUN_LINTING_MONOREPO = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(MONOREPO) node:20.18.2 npm run --silent + +SHELLCHECK_OPTS = \ + --shell=bash \ + --external-sources +SHELLCHECK_COLOR := $(if $(CI),--color=never,--color) +SHELLCHECK_FILES := { git ls-files "*.sh" -z; git grep -Plz "\A\#\!.*bash"; } | sort -zu + +shellcheck: + @$(SHELLCHECK_FILES) | xargs -0 -r docker run --rm -v $(HERE):/mnt -w /mnt \ + koalaman/shellcheck:stable $(SHELLCHECK_OPTS) $(SHELLCHECK_COLOR) + +shellcheck_fix: + @$(SHELLCHECK_FILES) | while IFS= read -r -d '' file; do \ + diff=$$(docker run --rm -v $(HERE):/mnt -w /mnt koalaman/shellcheck:stable $(SHELLCHECK_OPTS) --format=diff "$$file" 2>/dev/null); \ + if [ -n "$$diff" ] && ! echo "$$diff" | patch -p1 >/dev/null 2>&1; then echo "\033[31m$$file\033[0m"; \ + elif [ -n "$$diff" ]; then echo "$$file"; \ + else echo "\033[2m$$file\033[0m"; fi \ + done + +format: + $(RUN_LINTING) format + +format_ci: + $(RUN_LINTING_CI) format + +format_fix: + $(RUN_LINTING) format:fix + +lint: + $(RUN_LINTING) lint + +lint_ci: + $(RUN_LINTING_CI) lint + +lint_fix: + $(RUN_LINTING) lint:fix + +typecheck: + $(RUN_LINTING) types:check + +typecheck_ci: + $(RUN_LINTING_CI) types:check + +test: format lint typecheck shellcheck test_unit test_acceptance + +test_unit: +ifneq (,$(wildcard test/unit)) + $(DOCKER_COMPOSE_TEST_UNIT) run --rm test_unit + $(MAKE) test_unit_clean +endif + +test_clean: test_unit_clean +test_unit_clean: +ifneq (,$(wildcard test/unit)) + $(DOCKER_COMPOSE_TEST_UNIT) down -v -t 0 +endif + +test_acceptance: test_acceptance_clean test_acceptance_pre_run test_acceptance_run + $(MAKE) test_acceptance_clean + +test_acceptance_debug: test_acceptance_clean test_acceptance_pre_run test_acceptance_run_debug + $(MAKE) test_acceptance_clean + +test_acceptance_run: +ifneq (,$(wildcard test/acceptance)) + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance +endif + +test_acceptance_run_debug: +ifneq (,$(wildcard test/acceptance)) + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance npm run test:acceptance -- --inspect=0.0.0.0:19999 --inspect-brk +endif + +test_clean: test_acceptance_clean +test_acceptance_clean: + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) down -v -t 0 + +test_acceptance_pre_run: +ifneq (,$(wildcard test/acceptance/js/scripts/pre-run)) + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance test/acceptance/js/scripts/pre-run +endif + +benchmarks: + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance npm run benchmarks + +build: + docker build \ + --pull \ + --build-arg BUILDKIT_INLINE_CACHE=1 \ + --tag ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ + --tag us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ + --tag us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME):$(BRANCH_NAME) \ + --cache-from us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME):$(BRANCH_NAME) \ + --cache-from us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME):main \ + --file Dockerfile \ + ../.. + +tar: + $(DOCKER_COMPOSE) up tar + +publish: + + docker push $(DOCKER_REPO)/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + + +.PHONY: clean \ + format format_fix \ + lint lint_fix \ + build_types typecheck \ + lint_ci format_ci typecheck_ci \ + shellcheck shellcheck_fix \ + test test_clean test_unit test_unit_clean \ + test_acceptance test_acceptance_debug test_acceptance_pre_run \ + test_acceptance_run test_acceptance_run_debug test_acceptance_clean \ + benchmarks \ + build tar publish \ diff --git a/services/references/README.md b/services/references/README.md new file mode 100644 index 0000000000..41844d259a --- /dev/null +++ b/services/references/README.md @@ -0,0 +1,10 @@ +overleaf/references +=============== + +An API for providing citation-keys from user bib-files + +License +======= +The code in this repository is released under the GNU AFFERO GENERAL PUBLIC LICENSE, version 3. + +Based on https://github.com/overleaf/overleaf/commit/9964aebc794f9fd7ce1373ab3484f6b33b061af3 diff --git a/services/references/app.js b/services/references/app.js new file mode 100644 index 0000000000..a7da8720ed --- /dev/null +++ b/services/references/app.js @@ -0,0 +1,40 @@ +import '@overleaf/metrics/initialize.js' + +import express from 'express' +import Settings from '@overleaf/settings' +import logger from '@overleaf/logger' +import metrics from '@overleaf/metrics' +import ReferencesAPIController from './app/js/ReferencesAPIController.js' +import bodyParser from 'body-parser' + +const app = express() +metrics.injectMetricsRoute(app) + +app.use(bodyParser.json({ limit: '2mb' })) +app.use(metrics.http.monitor(logger)) + +app.post('/project/:project_id/index', ReferencesAPIController.index) +app.get('/status', (req, res) => res.send({ status: 'references api is up' })) + +const settings = + Settings.internal && Settings.internal.references + ? Settings.internal.references + : undefined +const host = settings && settings.host ? settings.host : 'localhost' +const port = settings && settings.port ? settings.port : 3056 + +logger.debug('Listening at', { host, port }) + +const server = app.listen(port, host, function (error) { + if (error) { + throw error + } + logger.info({ host, port }, 'references HTTP server starting up') +}) + +process.on('SIGTERM', () => { + server.close(() => { + logger.info({ host, port }, 'references HTTP server closed') + metrics.close() + }) +}) diff --git a/services/references/app/js/ReferencesAPIController.js b/services/references/app/js/ReferencesAPIController.js new file mode 100644 index 0000000000..ac51ca6bbd --- /dev/null +++ b/services/references/app/js/ReferencesAPIController.js @@ -0,0 +1,42 @@ +import logger from '@overleaf/logger' +import BibtexParser from './bib2json.js' + +export default { + async index(req, res) { + const { docUrls, fullIndex } = req.body + try { + const responses = await Promise.all( + docUrls.map(async (docUrl) => { + try { + const response = await fetch(docUrl) + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + return response.text() + } catch (error) { + logger.error({ error }, "Failed to fetch document from URL: " + docUrl) + return null + } + }) + ) + const keys = [] + for (const body of responses) { + if (!body) continue + + try { + const parsedEntries = BibtexParser(body).entries + const ks = parsedEntries + .filter(entry => entry.EntryKey) + .map(entry => entry.EntryKey) + keys.push(...ks) + } catch (error) { + logger.error({ error }, "bib file skipped.") + } + } + res.status(200).json({ keys }) + } catch (error) { + logger.error({ error }, "Unexpected error during indexing process.") + res.status(500).json({ error: "Failed to process bib files." }) + } + } +} diff --git a/services/references/app/js/bib2json.js b/services/references/app/js/bib2json.js new file mode 100644 index 0000000000..99cfcf70ee --- /dev/null +++ b/services/references/app/js/bib2json.js @@ -0,0 +1,1967 @@ +/* eslint-disable */ +/** + * Parser.js + * Copyright 2012-13 Mayank Lahiri + * mlahiri@gmail.com + * Released under the BSD License. + * + * Modifications 2016 Sharelatex + * Modifications 2017-2020 Overleaf + * + * A forgiving Bibtex parser that can: + * + * (1) operate in streaming or block mode, extracting entries as dictionaries. + * (2) convert Latex special characters to UTF-8. + * (3) best-effort parse malformed entries. + * (4) run in a CommonJS environment or a browser, without any dependencies. + * (5) be advanced-compiled by Google Closure Compiler. + * + * Handwritten as a labor of love, not auto-generated from a grammar. + * + * Modes of usage: + * + * (1) Synchronous, string + * + * var entries = BibtexParser(text); + * console.log(entries); + * + * (2) Asynchronous, stream + * + * function entryCallback(entry) { console.log(entry); } + * var parser = new BibtexParser(entryCallback); + * parser.parse(chunk1); + * parser.parse(chunk2); + * ... + * + * @param {text|function(Object)} arg0 Either a Bibtex string or callback + * function for processing parsed entries. + * @param {array} allowedKeys optimization: do not output key/value pairs that are not on this allowlist + * @constructor + */ +function BibtexParser(arg0, allowedKeys) { + // Determine how this function is to be used + if (typeof arg0 === 'string') { + // Passed a string, synchronous call without 'new' + const entries = [] + function accumulator(entry) { + entries.push(entry) + } + const parser = new BibtexParser(accumulator, allowedKeys) + parser.parse(arg0) + return { + entries, + errors: parser.getErrors(), + } + } + if (typeof arg0 !== 'function') { + throw 'Invalid parser construction.' + } + this.ALLOWEDKEYS_ = allowedKeys || [] + this.reset_(arg0) + this.initMacros_() + return this +} + +/** @enum {number} */ +BibtexParser.prototype.STATES_ = { + ENTRY_OR_JUNK: 0, + OBJECT_TYPE: 1, + ENTRY_KEY: 2, + KV_KEY: 3, + EQUALS: 4, + KV_VALUE: 5, +} +BibtexParser.prototype.reset_ = function (arg0) { + /** @private */ this.DATA_ = {} + /** @private */ this.CALLBACK_ = arg0 + /** @private */ this.CHAR_ = 0 + /** @private */ this.LINE_ = 1 + /** @private */ this.CHAR_IN_LINE_ = 0 + /** @private */ this.SKIPWS_ = true + /** @private */ this.SKIPCOMMENT_ = true + /** @private */ this.SKIPKVPAIR_ = false + /** @private */ this.PARSETMP_ = {} + /** @private */ this.SKIPTILLEOL_ = false + /** @private */ this.VALBRACES_ = null + /** @private */ this.BRACETYPE_ = null + /** @private */ this.BRACECOUNT_ = 0 + /** @private */ this.STATE_ = this.STATES_.ENTRY_OR_JUNK + /** @private */ this.ERRORS_ = [] +} +/** @private */ BibtexParser.prototype.ENTRY_TYPES_ = { + inproceedings: 1, + proceedings: 2, + article: 3, + techreport: 4, + misc: 5, + mastersthesis: 6, + book: 7, + phdthesis: 8, + incollection: 9, + unpublished: 10, + inbook: 11, + manual: 12, + periodical: 13, + booklet: 14, + masterthesis: 15, + conference: 16, + /* additional fields from biblatex */ + artwork: 17, + audio: 18, + bibnote: 19, + bookinbook: 20, + collection: 21, + commentary: 22, + customa: 23, + customb: 24, + customc: 25, + customd: 26, + custome: 27, + customf: 28, + image: 29, + inreference: 30, + jurisdiction: 31, + legal: 32, + legislation: 33, + letter: 34, + movie: 35, + music: 36, + mvbook: 37, + mvcollection: 38, + mvproceedings: 39, + mvreference: 40, + online: 41, + patent: 42, + performance: 43, + reference: 44, + report: 45, + review: 46, + set: 47, + software: 48, + standard: 49, + suppbook: 50, + suppcollection: 51, + thesis: 52, + video: 53, +} +BibtexParser.prototype.initMacros_ = function () { + // macros can be extended by the user via + // @string { macroName = "macroValue" } + /** @private */ this.MACROS_ = { + jan: 'January', + feb: 'February', + mar: 'March', + apr: 'April', + may: 'May', + jun: 'June', + jul: 'July', + aug: 'August', + sep: 'September', + oct: 'October', + nov: 'November', + dec: 'December', + Jan: 'January', + Feb: 'February', + Mar: 'March', + Apr: 'April', + May: 'May', + Jun: 'June', + Jul: 'July', + Aug: 'August', + Sep: 'September', + Oct: 'October', + Nov: 'November', + Dec: 'December', + } +} + +/** + * Gets an array of all errors encountered during parsing. + * Array entries are of the format: + * [ line number, character in line, character in stream, error text ] + * + * @returns Array + * @public + */ +BibtexParser.prototype.getErrors = function () { + return this.ERRORS_ +} + +/** + * Processes a chunk of data + * @public + */ +BibtexParser.prototype.parse = function (chunk) { + for (let i = 0; i < chunk.length; i++) this.processCharacter_(chunk[i]) +} + +/** + * Logs error at current stream position. + * + * @private + */ +BibtexParser.prototype.error_ = function (text) { + this.ERRORS_.push([this.LINE_, this.CHAR_IN_LINE_, this.CHAR_, text]) +} + +/** + * Called after an entire entry has been parsed from the stream. + * Performs post-processing and invokes the entry callback pointed to by + * this.CALLBACK_. Parsed (but unprocessed) entry data is in this.DATA_. + */ +BibtexParser.prototype.processEntry_ = function () { + const data = this.DATA_ + if (data.Fields) + for (const f in data.Fields) { + let raw = data.Fields[f] + + // Convert Latex/Bibtex special characters to UTF-8 equivalents + for (let i = 0; i < this.CHARCONV_.length; i++) { + const re = this.CHARCONV_[i][0] + const rep = this.CHARCONV_[i][1] + raw = raw.replace(re, rep) + } + + // Basic substitutions + raw = raw + .replace(/[\n\r\t]/g, ' ') + .replace(/\s\s+/g, ' ') + .replace(/^\s+|\s+$/g, '') + + // Remove braces and backslashes + const len = raw.length + let processedArr = [] + for (let i = 0; i < len; i++) { + let c = raw[i] + let skip = false + if (c == '\\' && i < len - 1) c = raw[++i] + else { + if (c == '{' || c == '}') skip = true + } + if (!skip) processedArr.push(c) + } + data.Fields[f] = processedArr.join('') + processedArr = null + } + + if (data.ObjectType == 'string') { + for (const f in data.Fields) { + this.MACROS_[f] = data.Fields[f] + } + } else { + // Parsed a new Bibtex entry + this.CALLBACK_(data) + } +} + +/** + * Processes next character in the stream, invoking the callback after + * each entry has been found and processed. + * + * @private + * @param {string} c Next character in input stream + */ +BibtexParser.prototype.processCharacter_ = function (c) { + // Housekeeping + this.CHAR_++ + this.CHAR_IN_LINE_++ + if (c == '\n') { + this.LINE_++ + this.CHAR_IN_LINE_ = 1 + } + + // Convenience states for skipping whitespace when needed + if (this.SKIPTILLEOL_) { + if (c == '\n') this.SKIPTILLEOL_ = false + return + } + if (this.SKIPCOMMENT_ && c == '%') { + this.SKIPTILLEOL_ = true + return + } + if (this.SKIPWS_ && /\s/.test(c)) return + this.SKIPWS_ = false + this.SKIPCOMMENT_ = false + this.SKIPTILLEOL_ = false + + // Main state machine + let AnotherIteration = true + while (AnotherIteration) { + // console.log(this.LINE_, this.CHAR_IN_LINE_, this.STATE_, c) + AnotherIteration = false + switch (this.STATE_) { + // -- Scan for an object marker ('@') + // -- Reset temporary data structure in case previous entry was garbled + case this.STATES_.ENTRY_OR_JUNK: + if (c == '@') { + // SUCCESS: Parsed a valid start-of-object marker. + // NEXT_STATE: OBJECT_TYPE + this.STATE_ = this.STATES_.OBJECT_TYPE + this.DATA_ = { + ObjectType: '', + } + } + this.BRACETYPE_ = null + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + break + + // Start at first non-whitespace character after start-of-object '@' + // -- Accept [A-Za-z], break on non-matching character + // -- Populate this.DATA_.EntryType and this.DATA_.ObjectType + case this.STATES_.OBJECT_TYPE: + if (/[A-Za-z]/.test(c)) { + this.DATA_.ObjectType += c.toLowerCase() + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + } else { + // Break from state and validate object type + const ot = this.DATA_.ObjectType + if (ot == 'comment') { + this.STATE_ = this.STATES_.ENTRY_OR_JUNK + } else { + if (ot == 'string') { + this.DATA_.ObjectType = ot + this.DATA_.Fields = {} + this.BRACETYPE_ = c + this.BRACECOUNT_ = 1 + this.STATE_ = this.STATES_.KV_KEY + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + this.PARSETMP_ = { + Key: '', + } + } else { + if (ot == 'preamble') { + this.STATE_ = this.STATES_.ENTRY_OR_JUNK + } else { + if (ot in this.ENTRY_TYPES_) { + // SUCCESS: Parsed a valid object type. + // NEXT_STATE: ENTRY_KEY + this.DATA_.ObjectType = 'entry' + this.DATA_.EntryType = ot + this.DATA_.EntryKey = '' + this.STATE_ = this.STATES_.ENTRY_KEY + AnotherIteration = true + } else { + // ERROR: Unrecognized object type. + // NEXT_STATE: ENTRY_OR_JUNK + this.error_( + 'Unrecognized object type: "' + this.DATA_.ObjectType + '"' + ) + this.STATE_ = this.STATES_.ENTRY_OR_JUNK + } + } + } + } + } + break + + // Start at first non-alphabetic character after an entry type + // -- Populate this.DATA_.EntryKey + case this.STATES_.ENTRY_KEY: + if ((c === '{' || c === '(') && this.BRACETYPE_ == null) { + this.BRACETYPE_ = c + this.BRACECOUNT_ = 1 + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + break + } + if (/[,%\s]/.test(c)) { + if (this.DATA_.EntryKey.length < 1) { + // Skip comments and whitespace before entry key + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + } else { + if (this.BRACETYPE_ == null) { + // ERROR: No opening brace for object + // NEXT_STATE: ENTRY_OR_JUNK + this.error_('No opening brace for object.') + this.STATE_ = this.STATES_.ENTRY_OR_JUNK + } else { + // SUCCESS: Parsed an entry key + // NEXT_STATE: KV_KEY + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + AnotherIteration = true + this.STATE_ = this.STATES_.KV_KEY + this.PARSETMP_.Key = '' + this.DATA_.Fields = {} + } + } + } else { + this.DATA_.EntryKey += c + this.SKIPWS_ = false + this.SKIPCOMMENT_ = false + } + break + + // Start at first non-whitespace/comment character after entry key. + // -- Populate this.PARSETMP_.Key + case this.STATES_.KV_KEY: + // Test for end of entry + if ( + (c == '}' && this.BRACETYPE_ == '{') || + (c == ')' && this.BRACETYPE_ == '(') + ) { + // SUCCESS: Parsed an entry, possible incomplete + // NEXT_STATE: ENTRY_OR_JUNK + this.processEntry_() + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + this.STATE_ = this.STATES_.ENTRY_OR_JUNK + break + } + if (/[\-A-Za-z:]/.test(c)) { + // Add to key + this.PARSETMP_.Key += c + this.SKIPWS_ = false + this.SKIPCOMMENT_ = false + } else { + // Either end of key or we haven't encountered start of key + if (this.PARSETMP_.Key.length < 1) { + // Keep going till we see a key + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + } else { + // SUCCESS: Found full key in K/V pair + // NEXT_STATE: EQUALS + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + this.STATE_ = this.STATES_.EQUALS + AnotherIteration = true + + if (this.DATA_.ObjectType !== 'string') { + // this entry is not a macro + // normalize the key to lower case + this.PARSETMP_.Key = this.PARSETMP_.Key.toLowerCase() + + // optimization: skip key/value pairs that are not on the allowlist + this.SKIPKVPAIR_ = + // has allowedKeys set + this.ALLOWEDKEYS_.length && + // key is not on the allowlist + this.ALLOWEDKEYS_.indexOf(this.PARSETMP_.Key) === -1 + } else { + this.SKIPKVPAIR_ = false + } + } + } + break + + // Start at first non-alphabetic character after K/V pair key. + case this.STATES_.EQUALS: + if ( + (c == '}' && this.BRACETYPE_ == '{') || + (c == ')' && this.BRACETYPE_ == '(') + ) { + // ERROR: K/V pair with key but no value + // NEXT_STATE: ENTRY_OR_JUNK + this.error_( + 'Key-value pair has key "' + this.PARSETMP_.Key + '", but no value.' + ) + this.processEntry_() + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + this.STATE_ = this.STATES_.ENTRY_OR_JUNK + break + } + if (c == '=') { + // SUCCESS: found an equal signs separating key and value + // NEXT_STATE: KV_VALUE + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + this.STATE_ = this.STATES_.KV_VALUE + this.PARSETMP_.Value = [] + this.VALBRACES_ = { '"': [], '{': [] } + } + break + + // Start at first non-whitespace/comment character after '=' + // -- Populate this.PARSETMP_.Value + case this.STATES_.KV_VALUE: + const delim = this.VALBRACES_ + // valueCharsArray is the list of characters that make up the + // current value + const valueCharsArray = this.PARSETMP_.Value + let doneParsingValue = false + + // Test for special characters + if (c == '"' || c == '{' || c == '}' || c == ',') { + if (c == ',') { + // This comma can mean: + // (1) just another comma literal + // (2) end of a macro reference + if (delim['"'].length + delim['{'].length === 0) { + // end of a macro reference + const macro = this.PARSETMP_.Value.join('').trim() + if (macro in this.MACROS_) { + // Successful macro reference + this.PARSETMP_.Value = [this.MACROS_[macro]] + } else { + // Reference to an undefined macro + this.error_('Reference to an undefined macro: ' + macro) + } + doneParsingValue = true + } + } + if (c == '"') { + // This quote can mean: + // (1) opening delimiter + // (2) closing delimiter + // (3) literal, if we have a '{' on the stack + if (delim['"'].length + delim['{'].length === 0) { + // opening delimiter + delim['"'].push(this.CHAR_) + this.SKIPWS_ = false + this.SKIPCOMMENT_ = false + break + } + if ( + delim['"'].length == 1 && + delim['{'].length == 0 && + (valueCharsArray.length == 0 || + valueCharsArray[valueCharsArray.length - 1] != '\\') + ) { + // closing delimiter + doneParsingValue = true + } else { + // literal, add to value + } + } + if (c == '{') { + // This brace can mean: + // (1) opening delimiter + // (2) stacked verbatim delimiter + if ( + valueCharsArray.length == 0 || + valueCharsArray[valueCharsArray.length - 1] != '\\' + ) { + delim['{'].push(this.CHAR_) + this.SKIPWS_ = false + this.SKIPCOMMENT_ = false + } else { + // literal, add to value + } + } + if (c == '}') { + // This brace can mean: + // (1) closing delimiter + // (2) closing stacked verbatim delimiter + // (3) end of object definition if value was a macro + if (delim['"'].length + delim['{'].length === 0) { + // end of object definition, after macro + const macro = this.PARSETMP_.Value.join('').trim() + if (macro in this.MACROS_) { + // Successful macro reference + this.PARSETMP_.Value = [this.MACROS_[macro]] + } else { + // Reference to an undefined macro + this.error_('Reference to an undefined macro: ' + macro) + } + AnotherIteration = true + doneParsingValue = true + } else { + // sometimes imported bibs will have {\},{\\}, {\\\}, {\\\\}, etc for whitespace, + // which would otherwise break the parsing. we watch for these occurences of + // 1+ backslashes in an empty bracket pair to gracefully handle the malformed bib file + const doubleSlash = + valueCharsArray.length >= 2 && + valueCharsArray[valueCharsArray.length - 1] === '\\' && // for \\} + valueCharsArray[valueCharsArray.length - 2] === '\\' + const singleSlash = + valueCharsArray.length >= 2 && + valueCharsArray[valueCharsArray.length - 1] === '\\' && // for {\} + valueCharsArray[valueCharsArray.length - 2] === '{' + + if ( + valueCharsArray.length == 0 || + valueCharsArray[valueCharsArray.length - 1] != '\\' || // for } + doubleSlash || + singleSlash + ) { + if (delim['{'].length > 0) { + // pop stack for stacked verbatim delimiter + delim['{'].splice(delim['{'].length - 1, 1) + if (delim['{'].length + delim['"'].length == 0) { + // closing delimiter + doneParsingValue = true + } else { + // end verbatim block + } + } + } else { + // literal, add to value + } + } + } + } + + // If here, then we are either done parsing the value or + // have a literal that should be added to the value. + if (doneParsingValue) { + // SUCCESS: value parsed + // NEXT_STATE: KV_KEY + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + this.STATE_ = this.STATES_.KV_KEY + if (!this.SKIPKVPAIR_) { + this.DATA_.Fields[this.PARSETMP_.Key] = + this.PARSETMP_.Value.join('') + } + this.PARSETMP_ = { Key: '' } + this.VALBRACES_ = null + } else { + this.PARSETMP_.Value.push(c) + if (this.PARSETMP_.Value.length >= 1000 * 20) { + this.PARSETMP_.Value = [] + this.STATE_ = this.STATES_.ENTRY_OR_JUNK + this.DATA_ = { ObjectType: '' } + this.BRACETYPE_ = null + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + } + } + break + } // end switch (this.STATE_) + } // end while(AnotherIteration) +} // end function processCharacter + +/** @private */ BibtexParser.prototype.CHARCONV_ = [ + [/\\space /g, '\u0020'], + [/\\textdollar /g, '\u0024'], + [/\\textquotesingle /g, '\u0027'], + [/\\ast /g, '\u002A'], + [/\\textbackslash /g, '\u005C'], + [/\\\^\{\}/g, '\u005E'], + [/\\textasciigrave /g, '\u0060'], + [/\\lbrace /g, '\u007B'], + [/\\vert /g, '\u007C'], + [/\\rbrace /g, '\u007D'], + [/\\textasciitilde /g, '\u007E'], + [/\\textexclamdown /g, '\u00A1'], + [/\\textcent /g, '\u00A2'], + [/\\textsterling /g, '\u00A3'], + [/\\textcurrency /g, '\u00A4'], + [/\\textyen /g, '\u00A5'], + [/\\textbrokenbar /g, '\u00A6'], + [/\\textsection /g, '\u00A7'], + [/\\textasciidieresis /g, '\u00A8'], + [/\\textcopyright /g, '\u00A9'], + [/\\textordfeminine /g, '\u00AA'], + [/\\guillemotleft /g, '\u00AB'], + [/\\lnot /g, '\u00AC'], + [/\\textregistered /g, '\u00AE'], + [/\\textasciimacron /g, '\u00AF'], + [/\\textdegree /g, '\u00B0'], + [/\\pm /g, '\u00B1'], + [/\\textasciiacute /g, '\u00B4'], + [/\\mathrm\{\\mu\}/g, '\u00B5'], + [/\\textparagraph /g, '\u00B6'], + [/\\cdot /g, '\u00B7'], + [/\\c\{\}/g, '\u00B8'], + [/\\textordmasculine /g, '\u00BA'], + [/\\guillemotright /g, '\u00BB'], + [/\\textonequarter /g, '\u00BC'], + [/\\textonehalf /g, '\u00BD'], + [/\\textthreequarters /g, '\u00BE'], + [/\\textquestiondown /g, '\u00BF'], + [/\\`\{A\}/g, '\u00C0'], + [/\\'\{A\}/g, '\u00C1'], + [/\\\^\{A\}/g, '\u00C2'], + [/\\~\{A\}/g, '\u00C3'], + [/\\"\{A\}/g, '\u00C4'], + [/\\AA /g, '\u00C5'], + [/\\AE /g, '\u00C6'], + [/\\c\{C\}/g, '\u00C7'], + [/\\`\{E\}/g, '\u00C8'], + [/\\'\{E\}/g, '\u00C9'], + [/\\\^\{E\}/g, '\u00CA'], + [/\\"\{E\}/g, '\u00CB'], + [/\\`\{I\}/g, '\u00CC'], + [/\\'\{I\}/g, '\u00CD'], + [/\\\^\{I\}/g, '\u00CE'], + [/\\"\{I\}/g, '\u00CF'], + [/\\DH /g, '\u00D0'], + [/\\~\{N\}/g, '\u00D1'], + [/\\`\{O\}/g, '\u00D2'], + [/\\'\{O\}/g, '\u00D3'], + [/\\\^\{O\}/g, '\u00D4'], + [/\\~\{O\}/g, '\u00D5'], + [/\\"\{O\}/g, '\u00D6'], + [/\\texttimes /g, '\u00D7'], + [/\\O /g, '\u00D8'], + [/\\`\{U\}/g, '\u00D9'], + [/\\'\{U\}/g, '\u00DA'], + [/\\\^\{U\}/g, '\u00DB'], + [/\\"\{U\}/g, '\u00DC'], + [/\\'\{Y\}/g, '\u00DD'], + [/\\TH /g, '\u00DE'], + [/\\ss /g, '\u00DF'], + [/\\`\{a\}/g, '\u00E0'], + [/\\'\{a\}/g, '\u00E1'], + [/\\\^\{a\}/g, '\u00E2'], + [/\\~\{a\}/g, '\u00E3'], + [/\\"\{a\}/g, '\u00E4'], + [/\\aa /g, '\u00E5'], + [/\\ae /g, '\u00E6'], + [/\\c\{c\}/g, '\u00E7'], + [/\\`\{e\}/g, '\u00E8'], + [/\\'\{e\}/g, '\u00E9'], + [/\\\^\{e\}/g, '\u00EA'], + [/\\"\{e\}/g, '\u00EB'], + [/\\`\{\\i\}/g, '\u00EC'], + [/\\'\{\\i\}/g, '\u00ED'], + [/\\\^\{\\i\}/g, '\u00EE'], + [/\\"\{\\i\}/g, '\u00EF'], + [/\\dh /g, '\u00F0'], + [/\\~\{n\}/g, '\u00F1'], + [/\\`\{o\}/g, '\u00F2'], + [/\\'\{o\}/g, '\u00F3'], + [/\\\^\{o\}/g, '\u00F4'], + [/\\~\{o\}/g, '\u00F5'], + [/\\"\{o\}/g, '\u00F6'], + [/\\div /g, '\u00F7'], + [/\\o /g, '\u00F8'], + [/\\`\{u\}/g, '\u00F9'], + [/\\'\{u\}/g, '\u00FA'], + [/\\\^\{u\}/g, '\u00FB'], + [/\\"\{u\}/g, '\u00FC'], + [/\\'\{y\}/g, '\u00FD'], + [/\\th /g, '\u00FE'], + [/\\"\{y\}/g, '\u00FF'], + [/\\=\{A\}/g, '\u0100'], + [/\\=\{a\}/g, '\u0101'], + [/\\u\{A\}/g, '\u0102'], + [/\\u\{a\}/g, '\u0103'], + [/\\k\{A\}/g, '\u0104'], + [/\\k\{a\}/g, '\u0105'], + [/\\'\{C\}/g, '\u0106'], + [/\\'\{c\}/g, '\u0107'], + [/\\\^\{C\}/g, '\u0108'], + [/\\\^\{c\}/g, '\u0109'], + [/\\.\{C\}/g, '\u010A'], + [/\\.\{c\}/g, '\u010B'], + [/\\v\{C\}/g, '\u010C'], + [/\\v\{c\}/g, '\u010D'], + [/\\v\{D\}/g, '\u010E'], + [/\\v\{d\}/g, '\u010F'], + [/\\DJ /g, '\u0110'], + [/\\dj /g, '\u0111'], + [/\\=\{E\}/g, '\u0112'], + [/\\=\{e\}/g, '\u0113'], + [/\\u\{E\}/g, '\u0114'], + [/\\u\{e\}/g, '\u0115'], + [/\\.\{E\}/g, '\u0116'], + [/\\.\{e\}/g, '\u0117'], + [/\\k\{E\}/g, '\u0118'], + [/\\k\{e\}/g, '\u0119'], + [/\\v\{E\}/g, '\u011A'], + [/\\v\{e\}/g, '\u011B'], + [/\\\^\{G\}/g, '\u011C'], + [/\\\^\{g\}/g, '\u011D'], + [/\\u\{G\}/g, '\u011E'], + [/\\u\{g\}/g, '\u011F'], + [/\\.\{G\}/g, '\u0120'], + [/\\.\{g\}/g, '\u0121'], + [/\\c\{G\}/g, '\u0122'], + [/\\c\{g\}/g, '\u0123'], + [/\\\^\{H\}/g, '\u0124'], + [/\\\^\{h\}/g, '\u0125'], + [/\\Elzxh /g, '\u0127'], + [/\\~\{I\}/g, '\u0128'], + [/\\~\{\\i\}/g, '\u0129'], + [/\\=\{I\}/g, '\u012A'], + [/\\=\{\\i\}/g, '\u012B'], + [/\\u\{I\}/g, '\u012C'], + [/\\u\{\\i\}/g, '\u012D'], + [/\\k\{I\}/g, '\u012E'], + [/\\k\{i\}/g, '\u012F'], + [/\\.\{I\}/g, '\u0130'], + [/\\i /g, '\u0131'], + [/\\\^\{J\}/g, '\u0134'], + [/\\\^\{\\j\}/g, '\u0135'], + [/\\c\{K\}/g, '\u0136'], + [/\\c\{k\}/g, '\u0137'], + [/\\'\{L\}/g, '\u0139'], + [/\\'\{l\}/g, '\u013A'], + [/\\c\{L\}/g, '\u013B'], + [/\\c\{l\}/g, '\u013C'], + [/\\v\{L\}/g, '\u013D'], + [/\\v\{l\}/g, '\u013E'], + [/\\L /g, '\u0141'], + [/\\l /g, '\u0142'], + [/\\'\{N\}/g, '\u0143'], + [/\\'\{n\}/g, '\u0144'], + [/\\c\{N\}/g, '\u0145'], + [/\\c\{n\}/g, '\u0146'], + [/\\v\{N\}/g, '\u0147'], + [/\\v\{n\}/g, '\u0148'], + [/\\NG /g, '\u014A'], + [/\\ng /g, '\u014B'], + [/\\=\{O\}/g, '\u014C'], + [/\\=\{o\}/g, '\u014D'], + [/\\u\{O\}/g, '\u014E'], + [/\\u\{o\}/g, '\u014F'], + [/\\H\{O\}/g, '\u0150'], + [/\\H\{o\}/g, '\u0151'], + [/\\OE /g, '\u0152'], + [/\\oe /g, '\u0153'], + [/\\'\{R\}/g, '\u0154'], + [/\\'\{r\}/g, '\u0155'], + [/\\c\{R\}/g, '\u0156'], + [/\\c\{r\}/g, '\u0157'], + [/\\v\{R\}/g, '\u0158'], + [/\\v\{r\}/g, '\u0159'], + [/\\'\{S\}/g, '\u015A'], + [/\\'\{s\}/g, '\u015B'], + [/\\\^\{S\}/g, '\u015C'], + [/\\\^\{s\}/g, '\u015D'], + [/\\c\{S\}/g, '\u015E'], + [/\\c\{s\}/g, '\u015F'], + [/\\v\{S\}/g, '\u0160'], + [/\\v\{s\}/g, '\u0161'], + [/\\c\{T\}/g, '\u0162'], + [/\\c\{t\}/g, '\u0163'], + [/\\v\{T\}/g, '\u0164'], + [/\\v\{t\}/g, '\u0165'], + [/\\~\{U\}/g, '\u0168'], + [/\\~\{u\}/g, '\u0169'], + [/\\=\{U\}/g, '\u016A'], + [/\\=\{u\}/g, '\u016B'], + [/\\u\{U\}/g, '\u016C'], + [/\\u\{u\}/g, '\u016D'], + [/\\r\{U\}/g, '\u016E'], + [/\\r\{u\}/g, '\u016F'], + [/\\H\{U\}/g, '\u0170'], + [/\\H\{u\}/g, '\u0171'], + [/\\k\{U\}/g, '\u0172'], + [/\\k\{u\}/g, '\u0173'], + [/\\\^\{W\}/g, '\u0174'], + [/\\\^\{w\}/g, '\u0175'], + [/\\\^\{Y\}/g, '\u0176'], + [/\\\^\{y\}/g, '\u0177'], + [/\\"\{Y\}/g, '\u0178'], + [/\\'\{Z\}/g, '\u0179'], + [/\\'\{z\}/g, '\u017A'], + [/\\.\{Z\}/g, '\u017B'], + [/\\.\{z\}/g, '\u017C'], + [/\\v\{Z\}/g, '\u017D'], + [/\\v\{z\}/g, '\u017E'], + [/\\texthvlig /g, '\u0195'], + [/\\textnrleg /g, '\u019E'], + [/\\eth /g, '\u01AA'], + [/\\textdoublepipe /g, '\u01C2'], + [/\\'\{g\}/g, '\u01F5'], + [/\\Elztrna /g, '\u0250'], + [/\\Elztrnsa /g, '\u0252'], + [/\\Elzopeno /g, '\u0254'], + [/\\Elzrtld /g, '\u0256'], + [/\\Elzschwa /g, '\u0259'], + [/\\varepsilon /g, '\u025B'], + [/\\Elzpgamma /g, '\u0263'], + [/\\Elzpbgam /g, '\u0264'], + [/\\Elztrnh /g, '\u0265'], + [/\\Elzbtdl /g, '\u026C'], + [/\\Elzrtll /g, '\u026D'], + [/\\Elztrnm /g, '\u026F'], + [/\\Elztrnmlr /g, '\u0270'], + [/\\Elzltlmr /g, '\u0271'], + [/\\Elzltln /g, '\u0272'], + [/\\Elzrtln /g, '\u0273'], + [/\\Elzclomeg /g, '\u0277'], + [/\\textphi /g, '\u0278'], + [/\\Elztrnr /g, '\u0279'], + [/\\Elztrnrl /g, '\u027A'], + [/\\Elzrttrnr /g, '\u027B'], + [/\\Elzrl /g, '\u027C'], + [/\\Elzrtlr /g, '\u027D'], + [/\\Elzfhr /g, '\u027E'], + [/\\Elzrtls /g, '\u0282'], + [/\\Elzesh /g, '\u0283'], + [/\\Elztrnt /g, '\u0287'], + [/\\Elzrtlt /g, '\u0288'], + [/\\Elzpupsil /g, '\u028A'], + [/\\Elzpscrv /g, '\u028B'], + [/\\Elzinvv /g, '\u028C'], + [/\\Elzinvw /g, '\u028D'], + [/\\Elztrny /g, '\u028E'], + [/\\Elzrtlz /g, '\u0290'], + [/\\Elzyogh /g, '\u0292'], + [/\\Elzglst /g, '\u0294'], + [/\\Elzreglst /g, '\u0295'], + [/\\Elzinglst /g, '\u0296'], + [/\\textturnk /g, '\u029E'], + [/\\Elzdyogh /g, '\u02A4'], + [/\\Elztesh /g, '\u02A7'], + [/\\textasciicaron /g, '\u02C7'], + [/\\Elzverts /g, '\u02C8'], + [/\\Elzverti /g, '\u02CC'], + [/\\Elzlmrk /g, '\u02D0'], + [/\\Elzhlmrk /g, '\u02D1'], + [/\\Elzsbrhr /g, '\u02D2'], + [/\\Elzsblhr /g, '\u02D3'], + [/\\Elzrais /g, '\u02D4'], + [/\\Elzlow /g, '\u02D5'], + [/\\textasciibreve /g, '\u02D8'], + [/\\textperiodcentered /g, '\u02D9'], + [/\\r\{\}/g, '\u02DA'], + [/\\k\{\}/g, '\u02DB'], + [/\\texttildelow /g, '\u02DC'], + [/\\H\{\}/g, '\u02DD'], + [/\\tone\{55\}/g, '\u02E5'], + [/\\tone\{44\}/g, '\u02E6'], + [/\\tone\{33\}/g, '\u02E7'], + [/\\tone\{22\}/g, '\u02E8'], + [/\\tone\{11\}/g, '\u02E9'], + [/\\cyrchar\\C/g, '\u030F'], + [/\\Elzpalh /g, '\u0321'], + [/\\Elzrh /g, '\u0322'], + [/\\Elzsbbrg /g, '\u032A'], + [/\\Elzxl /g, '\u0335'], + [/\\Elzbar /g, '\u0336'], + [/\\'\{A\}/g, '\u0386'], + [/\\'\{E\}/g, '\u0388'], + [/\\'\{H\}/g, '\u0389'], + [/\\'\{\}\{I\}/g, '\u038A'], + [/\\'\{\}O/g, '\u038C'], + [/\\mathrm\{'Y\}/g, '\u038E'], + [/\\mathrm\{'\\Omega\}/g, '\u038F'], + [/\\acute\{\\ddot\{\\iota\}\}/g, '\u0390'], + [/\\Alpha /g, '\u0391'], + [/\\Beta /g, '\u0392'], + [/\\Gamma /g, '\u0393'], + [/\\Delta /g, '\u0394'], + [/\\Epsilon /g, '\u0395'], + [/\\Zeta /g, '\u0396'], + [/\\Eta /g, '\u0397'], + [/\\Theta /g, '\u0398'], + [/\\Iota /g, '\u0399'], + [/\\Kappa /g, '\u039A'], + [/\\Lambda /g, '\u039B'], + [/\\Xi /g, '\u039E'], + [/\\Pi /g, '\u03A0'], + [/\\Rho /g, '\u03A1'], + [/\\Sigma /g, '\u03A3'], + [/\\Tau /g, '\u03A4'], + [/\\Upsilon /g, '\u03A5'], + [/\\Phi /g, '\u03A6'], + [/\\Chi /g, '\u03A7'], + [/\\Psi /g, '\u03A8'], + [/\\Omega /g, '\u03A9'], + [/\\mathrm\{\\ddot\{I\}\}/g, '\u03AA'], + [/\\mathrm\{\\ddot\{Y\}\}/g, '\u03AB'], + [/\\'\{\$\\alpha\$\}/g, '\u03AC'], + [/\\acute\{\\epsilon\}/g, '\u03AD'], + [/\\acute\{\\eta\}/g, '\u03AE'], + [/\\acute\{\\iota\}/g, '\u03AF'], + [/\\acute\{\\ddot\{\\upsilon\}\}/g, '\u03B0'], + [/\\alpha /g, '\u03B1'], + [/\\beta /g, '\u03B2'], + [/\\gamma /g, '\u03B3'], + [/\\delta /g, '\u03B4'], + [/\\epsilon /g, '\u03B5'], + [/\\zeta /g, '\u03B6'], + [/\\eta /g, '\u03B7'], + [/\\texttheta /g, '\u03B8'], + [/\\iota /g, '\u03B9'], + [/\\kappa /g, '\u03BA'], + [/\\lambda /g, '\u03BB'], + [/\\mu /g, '\u03BC'], + [/\\nu /g, '\u03BD'], + [/\\xi /g, '\u03BE'], + [/\\pi /g, '\u03C0'], + [/\\rho /g, '\u03C1'], + [/\\varsigma /g, '\u03C2'], + [/\\sigma /g, '\u03C3'], + [/\\tau /g, '\u03C4'], + [/\\upsilon /g, '\u03C5'], + [/\\varphi /g, '\u03C6'], + [/\\chi /g, '\u03C7'], + [/\\psi /g, '\u03C8'], + [/\\omega /g, '\u03C9'], + [/\\ddot\{\\iota\}/g, '\u03CA'], + [/\\ddot\{\\upsilon\}/g, '\u03CB'], + [/\\'\{o\}/g, '\u03CC'], + [/\\acute\{\\upsilon\}/g, '\u03CD'], + [/\\acute\{\\omega\}/g, '\u03CE'], + [/\\Pisymbol\{ppi022\}\{87\}/g, '\u03D0'], + [/\\textvartheta /g, '\u03D1'], + [/\\Upsilon /g, '\u03D2'], + [/\\phi /g, '\u03D5'], + [/\\varpi /g, '\u03D6'], + [/\\Stigma /g, '\u03DA'], + [/\\Digamma /g, '\u03DC'], + [/\\digamma /g, '\u03DD'], + [/\\Koppa /g, '\u03DE'], + [/\\Sampi /g, '\u03E0'], + [/\\varkappa /g, '\u03F0'], + [/\\varrho /g, '\u03F1'], + [/\\textTheta /g, '\u03F4'], + [/\\backepsilon /g, '\u03F6'], + [/\\cyrchar\\CYRYO /g, '\u0401'], + [/\\cyrchar\\CYRDJE /g, '\u0402'], + [/\\cyrchar\{\\'\\CYRG\}/g, '\u0403'], + [/\\cyrchar\\CYRIE /g, '\u0404'], + [/\\cyrchar\\CYRDZE /g, '\u0405'], + [/\\cyrchar\\CYRII /g, '\u0406'], + [/\\cyrchar\\CYRYI /g, '\u0407'], + [/\\cyrchar\\CYRJE /g, '\u0408'], + [/\\cyrchar\\CYRLJE /g, '\u0409'], + [/\\cyrchar\\CYRNJE /g, '\u040A'], + [/\\cyrchar\\CYRTSHE /g, '\u040B'], + [/\\cyrchar\{\\'\\CYRK\}/g, '\u040C'], + [/\\cyrchar\\CYRUSHRT /g, '\u040E'], + [/\\cyrchar\\CYRDZHE /g, '\u040F'], + [/\\cyrchar\\CYRA /g, '\u0410'], + [/\\cyrchar\\CYRB /g, '\u0411'], + [/\\cyrchar\\CYRV /g, '\u0412'], + [/\\cyrchar\\CYRG /g, '\u0413'], + [/\\cyrchar\\CYRD /g, '\u0414'], + [/\\cyrchar\\CYRE /g, '\u0415'], + [/\\cyrchar\\CYRZH /g, '\u0416'], + [/\\cyrchar\\CYRZ /g, '\u0417'], + [/\\cyrchar\\CYRI /g, '\u0418'], + [/\\cyrchar\\CYRISHRT /g, '\u0419'], + [/\\cyrchar\\CYRK /g, '\u041A'], + [/\\cyrchar\\CYRL /g, '\u041B'], + [/\\cyrchar\\CYRM /g, '\u041C'], + [/\\cyrchar\\CYRN /g, '\u041D'], + [/\\cyrchar\\CYRO /g, '\u041E'], + [/\\cyrchar\\CYRP /g, '\u041F'], + [/\\cyrchar\\CYRR /g, '\u0420'], + [/\\cyrchar\\CYRS /g, '\u0421'], + [/\\cyrchar\\CYRT /g, '\u0422'], + [/\\cyrchar\\CYRU /g, '\u0423'], + [/\\cyrchar\\CYRF /g, '\u0424'], + [/\\cyrchar\\CYRH /g, '\u0425'], + [/\\cyrchar\\CYRC /g, '\u0426'], + [/\\cyrchar\\CYRCH /g, '\u0427'], + [/\\cyrchar\\CYRSH /g, '\u0428'], + [/\\cyrchar\\CYRSHCH /g, '\u0429'], + [/\\cyrchar\\CYRHRDSN /g, '\u042A'], + [/\\cyrchar\\CYRERY /g, '\u042B'], + [/\\cyrchar\\CYRSFTSN /g, '\u042C'], + [/\\cyrchar\\CYREREV /g, '\u042D'], + [/\\cyrchar\\CYRYU /g, '\u042E'], + [/\\cyrchar\\CYRYA /g, '\u042F'], + [/\\cyrchar\\cyra /g, '\u0430'], + [/\\cyrchar\\cyrb /g, '\u0431'], + [/\\cyrchar\\cyrv /g, '\u0432'], + [/\\cyrchar\\cyrg /g, '\u0433'], + [/\\cyrchar\\cyrd /g, '\u0434'], + [/\\cyrchar\\cyre /g, '\u0435'], + [/\\cyrchar\\cyrzh /g, '\u0436'], + [/\\cyrchar\\cyrz /g, '\u0437'], + [/\\cyrchar\\cyri /g, '\u0438'], + [/\\cyrchar\\cyrishrt /g, '\u0439'], + [/\\cyrchar\\cyrk /g, '\u043A'], + [/\\cyrchar\\cyrl /g, '\u043B'], + [/\\cyrchar\\cyrm /g, '\u043C'], + [/\\cyrchar\\cyrn /g, '\u043D'], + [/\\cyrchar\\cyro /g, '\u043E'], + [/\\cyrchar\\cyrp /g, '\u043F'], + [/\\cyrchar\\cyrr /g, '\u0440'], + [/\\cyrchar\\cyrs /g, '\u0441'], + [/\\cyrchar\\cyrt /g, '\u0442'], + [/\\cyrchar\\cyru /g, '\u0443'], + [/\\cyrchar\\cyrf /g, '\u0444'], + [/\\cyrchar\\cyrh /g, '\u0445'], + [/\\cyrchar\\cyrc /g, '\u0446'], + [/\\cyrchar\\cyrch /g, '\u0447'], + [/\\cyrchar\\cyrsh /g, '\u0448'], + [/\\cyrchar\\cyrshch /g, '\u0449'], + [/\\cyrchar\\cyrhrdsn /g, '\u044A'], + [/\\cyrchar\\cyrery /g, '\u044B'], + [/\\cyrchar\\cyrsftsn /g, '\u044C'], + [/\\cyrchar\\cyrerev /g, '\u044D'], + [/\\cyrchar\\cyryu /g, '\u044E'], + [/\\cyrchar\\cyrya /g, '\u044F'], + [/\\cyrchar\\cyryo /g, '\u0451'], + [/\\cyrchar\\cyrdje /g, '\u0452'], + [/\\cyrchar\{\\'\\cyrg\}/g, '\u0453'], + [/\\cyrchar\\cyrie /g, '\u0454'], + [/\\cyrchar\\cyrdze /g, '\u0455'], + [/\\cyrchar\\cyrii /g, '\u0456'], + [/\\cyrchar\\cyryi /g, '\u0457'], + [/\\cyrchar\\cyrje /g, '\u0458'], + [/\\cyrchar\\cyrlje /g, '\u0459'], + [/\\cyrchar\\cyrnje /g, '\u045A'], + [/\\cyrchar\\cyrtshe /g, '\u045B'], + [/\\cyrchar\{\\'\\cyrk\}/g, '\u045C'], + [/\\cyrchar\\cyrushrt /g, '\u045E'], + [/\\cyrchar\\cyrdzhe /g, '\u045F'], + [/\\cyrchar\\CYROMEGA /g, '\u0460'], + [/\\cyrchar\\cyromega /g, '\u0461'], + [/\\cyrchar\\CYRYAT /g, '\u0462'], + [/\\cyrchar\\CYRIOTE /g, '\u0464'], + [/\\cyrchar\\cyriote /g, '\u0465'], + [/\\cyrchar\\CYRLYUS /g, '\u0466'], + [/\\cyrchar\\cyrlyus /g, '\u0467'], + [/\\cyrchar\\CYRIOTLYUS /g, '\u0468'], + [/\\cyrchar\\cyriotlyus /g, '\u0469'], + [/\\cyrchar\\CYRBYUS /g, '\u046A'], + [/\\cyrchar\\CYRIOTBYUS /g, '\u046C'], + [/\\cyrchar\\cyriotbyus /g, '\u046D'], + [/\\cyrchar\\CYRKSI /g, '\u046E'], + [/\\cyrchar\\cyrksi /g, '\u046F'], + [/\\cyrchar\\CYRPSI /g, '\u0470'], + [/\\cyrchar\\cyrpsi /g, '\u0471'], + [/\\cyrchar\\CYRFITA /g, '\u0472'], + [/\\cyrchar\\CYRIZH /g, '\u0474'], + [/\\cyrchar\\CYRUK /g, '\u0478'], + [/\\cyrchar\\cyruk /g, '\u0479'], + [/\\cyrchar\\CYROMEGARND /g, '\u047A'], + [/\\cyrchar\\cyromegarnd /g, '\u047B'], + [/\\cyrchar\\CYROMEGATITLO /g, '\u047C'], + [/\\cyrchar\\cyromegatitlo /g, '\u047D'], + [/\\cyrchar\\CYROT /g, '\u047E'], + [/\\cyrchar\\cyrot /g, '\u047F'], + [/\\cyrchar\\CYRKOPPA /g, '\u0480'], + [/\\cyrchar\\cyrkoppa /g, '\u0481'], + [/\\cyrchar\\cyrthousands /g, '\u0482'], + [/\\cyrchar\\cyrhundredthousands /g, '\u0488'], + [/\\cyrchar\\cyrmillions /g, '\u0489'], + [/\\cyrchar\\CYRSEMISFTSN /g, '\u048C'], + [/\\cyrchar\\cyrsemisftsn /g, '\u048D'], + [/\\cyrchar\\CYRRTICK /g, '\u048E'], + [/\\cyrchar\\cyrrtick /g, '\u048F'], + [/\\cyrchar\\CYRGUP /g, '\u0490'], + [/\\cyrchar\\cyrgup /g, '\u0491'], + [/\\cyrchar\\CYRGHCRS /g, '\u0492'], + [/\\cyrchar\\cyrghcrs /g, '\u0493'], + [/\\cyrchar\\CYRGHK /g, '\u0494'], + [/\\cyrchar\\cyrghk /g, '\u0495'], + [/\\cyrchar\\CYRZHDSC /g, '\u0496'], + [/\\cyrchar\\cyrzhdsc /g, '\u0497'], + [/\\cyrchar\\CYRZDSC /g, '\u0498'], + [/\\cyrchar\\cyrzdsc /g, '\u0499'], + [/\\cyrchar\\CYRKDSC /g, '\u049A'], + [/\\cyrchar\\cyrkdsc /g, '\u049B'], + [/\\cyrchar\\CYRKVCRS /g, '\u049C'], + [/\\cyrchar\\cyrkvcrs /g, '\u049D'], + [/\\cyrchar\\CYRKHCRS /g, '\u049E'], + [/\\cyrchar\\cyrkhcrs /g, '\u049F'], + [/\\cyrchar\\CYRKBEAK /g, '\u04A0'], + [/\\cyrchar\\cyrkbeak /g, '\u04A1'], + [/\\cyrchar\\CYRNDSC /g, '\u04A2'], + [/\\cyrchar\\cyrndsc /g, '\u04A3'], + [/\\cyrchar\\CYRNG /g, '\u04A4'], + [/\\cyrchar\\cyrng /g, '\u04A5'], + [/\\cyrchar\\CYRPHK /g, '\u04A6'], + [/\\cyrchar\\cyrphk /g, '\u04A7'], + [/\\cyrchar\\CYRABHHA /g, '\u04A8'], + [/\\cyrchar\\cyrabhha /g, '\u04A9'], + [/\\cyrchar\\CYRSDSC /g, '\u04AA'], + [/\\cyrchar\\cyrsdsc /g, '\u04AB'], + [/\\cyrchar\\CYRTDSC /g, '\u04AC'], + [/\\cyrchar\\cyrtdsc /g, '\u04AD'], + [/\\cyrchar\\CYRY /g, '\u04AE'], + [/\\cyrchar\\cyry /g, '\u04AF'], + [/\\cyrchar\\CYRYHCRS /g, '\u04B0'], + [/\\cyrchar\\cyryhcrs /g, '\u04B1'], + [/\\cyrchar\\CYRHDSC /g, '\u04B2'], + [/\\cyrchar\\cyrhdsc /g, '\u04B3'], + [/\\cyrchar\\CYRTETSE /g, '\u04B4'], + [/\\cyrchar\\cyrtetse /g, '\u04B5'], + [/\\cyrchar\\CYRCHRDSC /g, '\u04B6'], + [/\\cyrchar\\cyrchrdsc /g, '\u04B7'], + [/\\cyrchar\\CYRCHVCRS /g, '\u04B8'], + [/\\cyrchar\\cyrchvcrs /g, '\u04B9'], + [/\\cyrchar\\CYRSHHA /g, '\u04BA'], + [/\\cyrchar\\cyrshha /g, '\u04BB'], + [/\\cyrchar\\CYRABHCH /g, '\u04BC'], + [/\\cyrchar\\cyrabhch /g, '\u04BD'], + [/\\cyrchar\\CYRABHCHDSC /g, '\u04BE'], + [/\\cyrchar\\cyrabhchdsc /g, '\u04BF'], + [/\\cyrchar\\CYRpalochka /g, '\u04C0'], + [/\\cyrchar\\CYRKHK /g, '\u04C3'], + [/\\cyrchar\\cyrkhk /g, '\u04C4'], + [/\\cyrchar\\CYRNHK /g, '\u04C7'], + [/\\cyrchar\\cyrnhk /g, '\u04C8'], + [/\\cyrchar\\CYRCHLDSC /g, '\u04CB'], + [/\\cyrchar\\cyrchldsc /g, '\u04CC'], + [/\\cyrchar\\CYRAE /g, '\u04D4'], + [/\\cyrchar\\cyrae /g, '\u04D5'], + [/\\cyrchar\\CYRSCHWA /g, '\u04D8'], + [/\\cyrchar\\cyrschwa /g, '\u04D9'], + [/\\cyrchar\\CYRABHDZE /g, '\u04E0'], + [/\\cyrchar\\cyrabhdze /g, '\u04E1'], + [/\\cyrchar\\CYROTLD /g, '\u04E8'], + [/\\cyrchar\\cyrotld /g, '\u04E9'], + [/\\hspace\{0.6em\}/g, '\u2002'], + [/\\hspace\{1em\}/g, '\u2003'], + [/\\hspace\{0.33em\}/g, '\u2004'], + [/\\hspace\{0.25em\}/g, '\u2005'], + [/\\hspace\{0.166em\}/g, '\u2006'], + [/\\hphantom\{0\}/g, '\u2007'], + [/\\hphantom\{,\}/g, '\u2008'], + [/\\hspace\{0.167em\}/g, '\u2009'], + [/\\mkern1mu /g, '\u200A'], + [/\\textendash /g, '\u2013'], + [/\\textemdash /g, '\u2014'], + [/\\rule\{1em\}\{1pt\}/g, '\u2015'], + [/\\Vert /g, '\u2016'], + [/\\Elzreapos /g, '\u201B'], + [/\\textquotedblleft /g, '\u201C'], + [/\\textquotedblright /g, '\u201D'], + [/\\textdagger /g, '\u2020'], + [/\\textdaggerdbl /g, '\u2021'], + [/\\textbullet /g, '\u2022'], + [/\\ldots /g, '\u2026'], + [/\\textperthousand /g, '\u2030'], + [/\\textpertenthousand /g, '\u2031'], + [/\\backprime /g, '\u2035'], + [/\\guilsinglleft /g, '\u2039'], + [/\\guilsinglright /g, '\u203A'], + [/\\mkern4mu /g, '\u205F'], + [/\\nolinebreak /g, '\u2060'], + [/\\ensuremath\{\\Elzpes\}/g, '\u20A7'], + [/\\mbox\{\\texteuro\} /g, '\u20AC'], + [/\\dddot /g, '\u20DB'], + [/\\ddddot /g, '\u20DC'], + [/\\mathbb\{C\}/g, '\u2102'], + [/\\mathscr\{g\}/g, '\u210A'], + [/\\mathscr\{H\}/g, '\u210B'], + [/\\mathfrak\{H\}/g, '\u210C'], + [/\\mathbb\{H\}/g, '\u210D'], + [/\\hslash /g, '\u210F'], + [/\\mathscr\{I\}/g, '\u2110'], + [/\\mathfrak\{I\}/g, '\u2111'], + [/\\mathscr\{L\}/g, '\u2112'], + [/\\mathscr\{l\}/g, '\u2113'], + [/\\mathbb\{N\}/g, '\u2115'], + [/\\cyrchar\\textnumero /g, '\u2116'], + [/\\wp /g, '\u2118'], + [/\\mathbb\{P\}/g, '\u2119'], + [/\\mathbb\{Q\}/g, '\u211A'], + [/\\mathscr\{R\}/g, '\u211B'], + [/\\mathfrak\{R\}/g, '\u211C'], + [/\\mathbb\{R\}/g, '\u211D'], + [/\\Elzxrat /g, '\u211E'], + [/\\texttrademark /g, '\u2122'], + [/\\mathbb\{Z\}/g, '\u2124'], + [/\\Omega /g, '\u2126'], + [/\\mho /g, '\u2127'], + [/\\mathfrak\{Z\}/g, '\u2128'], + [/\\ElsevierGlyph\{2129\}/g, '\u2129'], + [/\\AA /g, '\u212B'], + [/\\mathscr\{B\}/g, '\u212C'], + [/\\mathfrak\{C\}/g, '\u212D'], + [/\\mathscr\{e\}/g, '\u212F'], + [/\\mathscr\{E\}/g, '\u2130'], + [/\\mathscr\{F\}/g, '\u2131'], + [/\\mathscr\{M\}/g, '\u2133'], + [/\\mathscr\{o\}/g, '\u2134'], + [/\\aleph /g, '\u2135'], + [/\\beth /g, '\u2136'], + [/\\gimel /g, '\u2137'], + [/\\daleth /g, '\u2138'], + [/\\textfrac\{1\}\{3\}/g, '\u2153'], + [/\\textfrac\{2\}\{3\}/g, '\u2154'], + [/\\textfrac\{1\}\{5\}/g, '\u2155'], + [/\\textfrac\{2\}\{5\}/g, '\u2156'], + [/\\textfrac\{3\}\{5\}/g, '\u2157'], + [/\\textfrac\{4\}\{5\}/g, '\u2158'], + [/\\textfrac\{1\}\{6\}/g, '\u2159'], + [/\\textfrac\{5\}\{6\}/g, '\u215A'], + [/\\textfrac\{1\}\{8\}/g, '\u215B'], + [/\\textfrac\{3\}\{8\}/g, '\u215C'], + [/\\textfrac\{5\}\{8\}/g, '\u215D'], + [/\\textfrac\{7\}\{8\}/g, '\u215E'], + [/\\leftarrow /g, '\u2190'], + [/\\uparrow /g, '\u2191'], + [/\\rightarrow /g, '\u2192'], + [/\\downarrow /g, '\u2193'], + [/\\leftrightarrow /g, '\u2194'], + [/\\updownarrow /g, '\u2195'], + [/\\nwarrow /g, '\u2196'], + [/\\nearrow /g, '\u2197'], + [/\\searrow /g, '\u2198'], + [/\\swarrow /g, '\u2199'], + [/\\nleftarrow /g, '\u219A'], + [/\\nrightarrow /g, '\u219B'], + [/\\arrowwaveright /g, '\u219C'], + [/\\arrowwaveright /g, '\u219D'], + [/\\twoheadleftarrow /g, '\u219E'], + [/\\twoheadrightarrow /g, '\u21A0'], + [/\\leftarrowtail /g, '\u21A2'], + [/\\rightarrowtail /g, '\u21A3'], + [/\\mapsto /g, '\u21A6'], + [/\\hookleftarrow /g, '\u21A9'], + [/\\hookrightarrow /g, '\u21AA'], + [/\\looparrowleft /g, '\u21AB'], + [/\\looparrowright /g, '\u21AC'], + [/\\leftrightsquigarrow /g, '\u21AD'], + [/\\nleftrightarrow /g, '\u21AE'], + [/\\Lsh /g, '\u21B0'], + [/\\Rsh /g, '\u21B1'], + [/\\ElsevierGlyph\{21B3\}/g, '\u21B3'], + [/\\curvearrowleft /g, '\u21B6'], + [/\\curvearrowright /g, '\u21B7'], + [/\\circlearrowleft /g, '\u21BA'], + [/\\circlearrowright /g, '\u21BB'], + [/\\leftharpoonup /g, '\u21BC'], + [/\\leftharpoondown /g, '\u21BD'], + [/\\upharpoonright /g, '\u21BE'], + [/\\upharpoonleft /g, '\u21BF'], + [/\\rightharpoonup /g, '\u21C0'], + [/\\rightharpoondown /g, '\u21C1'], + [/\\downharpoonright /g, '\u21C2'], + [/\\downharpoonleft /g, '\u21C3'], + [/\\rightleftarrows /g, '\u21C4'], + [/\\dblarrowupdown /g, '\u21C5'], + [/\\leftrightarrows /g, '\u21C6'], + [/\\leftleftarrows /g, '\u21C7'], + [/\\upuparrows /g, '\u21C8'], + [/\\rightrightarrows /g, '\u21C9'], + [/\\downdownarrows /g, '\u21CA'], + [/\\leftrightharpoons /g, '\u21CB'], + [/\\rightleftharpoons /g, '\u21CC'], + [/\\nLeftarrow /g, '\u21CD'], + [/\\nLeftrightarrow /g, '\u21CE'], + [/\\nRightarrow /g, '\u21CF'], + [/\\Leftarrow /g, '\u21D0'], + [/\\Uparrow /g, '\u21D1'], + [/\\Rightarrow /g, '\u21D2'], + [/\\Downarrow /g, '\u21D3'], + [/\\Leftrightarrow /g, '\u21D4'], + [/\\Updownarrow /g, '\u21D5'], + [/\\Lleftarrow /g, '\u21DA'], + [/\\Rrightarrow /g, '\u21DB'], + [/\\rightsquigarrow /g, '\u21DD'], + [/\\DownArrowUpArrow /g, '\u21F5'], + [/\\forall /g, '\u2200'], + [/\\complement /g, '\u2201'], + [/\\partial /g, '\u2202'], + [/\\exists /g, '\u2203'], + [/\\nexists /g, '\u2204'], + [/\\varnothing /g, '\u2205'], + [/\\nabla /g, '\u2207'], + [/\\in /g, '\u2208'], + [/\\not\\in /g, '\u2209'], + [/\\ni /g, '\u220B'], + [/\\not\\ni /g, '\u220C'], + [/\\prod /g, '\u220F'], + [/\\coprod /g, '\u2210'], + [/\\sum /g, '\u2211'], + [/\\mp /g, '\u2213'], + [/\\dotplus /g, '\u2214'], + [/\\setminus /g, '\u2216'], + [/\\circ /g, '\u2218'], + [/\\bullet /g, '\u2219'], + [/\\surd /g, '\u221A'], + [/\\propto /g, '\u221D'], + [/\\infty /g, '\u221E'], + [/\\rightangle /g, '\u221F'], + [/\\angle /g, '\u2220'], + [/\\measuredangle /g, '\u2221'], + [/\\sphericalangle /g, '\u2222'], + [/\\mid /g, '\u2223'], + [/\\nmid /g, '\u2224'], + [/\\parallel /g, '\u2225'], + [/\\nparallel /g, '\u2226'], + [/\\wedge /g, '\u2227'], + [/\\vee /g, '\u2228'], + [/\\cap /g, '\u2229'], + [/\\cup /g, '\u222A'], + [/\\int /g, '\u222B'], + [/\\int\\!\\int /g, '\u222C'], + [/\\int\\!\\int\\!\\int /g, '\u222D'], + [/\\oint /g, '\u222E'], + [/\\surfintegral /g, '\u222F'], + [/\\volintegral /g, '\u2230'], + [/\\clwintegral /g, '\u2231'], + [/\\ElsevierGlyph\{2232\}/g, '\u2232'], + [/\\ElsevierGlyph\{2233\}/g, '\u2233'], + [/\\therefore /g, '\u2234'], + [/\\because /g, '\u2235'], + [/\\Colon /g, '\u2237'], + [/\\ElsevierGlyph\{2238\}/g, '\u2238'], + [/\\mathbin\{\{:\}\\!\\!\{\-\}\\!\\!\{:\}\}/g, '\u223A'], + [/\\homothetic /g, '\u223B'], + [/\\sim /g, '\u223C'], + [/\\backsim /g, '\u223D'], + [/\\lazysinv /g, '\u223E'], + [/\\wr /g, '\u2240'], + [/\\not\\sim /g, '\u2241'], + [/\\ElsevierGlyph\{2242\}/g, '\u2242'], + [/\\NotEqualTilde /g, '\u2242-00338'], + [/\\simeq /g, '\u2243'], + [/\\not\\simeq /g, '\u2244'], + [/\\cong /g, '\u2245'], + [/\\approxnotequal /g, '\u2246'], + [/\\not\\cong /g, '\u2247'], + [/\\approx /g, '\u2248'], + [/\\not\\approx /g, '\u2249'], + [/\\approxeq /g, '\u224A'], + [/\\tildetrpl /g, '\u224B'], + [/\\not\\apid /g, '\u224B-00338'], + [/\\allequal /g, '\u224C'], + [/\\asymp /g, '\u224D'], + [/\\Bumpeq /g, '\u224E'], + [/\\NotHumpDownHump /g, '\u224E-00338'], + [/\\bumpeq /g, '\u224F'], + [/\\NotHumpEqual /g, '\u224F-00338'], + [/\\doteq /g, '\u2250'], + [/\\not\\doteq/g, '\u2250-00338'], + [/\\doteqdot /g, '\u2251'], + [/\\fallingdotseq /g, '\u2252'], + [/\\risingdotseq /g, '\u2253'], + [/\\eqcirc /g, '\u2256'], + [/\\circeq /g, '\u2257'], + [/\\estimates /g, '\u2259'], + [/\\ElsevierGlyph\{225A\}/g, '\u225A'], + [/\\starequal /g, '\u225B'], + [/\\triangleq /g, '\u225C'], + [/\\ElsevierGlyph\{225F\}/g, '\u225F'], + [/\\not =/g, '\u2260'], + [/\\equiv /g, '\u2261'], + [/\\not\\equiv /g, '\u2262'], + [/\\leq /g, '\u2264'], + [/\\geq /g, '\u2265'], + [/\\leqq /g, '\u2266'], + [/\\geqq /g, '\u2267'], + [/\\lneqq /g, '\u2268'], + [/\\lvertneqq /g, '\u2268-0FE00'], + [/\\gneqq /g, '\u2269'], + [/\\gvertneqq /g, '\u2269-0FE00'], + [/\\ll /g, '\u226A'], + [/\\NotLessLess /g, '\u226A-00338'], + [/\\gg /g, '\u226B'], + [/\\NotGreaterGreater /g, '\u226B-00338'], + [/\\between /g, '\u226C'], + [/\\not\\kern\-0.3em\\times /g, '\u226D'], + [/\\not/g, '\u226F'], + [/\\not\\leq /g, '\u2270'], + [/\\not\\geq /g, '\u2271'], + [/\\lessequivlnt /g, '\u2272'], + [/\\greaterequivlnt /g, '\u2273'], + [/\\ElsevierGlyph\{2274\}/g, '\u2274'], + [/\\ElsevierGlyph\{2275\}/g, '\u2275'], + [/\\lessgtr /g, '\u2276'], + [/\\gtrless /g, '\u2277'], + [/\\notlessgreater /g, '\u2278'], + [/\\notgreaterless /g, '\u2279'], + [/\\prec /g, '\u227A'], + [/\\succ /g, '\u227B'], + [/\\preccurlyeq /g, '\u227C'], + [/\\succcurlyeq /g, '\u227D'], + [/\\precapprox /g, '\u227E'], + [/\\NotPrecedesTilde /g, '\u227E-00338'], + [/\\succapprox /g, '\u227F'], + [/\\NotSucceedsTilde /g, '\u227F-00338'], + [/\\not\\prec /g, '\u2280'], + [/\\not\\succ /g, '\u2281'], + [/\\subset /g, '\u2282'], + [/\\supset /g, '\u2283'], + [/\\not\\subset /g, '\u2284'], + [/\\not\\supset /g, '\u2285'], + [/\\subseteq /g, '\u2286'], + [/\\supseteq /g, '\u2287'], + [/\\not\\subseteq /g, '\u2288'], + [/\\not\\supseteq /g, '\u2289'], + [/\\subsetneq /g, '\u228A'], + [/\\varsubsetneqq /g, '\u228A-0FE00'], + [/\\supsetneq /g, '\u228B'], + [/\\varsupsetneq /g, '\u228B-0FE00'], + [/\\uplus /g, '\u228E'], + [/\\sqsubset /g, '\u228F'], + [/\\NotSquareSubset /g, '\u228F-00338'], + [/\\sqsupset /g, '\u2290'], + [/\\NotSquareSuperset /g, '\u2290-00338'], + [/\\sqsubseteq /g, '\u2291'], + [/\\sqsupseteq /g, '\u2292'], + [/\\sqcap /g, '\u2293'], + [/\\sqcup /g, '\u2294'], + [/\\oplus /g, '\u2295'], + [/\\ominus /g, '\u2296'], + [/\\otimes /g, '\u2297'], + [/\\oslash /g, '\u2298'], + [/\\odot /g, '\u2299'], + [/\\circledcirc /g, '\u229A'], + [/\\circledast /g, '\u229B'], + [/\\circleddash /g, '\u229D'], + [/\\boxplus /g, '\u229E'], + [/\\boxminus /g, '\u229F'], + [/\\boxtimes /g, '\u22A0'], + [/\\boxdot /g, '\u22A1'], + [/\\vdash /g, '\u22A2'], + [/\\dashv /g, '\u22A3'], + [/\\top /g, '\u22A4'], + [/\\perp /g, '\u22A5'], + [/\\truestate /g, '\u22A7'], + [/\\forcesextra /g, '\u22A8'], + [/\\Vdash /g, '\u22A9'], + [/\\Vvdash /g, '\u22AA'], + [/\\VDash /g, '\u22AB'], + [/\\nvdash /g, '\u22AC'], + [/\\nvDash /g, '\u22AD'], + [/\\nVdash /g, '\u22AE'], + [/\\nVDash /g, '\u22AF'], + [/\\vartriangleleft /g, '\u22B2'], + [/\\vartriangleright /g, '\u22B3'], + [/\\trianglelefteq /g, '\u22B4'], + [/\\trianglerighteq /g, '\u22B5'], + [/\\original /g, '\u22B6'], + [/\\image /g, '\u22B7'], + [/\\multimap /g, '\u22B8'], + [/\\hermitconjmatrix /g, '\u22B9'], + [/\\intercal /g, '\u22BA'], + [/\\veebar /g, '\u22BB'], + [/\\rightanglearc /g, '\u22BE'], + [/\\ElsevierGlyph\{22C0\}/g, '\u22C0'], + [/\\ElsevierGlyph\{22C1\}/g, '\u22C1'], + [/\\bigcap /g, '\u22C2'], + [/\\bigcup /g, '\u22C3'], + [/\\diamond /g, '\u22C4'], + [/\\cdot /g, '\u22C5'], + [/\\star /g, '\u22C6'], + [/\\divideontimes /g, '\u22C7'], + [/\\bowtie /g, '\u22C8'], + [/\\ltimes /g, '\u22C9'], + [/\\rtimes /g, '\u22CA'], + [/\\leftthreetimes /g, '\u22CB'], + [/\\rightthreetimes /g, '\u22CC'], + [/\\backsimeq /g, '\u22CD'], + [/\\curlyvee /g, '\u22CE'], + [/\\curlywedge /g, '\u22CF'], + [/\\Subset /g, '\u22D0'], + [/\\Supset /g, '\u22D1'], + [/\\Cap /g, '\u22D2'], + [/\\Cup /g, '\u22D3'], + [/\\pitchfork /g, '\u22D4'], + [/\\lessdot /g, '\u22D6'], + [/\\gtrdot /g, '\u22D7'], + [/\\verymuchless /g, '\u22D8'], + [/\\verymuchgreater /g, '\u22D9'], + [/\\lesseqgtr /g, '\u22DA'], + [/\\gtreqless /g, '\u22DB'], + [/\\curlyeqprec /g, '\u22DE'], + [/\\curlyeqsucc /g, '\u22DF'], + [/\\not\\sqsubseteq /g, '\u22E2'], + [/\\not\\sqsupseteq /g, '\u22E3'], + [/\\Elzsqspne /g, '\u22E5'], + [/\\lnsim /g, '\u22E6'], + [/\\gnsim /g, '\u22E7'], + [/\\precedesnotsimilar /g, '\u22E8'], + [/\\succnsim /g, '\u22E9'], + [/\\ntriangleleft /g, '\u22EA'], + [/\\ntriangleright /g, '\u22EB'], + [/\\ntrianglelefteq /g, '\u22EC'], + [/\\ntrianglerighteq /g, '\u22ED'], + [/\\vdots /g, '\u22EE'], + [/\\cdots /g, '\u22EF'], + [/\\upslopeellipsis /g, '\u22F0'], + [/\\downslopeellipsis /g, '\u22F1'], + [/\\barwedge /g, '\u2305'], + [/\\perspcorrespond /g, '\u2306'], + [/\\lceil /g, '\u2308'], + [/\\rceil /g, '\u2309'], + [/\\lfloor /g, '\u230A'], + [/\\rfloor /g, '\u230B'], + [/\\recorder /g, '\u2315'], + [/\\mathchar"2208/g, '\u2316'], + [/\\ulcorner /g, '\u231C'], + [/\\urcorner /g, '\u231D'], + [/\\llcorner /g, '\u231E'], + [/\\lrcorner /g, '\u231F'], + [/\\frown /g, '\u2322'], + [/\\smile /g, '\u2323'], + [/\\langle /g, '\u2329'], + [/\\rangle /g, '\u232A'], + [/\\ElsevierGlyph\{E838\}/g, '\u233D'], + [/\\Elzdlcorn /g, '\u23A3'], + [/\\lmoustache /g, '\u23B0'], + [/\\rmoustache /g, '\u23B1'], + [/\\textvisiblespace /g, '\u2423'], + [/\\ding\{172\}/g, '\u2460'], + [/\\ding\{173\}/g, '\u2461'], + [/\\ding\{174\}/g, '\u2462'], + [/\\ding\{175\}/g, '\u2463'], + [/\\ding\{176\}/g, '\u2464'], + [/\\ding\{177\}/g, '\u2465'], + [/\\ding\{178\}/g, '\u2466'], + [/\\ding\{179\}/g, '\u2467'], + [/\\ding\{180\}/g, '\u2468'], + [/\\ding\{181\}/g, '\u2469'], + [/\\circledS /g, '\u24C8'], + [/\\Elzdshfnc /g, '\u2506'], + [/\\Elzsqfnw /g, '\u2519'], + [/\\diagup /g, '\u2571'], + [/\\ding\{110\}/g, '\u25A0'], + [/\\square /g, '\u25A1'], + [/\\blacksquare /g, '\u25AA'], + [/\\fbox\{~~\}/g, '\u25AD'], + [/\\Elzvrecto /g, '\u25AF'], + [/\\ElsevierGlyph\{E381\}/g, '\u25B1'], + [/\\ding\{115\}/g, '\u25B2'], + [/\\bigtriangleup /g, '\u25B3'], + [/\\blacktriangle /g, '\u25B4'], + [/\\vartriangle /g, '\u25B5'], + [/\\blacktriangleright /g, '\u25B8'], + [/\\triangleright /g, '\u25B9'], + [/\\ding\{116\}/g, '\u25BC'], + [/\\bigtriangledown /g, '\u25BD'], + [/\\blacktriangledown /g, '\u25BE'], + [/\\triangledown /g, '\u25BF'], + [/\\blacktriangleleft /g, '\u25C2'], + [/\\triangleleft /g, '\u25C3'], + [/\\ding\{117\}/g, '\u25C6'], + [/\\lozenge /g, '\u25CA'], + [/\\bigcirc /g, '\u25CB'], + [/\\ding\{108\}/g, '\u25CF'], + [/\\Elzcirfl /g, '\u25D0'], + [/\\Elzcirfr /g, '\u25D1'], + [/\\Elzcirfb /g, '\u25D2'], + [/\\ding\{119\}/g, '\u25D7'], + [/\\Elzrvbull /g, '\u25D8'], + [/\\Elzsqfl /g, '\u25E7'], + [/\\Elzsqfr /g, '\u25E8'], + [/\\Elzsqfse /g, '\u25EA'], + [/\\bigcirc /g, '\u25EF'], + [/\\ding\{72\}/g, '\u2605'], + [/\\ding\{73\}/g, '\u2606'], + [/\\ding\{37\}/g, '\u260E'], + [/\\ding\{42\}/g, '\u261B'], + [/\\ding\{43\}/g, '\u261E'], + [/\\rightmoon /g, '\u263E'], + [/\\mercury /g, '\u263F'], + [/\\venus /g, '\u2640'], + [/\\male /g, '\u2642'], + [/\\jupiter /g, '\u2643'], + [/\\saturn /g, '\u2644'], + [/\\uranus /g, '\u2645'], + [/\\neptune /g, '\u2646'], + [/\\pluto /g, '\u2647'], + [/\\aries /g, '\u2648'], + [/\\taurus /g, '\u2649'], + [/\\gemini /g, '\u264A'], + [/\\cancer /g, '\u264B'], + [/\\leo /g, '\u264C'], + [/\\virgo /g, '\u264D'], + [/\\libra /g, '\u264E'], + [/\\scorpio /g, '\u264F'], + [/\\sagittarius /g, '\u2650'], + [/\\capricornus /g, '\u2651'], + [/\\aquarius /g, '\u2652'], + [/\\pisces /g, '\u2653'], + [/\\ding\{171\}/g, '\u2660'], + [/\\diamond /g, '\u2662'], + [/\\ding\{168\}/g, '\u2663'], + [/\\ding\{170\}/g, '\u2665'], + [/\\ding\{169\}/g, '\u2666'], + [/\\quarternote /g, '\u2669'], + [/\\eighthnote /g, '\u266A'], + [/\\flat /g, '\u266D'], + [/\\natural /g, '\u266E'], + [/\\sharp /g, '\u266F'], + [/\\ding\{33\}/g, '\u2701'], + [/\\ding\{34\}/g, '\u2702'], + [/\\ding\{35\}/g, '\u2703'], + [/\\ding\{36\}/g, '\u2704'], + [/\\ding\{38\}/g, '\u2706'], + [/\\ding\{39\}/g, '\u2707'], + [/\\ding\{40\}/g, '\u2708'], + [/\\ding\{41\}/g, '\u2709'], + [/\\ding\{44\}/g, '\u270C'], + [/\\ding\{45\}/g, '\u270D'], + [/\\ding\{46\}/g, '\u270E'], + [/\\ding\{47\}/g, '\u270F'], + [/\\ding\{48\}/g, '\u2710'], + [/\\ding\{49\}/g, '\u2711'], + [/\\ding\{50\}/g, '\u2712'], + [/\\ding\{51\}/g, '\u2713'], + [/\\ding\{52\}/g, '\u2714'], + [/\\ding\{53\}/g, '\u2715'], + [/\\ding\{54\}/g, '\u2716'], + [/\\ding\{55\}/g, '\u2717'], + [/\\ding\{56\}/g, '\u2718'], + [/\\ding\{57\}/g, '\u2719'], + [/\\ding\{58\}/g, '\u271A'], + [/\\ding\{59\}/g, '\u271B'], + [/\\ding\{60\}/g, '\u271C'], + [/\\ding\{61\}/g, '\u271D'], + [/\\ding\{62\}/g, '\u271E'], + [/\\ding\{63\}/g, '\u271F'], + [/\\ding\{64\}/g, '\u2720'], + [/\\ding\{65\}/g, '\u2721'], + [/\\ding\{66\}/g, '\u2722'], + [/\\ding\{67\}/g, '\u2723'], + [/\\ding\{68\}/g, '\u2724'], + [/\\ding\{69\}/g, '\u2725'], + [/\\ding\{70\}/g, '\u2726'], + [/\\ding\{71\}/g, '\u2727'], + [/\\ding\{73\}/g, '\u2729'], + [/\\ding\{74\}/g, '\u272A'], + [/\\ding\{75\}/g, '\u272B'], + [/\\ding\{76\}/g, '\u272C'], + [/\\ding\{77\}/g, '\u272D'], + [/\\ding\{78\}/g, '\u272E'], + [/\\ding\{79\}/g, '\u272F'], + [/\\ding\{80\}/g, '\u2730'], + [/\\ding\{81\}/g, '\u2731'], + [/\\ding\{82\}/g, '\u2732'], + [/\\ding\{83\}/g, '\u2733'], + [/\\ding\{84\}/g, '\u2734'], + [/\\ding\{85\}/g, '\u2735'], + [/\\ding\{86\}/g, '\u2736'], + [/\\ding\{87\}/g, '\u2737'], + [/\\ding\{88\}/g, '\u2738'], + [/\\ding\{89\}/g, '\u2739'], + [/\\ding\{90\}/g, '\u273A'], + [/\\ding\{91\}/g, '\u273B'], + [/\\ding\{92\}/g, '\u273C'], + [/\\ding\{93\}/g, '\u273D'], + [/\\ding\{94\}/g, '\u273E'], + [/\\ding\{95\}/g, '\u273F'], + [/\\ding\{96\}/g, '\u2740'], + [/\\ding\{97\}/g, '\u2741'], + [/\\ding\{98\}/g, '\u2742'], + [/\\ding\{99\}/g, '\u2743'], + [/\\ding\{100\}/g, '\u2744'], + [/\\ding\{101\}/g, '\u2745'], + [/\\ding\{102\}/g, '\u2746'], + [/\\ding\{103\}/g, '\u2747'], + [/\\ding\{104\}/g, '\u2748'], + [/\\ding\{105\}/g, '\u2749'], + [/\\ding\{106\}/g, '\u274A'], + [/\\ding\{107\}/g, '\u274B'], + [/\\ding\{109\}/g, '\u274D'], + [/\\ding\{111\}/g, '\u274F'], + [/\\ding\{112\}/g, '\u2750'], + [/\\ding\{113\}/g, '\u2751'], + [/\\ding\{114\}/g, '\u2752'], + [/\\ding\{118\}/g, '\u2756'], + [/\\ding\{120\}/g, '\u2758'], + [/\\ding\{121\}/g, '\u2759'], + [/\\ding\{122\}/g, '\u275A'], + [/\\ding\{123\}/g, '\u275B'], + [/\\ding\{124\}/g, '\u275C'], + [/\\ding\{125\}/g, '\u275D'], + [/\\ding\{126\}/g, '\u275E'], + [/\\ding\{161\}/g, '\u2761'], + [/\\ding\{162\}/g, '\u2762'], + [/\\ding\{163\}/g, '\u2763'], + [/\\ding\{164\}/g, '\u2764'], + [/\\ding\{165\}/g, '\u2765'], + [/\\ding\{166\}/g, '\u2766'], + [/\\ding\{167\}/g, '\u2767'], + [/\\ding\{182\}/g, '\u2776'], + [/\\ding\{183\}/g, '\u2777'], + [/\\ding\{184\}/g, '\u2778'], + [/\\ding\{185\}/g, '\u2779'], + [/\\ding\{186\}/g, '\u277A'], + [/\\ding\{187\}/g, '\u277B'], + [/\\ding\{188\}/g, '\u277C'], + [/\\ding\{189\}/g, '\u277D'], + [/\\ding\{190\}/g, '\u277E'], + [/\\ding\{191\}/g, '\u277F'], + [/\\ding\{192\}/g, '\u2780'], + [/\\ding\{193\}/g, '\u2781'], + [/\\ding\{194\}/g, '\u2782'], + [/\\ding\{195\}/g, '\u2783'], + [/\\ding\{196\}/g, '\u2784'], + [/\\ding\{197\}/g, '\u2785'], + [/\\ding\{198\}/g, '\u2786'], + [/\\ding\{199\}/g, '\u2787'], + [/\\ding\{200\}/g, '\u2788'], + [/\\ding\{201\}/g, '\u2789'], + [/\\ding\{202\}/g, '\u278A'], + [/\\ding\{203\}/g, '\u278B'], + [/\\ding\{204\}/g, '\u278C'], + [/\\ding\{205\}/g, '\u278D'], + [/\\ding\{206\}/g, '\u278E'], + [/\\ding\{207\}/g, '\u278F'], + [/\\ding\{208\}/g, '\u2790'], + [/\\ding\{209\}/g, '\u2791'], + [/\\ding\{210\}/g, '\u2792'], + [/\\ding\{211\}/g, '\u2793'], + [/\\ding\{212\}/g, '\u2794'], + [/\\ding\{216\}/g, '\u2798'], + [/\\ding\{217\}/g, '\u2799'], + [/\\ding\{218\}/g, '\u279A'], + [/\\ding\{219\}/g, '\u279B'], + [/\\ding\{220\}/g, '\u279C'], + [/\\ding\{221\}/g, '\u279D'], + [/\\ding\{222\}/g, '\u279E'], + [/\\ding\{223\}/g, '\u279F'], + [/\\ding\{224\}/g, '\u27A0'], + [/\\ding\{225\}/g, '\u27A1'], + [/\\ding\{226\}/g, '\u27A2'], + [/\\ding\{227\}/g, '\u27A3'], + [/\\ding\{228\}/g, '\u27A4'], + [/\\ding\{229\}/g, '\u27A5'], + [/\\ding\{230\}/g, '\u27A6'], + [/\\ding\{231\}/g, '\u27A7'], + [/\\ding\{232\}/g, '\u27A8'], + [/\\ding\{233\}/g, '\u27A9'], + [/\\ding\{234\}/g, '\u27AA'], + [/\\ding\{235\}/g, '\u27AB'], + [/\\ding\{236\}/g, '\u27AC'], + [/\\ding\{237\}/g, '\u27AD'], + [/\\ding\{238\}/g, '\u27AE'], + [/\\ding\{239\}/g, '\u27AF'], + [/\\ding\{241\}/g, '\u27B1'], + [/\\ding\{242\}/g, '\u27B2'], + [/\\ding\{243\}/g, '\u27B3'], + [/\\ding\{244\}/g, '\u27B4'], + [/\\ding\{245\}/g, '\u27B5'], + [/\\ding\{246\}/g, '\u27B6'], + [/\\ding\{247\}/g, '\u27B7'], + [/\\ding\{248\}/g, '\u27B8'], + [/\\ding\{249\}/g, '\u27B9'], + [/\\ding\{250\}/g, '\u27BA'], + [/\\ding\{251\}/g, '\u27BB'], + [/\\ding\{252\}/g, '\u27BC'], + [/\\ding\{253\}/g, '\u27BD'], + [/\\ding\{254\}/g, '\u27BE'], + [/\\longleftarrow /g, '\u27F5'], + [/\\longrightarrow /g, '\u27F6'], + [/\\longleftrightarrow /g, '\u27F7'], + [/\\Longleftarrow /g, '\u27F8'], + [/\\Longrightarrow /g, '\u27F9'], + [/\\Longleftrightarrow /g, '\u27FA'], + [/\\longmapsto /g, '\u27FC'], + [/\\sim\\joinrel\\leadsto/g, '\u27FF'], + [/\\ElsevierGlyph\{E212\}/g, '\u2905'], + [/\\UpArrowBar /g, '\u2912'], + [/\\DownArrowBar /g, '\u2913'], + [/\\ElsevierGlyph\{E20C\}/g, '\u2923'], + [/\\ElsevierGlyph\{E20D\}/g, '\u2924'], + [/\\ElsevierGlyph\{E20B\}/g, '\u2925'], + [/\\ElsevierGlyph\{E20A\}/g, '\u2926'], + [/\\ElsevierGlyph\{E211\}/g, '\u2927'], + [/\\ElsevierGlyph\{E20E\}/g, '\u2928'], + [/\\ElsevierGlyph\{E20F\}/g, '\u2929'], + [/\\ElsevierGlyph\{E210\}/g, '\u292A'], + [/\\ElsevierGlyph\{E21C\}/g, '\u2933'], + [/\\ElsevierGlyph\{E21D\}/g, '\u2933-00338'], + [/\\ElsevierGlyph\{E21A\}/g, '\u2936'], + [/\\ElsevierGlyph\{E219\}/g, '\u2937'], + [/\\Elolarr /g, '\u2940'], + [/\\Elorarr /g, '\u2941'], + [/\\ElzRlarr /g, '\u2942'], + [/\\ElzrLarr /g, '\u2944'], + [/\\Elzrarrx /g, '\u2947'], + [/\\LeftRightVector /g, '\u294E'], + [/\\RightUpDownVector /g, '\u294F'], + [/\\DownLeftRightVector /g, '\u2950'], + [/\\LeftUpDownVector /g, '\u2951'], + [/\\LeftVectorBar /g, '\u2952'], + [/\\RightVectorBar /g, '\u2953'], + [/\\RightUpVectorBar /g, '\u2954'], + [/\\RightDownVectorBar /g, '\u2955'], + [/\\DownLeftVectorBar /g, '\u2956'], + [/\\DownRightVectorBar /g, '\u2957'], + [/\\LeftUpVectorBar /g, '\u2958'], + [/\\LeftDownVectorBar /g, '\u2959'], + [/\\LeftTeeVector /g, '\u295A'], + [/\\RightTeeVector /g, '\u295B'], + [/\\RightUpTeeVector /g, '\u295C'], + [/\\RightDownTeeVector /g, '\u295D'], + [/\\DownLeftTeeVector /g, '\u295E'], + [/\\DownRightTeeVector /g, '\u295F'], + [/\\LeftUpTeeVector /g, '\u2960'], + [/\\LeftDownTeeVector /g, '\u2961'], + [/\\UpEquilibrium /g, '\u296E'], + [/\\ReverseUpEquilibrium /g, '\u296F'], + [/\\RoundImplies /g, '\u2970'], + [/\\ElsevierGlyph\{E214\}/g, '\u297C'], + [/\\ElsevierGlyph\{E215\}/g, '\u297D'], + [/\\Elztfnc /g, '\u2980'], + [/\\ElsevierGlyph\{3018\}/g, '\u2985'], + [/\\Elroang /g, '\u2986'], + [/\\ElsevierGlyph\{E291\}/g, '\u2994'], + [/\\Elzddfnc /g, '\u2999'], + [/\\Angle /g, '\u299C'], + [/\\Elzlpargt /g, '\u29A0'], + [/\\ElsevierGlyph\{E260\}/g, '\u29B5'], + [/\\ElsevierGlyph\{E61B\}/g, '\u29B6'], + [/\\ElzLap /g, '\u29CA'], + [/\\Elzdefas /g, '\u29CB'], + [/\\LeftTriangleBar /g, '\u29CF'], + [/\\NotLeftTriangleBar /g, '\u29CF-00338'], + [/\\RightTriangleBar /g, '\u29D0'], + [/\\NotRightTriangleBar /g, '\u29D0-00338'], + [/\\ElsevierGlyph\{E372\}/g, '\u29DC'], + [/\\blacklozenge /g, '\u29EB'], + [/\\RuleDelayed /g, '\u29F4'], + [/\\Elxuplus /g, '\u2A04'], + [/\\ElzThr /g, '\u2A05'], + [/\\Elxsqcup /g, '\u2A06'], + [/\\ElzInf /g, '\u2A07'], + [/\\ElzSup /g, '\u2A08'], + [/\\ElzCint /g, '\u2A0D'], + [/\\clockoint /g, '\u2A0F'], + [/\\ElsevierGlyph\{E395\}/g, '\u2A10'], + [/\\sqrint /g, '\u2A16'], + [/\\ElsevierGlyph\{E25A\}/g, '\u2A25'], + [/\\ElsevierGlyph\{E25B\}/g, '\u2A2A'], + [/\\ElsevierGlyph\{E25C\}/g, '\u2A2D'], + [/\\ElsevierGlyph\{E25D\}/g, '\u2A2E'], + [/\\ElzTimes /g, '\u2A2F'], + [/\\ElsevierGlyph\{E25E\}/g, '\u2A34'], + [/\\ElsevierGlyph\{E25E\}/g, '\u2A35'], + [/\\ElsevierGlyph\{E259\}/g, '\u2A3C'], + [/\\amalg /g, '\u2A3F'], + [/\\ElzAnd /g, '\u2A53'], + [/\\ElzOr /g, '\u2A54'], + [/\\ElsevierGlyph\{E36E\}/g, '\u2A55'], + [/\\ElOr /g, '\u2A56'], + [/\\perspcorrespond /g, '\u2A5E'], + [/\\Elzminhat /g, '\u2A5F'], + [/\\ElsevierGlyph\{225A\}/g, '\u2A63'], + [/\\stackrel\{*\}\{=\}/g, '\u2A6E'], + [/\\Equal /g, '\u2A75'], + [/\\leqslant /g, '\u2A7D'], + [/\\nleqslant /g, '\u2A7D-00338'], + [/\\geqslant /g, '\u2A7E'], + [/\\ngeqslant /g, '\u2A7E-00338'], + [/\\lessapprox /g, '\u2A85'], + [/\\gtrapprox /g, '\u2A86'], + [/\\lneq /g, '\u2A87'], + [/\\gneq /g, '\u2A88'], + [/\\lnapprox /g, '\u2A89'], + [/\\gnapprox /g, '\u2A8A'], + [/\\lesseqqgtr /g, '\u2A8B'], + [/\\gtreqqless /g, '\u2A8C'], + [/\\eqslantless /g, '\u2A95'], + [/\\eqslantgtr /g, '\u2A96'], + [/\\Pisymbol\{ppi020\}\{117\}/g, '\u2A9D'], + [/\\Pisymbol\{ppi020\}\{105\}/g, '\u2A9E'], + [/\\NestedLessLess /g, '\u2AA1'], + [/\\NotNestedLessLess /g, '\u2AA1-00338'], + [/\\NestedGreaterGreater /g, '\u2AA2'], + [/\\NotNestedGreaterGreater /g, '\u2AA2-00338'], + [/\\preceq /g, '\u2AAF'], + [/\\not\\preceq /g, '\u2AAF-00338'], + [/\\succeq /g, '\u2AB0'], + [/\\not\\succeq /g, '\u2AB0-00338'], + [/\\precneqq /g, '\u2AB5'], + [/\\succneqq /g, '\u2AB6'], + [/\\precapprox /g, '\u2AB7'], + [/\\succapprox /g, '\u2AB8'], + [/\\precnapprox /g, '\u2AB9'], + [/\\succnapprox /g, '\u2ABA'], + [/\\subseteqq /g, '\u2AC5'], + [/\\nsubseteqq /g, '\u2AC5-00338'], + [/\\supseteqq /g, '\u2AC6'], + [/\\nsupseteqq/g, '\u2AC6-00338'], + [/\\subsetneqq /g, '\u2ACB'], + [/\\supsetneqq /g, '\u2ACC'], + [/\\ElsevierGlyph\{E30D\}/g, '\u2AEB'], + [/\\Elztdcol /g, '\u2AF6'], + [/\\ElsevierGlyph\{300A\}/g, '\u300A'], + [/\\ElsevierGlyph\{300B\}/g, '\u300B'], + [/\\ElsevierGlyph\{3018\}/g, '\u3018'], + [/\\ElsevierGlyph\{3019\}/g, '\u3019'], + [/\\openbracketleft /g, '\u301A'], + [/\\openbracketright /g, '\u301B'], +] + +export default BibtexParser +if (typeof module !== 'undefined' && module.exports) { + module.exports = BibtexParser +} diff --git a/services/references/buildscript.txt b/services/references/buildscript.txt new file mode 100644 index 0000000000..05771cd85a --- /dev/null +++ b/services/references/buildscript.txt @@ -0,0 +1,9 @@ +references +--dependencies=mongo +--docker-repos=us-east1-docker.pkg.dev/overleaf-ops/ol-docker +--env-add= +--env-pass-through= +--esmock-loader=True +--node-version=20.18.2 +--public-repo=False +--script-version=4.5.0 diff --git a/services/references/config/settings.defaults.cjs b/services/references/config/settings.defaults.cjs new file mode 100644 index 0000000000..2551f99f09 --- /dev/null +++ b/services/references/config/settings.defaults.cjs @@ -0,0 +1,9 @@ +module.exports = { + internal: { + references: { + port: 3056, + host: process.env.REFERENCES_HOST || '127.0.0.1', + }, + }, +} + diff --git a/services/references/docker-compose.ci.yml b/services/references/docker-compose.ci.yml new file mode 100644 index 0000000000..51eb64d126 --- /dev/null +++ b/services/references/docker-compose.ci.yml @@ -0,0 +1,52 @@ +# This file was auto-generated, do not edit it directly. +# Instead run bin/update_build_scripts from +# https://github.com/overleaf/internal/ + +version: "2.3" + +services: + test_unit: + image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + user: node + command: npm run test:unit:_run + environment: + NODE_ENV: test + NODE_OPTIONS: "--unhandled-rejections=strict" + + + test_acceptance: + build: . + image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + environment: + ELASTIC_SEARCH_DSN: es:9200 + MONGO_HOST: mongo + POSTGRES_HOST: postgres + MOCHA_GREP: ${MOCHA_GREP} + NODE_ENV: test + NODE_OPTIONS: "--unhandled-rejections=strict" + depends_on: + mongo: + condition: service_started + user: node + command: npm run test:acceptance + + + tar: + build: . + image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + volumes: + - ./:/tmp/build/ + command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs . + user: root + mongo: + image: mongo:6.0.13 + command: --replSet overleaf + volumes: + - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js + environment: + MONGO_INITDB_DATABASE: sharelatex + extra_hosts: + # Required when using the automatic database setup for initializing the + # replica set. This override is not needed when running the setup after + # starting up mongo. + - mongo:127.0.0.1 diff --git a/services/references/docker-compose.yml b/services/references/docker-compose.yml new file mode 100644 index 0000000000..ad71431768 --- /dev/null +++ b/services/references/docker-compose.yml @@ -0,0 +1,56 @@ +# This file was auto-generated, do not edit it directly. +# Instead run bin/update_build_scripts from +# https://github.com/overleaf/internal/ + +version: "2.3" + +services: + test_unit: + image: node:20.18.2 + volumes: + - .:/overleaf/services/references + - ../../node_modules:/overleaf/node_modules + - ../../libraries:/overleaf/libraries + working_dir: /overleaf/services/references + environment: + MOCHA_GREP: ${MOCHA_GREP} + LOG_LEVEL: ${LOG_LEVEL:-} + NODE_ENV: test + NODE_OPTIONS: "--unhandled-rejections=strict" + command: npm run --silent test:unit + user: node + + test_acceptance: + image: node:20.18.2 + volumes: + - .:/overleaf/services/references + - ../../node_modules:/overleaf/node_modules + - ../../libraries:/overleaf/libraries + working_dir: /overleaf/services/references + environment: + ELASTIC_SEARCH_DSN: es:9200 + MONGO_HOST: mongo + POSTGRES_HOST: postgres + MOCHA_GREP: ${MOCHA_GREP} + LOG_LEVEL: ${LOG_LEVEL:-} + NODE_ENV: test + NODE_OPTIONS: "--unhandled-rejections=strict" + user: node + depends_on: + mongo: + condition: service_started + command: npm run --silent test:acceptance + + mongo: + image: mongo:6.0.13 + command: --replSet overleaf + volumes: + - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js + environment: + MONGO_INITDB_DATABASE: sharelatex + extra_hosts: + # Required when using the automatic database setup for initializing the + # replica set. This override is not needed when running the setup after + # starting up mongo. + - mongo:127.0.0.1 + diff --git a/services/references/package.json b/services/references/package.json new file mode 100644 index 0000000000..9b0988e7ac --- /dev/null +++ b/services/references/package.json @@ -0,0 +1,26 @@ +{ + "name": "@overleaf/references", + "description": "An API for providing citation-keys", + "private": true, + "type": "module", + "main": "app.js", + "scripts": { + "start": "node app.js" + }, + "version": "0.1.0", + "dependencies": { + "@overleaf/settings": "*", + "@overleaf/logger": "*", + "@overleaf/metrics": "*", + "async": "^3.2.5", + "express": "^4.21.2" + }, + "devDependencies": { + "chai": "^4.3.6", + "chai-as-promised": "^7.1.1", + "esmock": "^2.6.9", + "mocha": "^11.1.0", + "sinon": "^9.2.4", + "typescript": "^5.0.4" + } +} diff --git a/services/references/tsconfig.json b/services/references/tsconfig.json new file mode 100644 index 0000000000..d3fdd3022a --- /dev/null +++ b/services/references/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.backend.json", + "include": [ + "app.js", + "app/js/**/*", + "benchmarks/**/*", + "config/**/*", + "scripts/**/*", + "test/**/*", + "types" + ] +} diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index 07558a0420..240bda9071 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -264,6 +264,9 @@ module.exports = { notifications: { url: `http://${process.env.NOTIFICATIONS_HOST || '127.0.0.1'}:3042`, }, + references: { + url: `http://${process.env.REFERENCES_HOST || '127.0.0.1'}:3056`, + }, webpack: { url: `http://${process.env.WEBPACK_HOST || '127.0.0.1'}:3808`, }, From c40ab3234dd413b045157c9739d97ff91c25c6cb Mon Sep 17 00:00:00 2001 From: Rebeka Dekany <50901361+rebekadekany@users.noreply.github.com> Date: Mon, 23 Jun 2025 10:08:08 +0200 Subject: [PATCH 002/250] Prettier for PUG templates (#26170) * Setup prettier * Ignore these pug templates by prettier * Fix typo * Fix prettier error * Add prettier-ignore for quoting of event-segmentation attribute * Manual tab indentation * Interpolate * Remove unbuffered if conditional * Inline event-segmentation objects and remove prettier-ignore rule * Fix spacing before interpolation * Source format * Source format GitOrigin-RevId: c30e037f5caf8f91efc1bd9e75f81ae533b5a506 --- package-lock.json | 28 ++ services/web/.prettierignore | 25 + services/web/.prettierrc | 16 +- services/web/Makefile | 7 + services/web/app/views/_cookie_banner.pug | 18 +- services/web/app/views/_metadata.pug | 157 ++++--- .../web/app/views/_mixins/back_to_btns.pug | 4 +- .../web/app/views/_mixins/begin_now_card.pug | 10 +- .../app/views/_mixins/bookmarkable_tabset.pug | 8 +- .../web/app/views/_mixins/bootstrap_js.pug | 4 +- services/web/app/views/_mixins/eyebrow.pug | 8 +- .../views/_mixins/faq_search-marketing.pug | 29 +- .../web/app/views/_mixins/foot_scripts.pug | 15 +- .../web/app/views/_mixins/formMessages.pug | 110 ++--- services/web/app/views/_mixins/links.pug | 75 ++- .../web/app/views/_mixins/material_symbol.pug | 5 +- services/web/app/views/_mixins/navbar.pug | 14 +- .../web/app/views/_mixins/notification.pug | 16 +- services/web/app/views/_mixins/pagination.pug | 45 +- .../app/views/_mixins/previous_page_link.pug | 2 +- services/web/app/views/_mixins/quote.pug | 28 +- services/web/app/views/_mixins/recaptcha.pug | 2 +- .../reconfirm_affiliation-marketing.pug | 36 +- .../app/views/_mixins/terms_of_service.pug | 2 +- services/web/app/views/admin/index.pug | 85 ++-- .../web/app/views/beta_program/opt_in.pug | 49 +- services/web/app/views/general/400.pug | 14 +- services/web/app/views/general/404.pug | 4 +- services/web/app/views/general/500.pug | 10 +- services/web/app/views/general/closed.pug | 2 +- .../web/app/views/general/post-gateway.pug | 14 +- .../app/views/general/unsupported-browser.pug | 10 +- services/web/app/views/layout-base.pug | 133 ++++-- services/web/app/views/layout-marketing.pug | 10 +- services/web/app/views/layout-react.pug | 70 +-- .../web/app/views/layout-website-redesign.pug | 12 +- .../web/app/views/layout/fat-footer-base.pug | 67 ++- .../layout/fat-footer-website-redesign.pug | 79 ++-- services/web/app/views/layout/fat-footer.pug | 79 ++-- .../layout/language-picker-bootstrap-5.pug | 30 +- .../web/app/views/layout/language-picker.pug | 27 +- .../web/app/views/layout/layout-no-js.pug | 14 +- .../layout/navbar-marketing-bootstrap-5.pug | 204 ++++---- .../web/app/views/layout/navbar-marketing.pug | 191 ++++---- .../views/layout/navbar-website-redesign.pug | 182 ++++---- .../views/layout/thin-footer-bootstrap-5.pug | 4 +- services/web/app/views/layout/thin-footer.pug | 4 +- .../project/editor/new_from_template.pug | 58 +-- .../app/views/project/ide-react-detached.pug | 2 +- services/web/app/views/project/ide-react.pug | 9 +- .../app/views/project/invite/not-valid.pug | 6 +- .../web/app/views/project/invite/show.pug | 16 +- services/web/app/views/project/list-react.pug | 106 +++-- .../app/views/project/token/access-react.pug | 10 +- .../views/project/token/sharing-updates.pug | 8 +- .../web/app/views/subscriptions/add-seats.pug | 18 +- .../canceled-subscription-react.pug | 4 +- .../views/subscriptions/dashboard-react.pug | 109 +++-- .../manually-collected-subscription.pug | 6 +- .../missing-billing-information.pug | 6 +- .../views/subscriptions/plans/_faq_new.pug | 70 ++- .../subscriptions/plans/_plans_faq_tabs.pug | 438 +++++++++--------- .../views/subscriptions/preview-change.pug | 12 +- .../subscriptions/subtotal-limit-exceeded.pug | 6 +- .../successful-subscription-react.pug | 8 +- .../subscriptions/team/group-invites.pug | 6 +- .../subscriptions/team/invite-managed.pug | 26 +- .../app/views/subscriptions/team/invite.pug | 26 +- .../subscriptions/team/invite_logged_out.pug | 14 +- .../upgrade-group-subscription-react.pug | 14 +- .../web/app/views/user/accountSuspended.pug | 2 +- .../app/views/user/compromised_password.pug | 2 +- .../app/views/user/confirmSecondaryEmail.pug | 2 +- services/web/app/views/user/confirm_email.pug | 54 +-- .../web/app/views/user/email-preferences.pug | 34 +- services/web/app/views/user/login.pug | 40 +- .../web/app/views/user/one_time_login.pug | 4 +- .../web/app/views/user/passwordReset-bs5.pug | 56 +-- services/web/app/views/user/passwordReset.pug | 70 +-- .../app/views/user/primaryEmailCheck-bs5.pug | 38 +- services/web/app/views/user/reconfirm-bs5.pug | 55 +-- services/web/app/views/user/reconfirm.pug | 57 +-- services/web/app/views/user/register.pug | 6 +- services/web/app/views/user/sessions.pug | 32 +- .../web/app/views/user/setPassword-bs5.pug | 50 +- services/web/app/views/user/setPassword.pug | 66 +-- services/web/app/views/user/settings.pug | 81 ++-- .../user_membership/group-managers-react.pug | 10 +- .../user_membership/group-members-react.pug | 40 +- .../institution-managers-react.pug | 10 +- .../web/app/views/user_membership/new.pug | 19 +- .../publisher-managers-react.pug | 10 +- .../modules/launchpad/app/views/launchpad.pug | 159 +++---- .../user-activate/app/views/user/activate.pug | 58 ++- .../user-activate/app/views/user/register.pug | 4 +- services/web/package.json | 3 + 96 files changed, 2140 insertions(+), 1758 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2a3bb7696d..53388a5732 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8911,6 +8911,33 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@prettier/plugin-pug": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@prettier/plugin-pug/-/plugin-pug-3.4.0.tgz", + "integrity": "sha512-Jzd5rE/ellJz3vqfxyVewPsCHXw1dmIzJ3AXhAnqVBKQOj2u73ZS2oUacji8CbQSsYyCy7GXFjXWDlDTMG1x2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/Shinigami92" + }, + { + "type": "paypal", + "url": "https://www.paypal.com/donate/?hosted_button_id=L7GY729FBKTZY" + } + ], + "license": "MIT", + "dependencies": { + "pug-lexer": "^5.0.1" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + }, + "peerDependencies": { + "prettier": "^3.0.0" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -45272,6 +45299,7 @@ "@pollyjs/adapter-node-http": "^6.0.6", "@pollyjs/core": "^6.0.6", "@pollyjs/persister-fs": "^6.0.6", + "@prettier/plugin-pug": "^3.4.0", "@replit/codemirror-emacs": "overleaf/codemirror-emacs#4394c03858f27053f8768258e9493866e06e938e", "@replit/codemirror-indentation-markers": "overleaf/codemirror-indentation-markers#78264032eb286bc47871569ae87bff5ca1c6c161", "@replit/codemirror-vim": "overleaf/codemirror-vim#1bef138382d948018f3f9b8a4d7a70ab61774e4b", diff --git a/services/web/.prettierignore b/services/web/.prettierignore index 94ab5579c2..2e8db8b35b 100644 --- a/services/web/.prettierignore +++ b/services/web/.prettierignore @@ -13,3 +13,28 @@ frontend/js/features/source-editor/lezer-latex/latex.terms.mjs frontend/js/features/source-editor/lezer-bibtex/bibtex.mjs frontend/js/features/source-editor/lezer-bibtex/bibtex.terms.mjs frontend/js/features/source-editor/hunspell/wasm/hunspell.mjs + +# complex pages +app/views/project/editor.pug +app/views/project/editor/** +modules/open-in-overleaf/app/views/documentation.pug +modules/references-search/app/views/project/editor/** +modules/rich-text/app/views/toolbar.pug + +# loops +app/views/referal/bonus.pug +modules/templates/app/views/tag.pug + +# expressions that could not be formatted correctly +app/views/_mixins/faq_search.pug +app/views/external/home/v2.pug +app/views/project/token/access.pug +app/views/user/primaryEmailCheck.pug +app/views/user/restricted.pug +modules/admin-panel/app/views/project/show.pug +modules/templates/app/views/project/editor/_left-menu.pug +modules/two-factor-authentication/app/views/_mixins.pug + +# minified files +app/views/_google_analytics.pug +app/views/_customer_io.pug diff --git a/services/web/.prettierrc b/services/web/.prettierrc index 13e31862ff..b99212a874 100644 --- a/services/web/.prettierrc +++ b/services/web/.prettierrc @@ -1,9 +1,23 @@ { "arrowParens": "avoid", "jsxSingleQuote": false, + "pugAttributeSeparator": "as-needed", + "pugBracketSpacing": false, + "pugClassNotation": "as-is", + "pugIdNotation": "as-is", + "pugSortAttributesBeginning": ["name", "data-type"], + "plugins": ["@prettier/plugin-pug"], "semi": false, "singleQuote": true, "trailingComma": "es5", "tabWidth": 2, - "useTabs": false + "useTabs": false, + "overrides": [ + { + "files": "*.pug", + "options": { + "useTabs": true + } + } + ] } diff --git a/services/web/Makefile b/services/web/Makefile index 6ebbc357c6..f5a7542691 100644 --- a/services/web/Makefile +++ b/services/web/Makefile @@ -455,12 +455,19 @@ format: format_styles format_styles: npm run --silent format:styles +format: format_pug +format_pug: + npm run --silent format:pug + format_fix: npm run --silent format:fix format_styles_fix: npm run --silent format:styles:fix +format_pug_fix: + npm run --silent format:pug:fix + format_in_docker: $(RUN_LINT_FORMAT) make format -j2 --output-sync diff --git a/services/web/app/views/_cookie_banner.pug b/services/web/app/views/_cookie_banner.pug index 2d5631f9c8..56974326cd 100644 --- a/services/web/app/views/_cookie_banner.pug +++ b/services/web/app/views/_cookie_banner.pug @@ -1,5 +1,13 @@ -section.cookie-banner.hidden-print.hidden(aria-label="Cookie banner") - .cookie-banner-content We only use cookies for essential purposes and to improve your experience on our site. You can find out more in our cookie policy. - .cookie-banner-actions - button(type="button" class="btn btn-link btn-sm" data-ol-cookie-banner-set-consent="essential") Essential cookies only - button(type="button" class="btn btn-primary btn-sm" data-ol-cookie-banner-set-consent="all") Accept all cookies +section.cookie-banner.hidden-print.hidden(aria-label='Cookie banner') + .cookie-banner-content We only use cookies for essential purposes and to improve your experience on our site. You can find out more in our cookie policy. + .cookie-banner-actions + button( + type='button' + class='btn btn-link btn-sm' + data-ol-cookie-banner-set-consent='essential' + ) Essential cookies only + button( + type='button' + class='btn btn-primary btn-sm' + data-ol-cookie-banner-set-consent='all' + ) Accept all cookies diff --git a/services/web/app/views/_metadata.pug b/services/web/app/views/_metadata.pug index a784860095..6d7c599546 100644 --- a/services/web/app/views/_metadata.pug +++ b/services/web/app/views/_metadata.pug @@ -1,123 +1,140 @@ - //- 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")) +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") + 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)) + 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) +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")) + meta(name='description' content=translate('site_description')) + meta(itemprop='description' content=translate('site_description')) //- Image -if (metadata && metadata.image && metadata.image.fields) +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) + 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) + 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')) + 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 Overleaf Community Edition/Server Pro - meta(itemprop="image", content='/apple-touch-icon.png') - meta(name="image", content='/apple-touch-icon.png') + meta(itemprop='image' content='/apple-touch-icon.png') + meta(name='image' content='/apple-touch-icon.png') //- Keywords -if (metadata && metadata.keywords) - meta(name="keywords" content=metadata.keywords) +if metadata && metadata.keywords + meta(name='keywords' content=metadata.keywords) //- Misc -meta(itemprop="name", content=settings.appName + ", the Online LaTeX Editor") +meta(itemprop='name' content=settings.appName + ', the Online LaTeX Editor') -if (metadata && metadata.robotsNoindexNofollow) - meta(name="robots" content="noindex, nofollow") +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) +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) + 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) + 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')) + meta( + name='twitter:image' + content=buildImgPath('ol-brand/overleaf_og_logo.png') + ) else //- the default image for Overleaf Community Edition/Server Pro - meta(name="twitter:image", content='/apple-touch-icon.png') + meta(name='twitter:image' content='/apple-touch-icon.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 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) +if metadata && metadata.openGraphDescription + meta(property='og:description' content=metadata.openGraphDescription) else - meta(property="og:description", content=translate("site_description")) + meta(property='og:description' content=translate('site_description')) -if (metadata && metadata.openGraphImage && metadata.openGraphImage.fields) +if metadata && metadata.openGraphImage && metadata.openGraphImage.fields //- from the CMS - meta(property="og:image", content=metadata.openGraphImage.fields.file.url) -else if (settings.overleaf) + 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')) + meta( + property='og:image' + content=buildImgPath('ol-brand/overleaf_og_logo.png') + ) else //- the default image for Overleaf Community Edition/Server Pro - meta(property="og:image", content='/apple-touch-icon.png') + meta(property='og:image' content='/apple-touch-icon.png') -if (metadata && metadata.openGraphType) - meta(property="og:type", metadata.openGraphType) +if metadata && metadata.openGraphType + meta(property='og:type' metadata.openGraphType) else - meta(property="og:type", content="website") + meta(property='og:type' content='website') -if (metadata && metadata.openGraphVideo) +if metadata && metadata.openGraphVideo //- from the CMS - meta(property="og:video", content=metadata.openGraphVideo) + meta(property='og:video' content=metadata.openGraphVideo) //- Viewport if !metadata || metadata.viewport !== false - meta(name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes") + meta( + name='viewport' + content='width=device-width, initial-scale=1.0, user-scalable=yes' + ) //- Noindex if settings.robotsNoindex - meta(name="robots" content="noindex") + meta(name='robots' content='noindex') //- Icons -link(rel="icon", sizes="32x32", href="/favicon-32x32.png") -link(rel="icon", sizes="16x16", href="/favicon-16x16.png") -link(rel="icon", href="/favicon.svg" type="image/svg+xml") -link(rel="apple-touch-icon", href="/apple-touch-icon.png") -link(rel="mask-icon", href="/mask-favicon.svg", color="#046530") +link(rel='icon' sizes='32x32' href='/favicon-32x32.png') +link(rel='icon' sizes='16x16' href='/favicon-16x16.png') +link(rel='icon' href='/favicon.svg' type='image/svg+xml') +link(rel='apple-touch-icon' href='/apple-touch-icon.png') +link(rel='mask-icon' href='/mask-favicon.svg' color='#046530') //- Canonical Tag for SEO -if (metadata && metadata.canonicalURL) - link(rel="canonical" href=metadata.canonicalURL) +if metadata && metadata.canonicalURL + link(rel='canonical' href=metadata.canonicalURL) //- Manifest //- Does not currently contain a start_url to prevent browser installation prompts -link(rel="manifest" href="/web.sitemanifest") +link(rel='manifest' href='/web.sitemanifest') diff --git a/services/web/app/views/_mixins/back_to_btns.pug b/services/web/app/views/_mixins/back_to_btns.pug index dacd9ea7a2..c3a1f4f76e 100644 --- a/services/web/app/views/_mixins/back_to_btns.pug +++ b/services/web/app/views/_mixins/back_to_btns.pug @@ -1,4 +1,6 @@ mixin back-to-btns(settingsAnchor) .d-flex.flex-column.flex-sm-row.gap-3 - a.btn.btn-secondary(href=`/user/settings${settingsAnchor ? '#' + settingsAnchor : '' }`) #{translate('back_to_account_settings')} + a.btn.btn-secondary( + href=`/user/settings${settingsAnchor ? '#' + settingsAnchor : '' }` + ) #{translate('back_to_account_settings')} a.btn.btn-secondary(href='/project') #{translate('back_to_your_projects')} diff --git a/services/web/app/views/_mixins/begin_now_card.pug b/services/web/app/views/_mixins/begin_now_card.pug index 8f9919553d..641703d606 100644 --- a/services/web/app/views/_mixins/begin_now_card.pug +++ b/services/web/app/views/_mixins/begin_now_card.pug @@ -1,10 +1,10 @@ -mixin begin_now_card() +mixin begin_now_card - var registerURL = '/register' - var plansURL = '/user/subscription/plans' - var isUserLoggedIn = !!getSessionUser() .begin-now-card - div.card.card-pattern + .card.card-pattern .card-body p.dm-mono span.font-size-display-xs @@ -16,10 +16,8 @@ mixin begin_now_card() p #{translate("discover_why_over_people_worldwide_trust_overleaf", {count: settings.userCountInMillions})} p.card-links if !isUserLoggedIn - a.btn.btn-primary.card-link( - href=registerURL - ) #{translate("sign_up_for_free")} + a.btn.btn-primary.card-link(href=registerURL) #{translate("sign_up_for_free")} a.btn.card-link( - class = isUserLoggedIn ? 'btn-primary' : 'btn-secondary' + class=isUserLoggedIn ? 'btn-primary' : 'btn-secondary' href=plansURL ) #{translate("explore_all_plans")} diff --git a/services/web/app/views/_mixins/bookmarkable_tabset.pug b/services/web/app/views/_mixins/bookmarkable_tabset.pug index 27ac74ef66..72579c7725 100644 --- a/services/web/app/views/_mixins/bookmarkable_tabset.pug +++ b/services/web/app/views/_mixins/bookmarkable_tabset.pug @@ -1,10 +1,10 @@ mixin bookmarkable-tabset-header(id, title, active) - li(role="presentation") + li(role='presentation') a.nav-link( href='#' + id - class=(active ? 'active' : '') + class=active ? 'active' : '' aria-controls=id - role="tab" - data-toggle="tab" + role='tab' + data-toggle='tab' data-ol-bookmarkable-tab ) #{title} diff --git a/services/web/app/views/_mixins/bootstrap_js.pug b/services/web/app/views/_mixins/bootstrap_js.pug index 866b0b4218..746e811bf7 100644 --- a/services/web/app/views/_mixins/bootstrap_js.pug +++ b/services/web/app/views/_mixins/bootstrap_js.pug @@ -1,3 +1,3 @@ mixin bootstrap-js(bootstrapVersion) - each file in (entrypointScripts(bootstrapVersion === 5 ? 'bootstrap-5' : 'bootstrap-3')) - script(type="text/javascript", nonce=scriptNonce, src=file) + each file in entrypointScripts(bootstrapVersion === 5 ? 'bootstrap-5' : 'bootstrap-3') + script(type='text/javascript' nonce=scriptNonce src=file) diff --git a/services/web/app/views/_mixins/eyebrow.pug b/services/web/app/views/_mixins/eyebrow.pug index c5f01a10db..3eae0b8308 100644 --- a/services/web/app/views/_mixins/eyebrow.pug +++ b/services/web/app/views/_mixins/eyebrow.pug @@ -1,5 +1,5 @@ mixin eyebrow(text) - span.eyebrow-text - span(aria-hidden="true") { - span #{text} - span(aria-hidden="true") } \ No newline at end of file + span.eyebrow-text + span(aria-hidden='true') { + span #{text} + span(aria-hidden='true') } diff --git a/services/web/app/views/_mixins/faq_search-marketing.pug b/services/web/app/views/_mixins/faq_search-marketing.pug index aa41d00f9b..1b161dbdf6 100644 --- a/services/web/app/views/_mixins/faq_search-marketing.pug +++ b/services/web/app/views/_mixins/faq_search-marketing.pug @@ -1,30 +1,37 @@ mixin faq_search-marketing(headerText, headerClass) - if (typeof(settings.algolia) != "undefined" && typeof(settings.algolia.indexes) != "undefined" && typeof(settings.algolia.indexes.wiki) != "undefined") + if typeof settings.algolia != 'undefined' && typeof settings.algolia.indexes != 'undefined' && typeof settings.algolia.indexes.wiki != 'undefined' if headerText div(class=headerClass) #{headerText} .wiki - form.project-search.form-horizontal(role="search" data-ol-faq-search) + form.project-search.form-horizontal(role='search' data-ol-faq-search) .form-group.has-feedback.has-feedback-left .col-sm-12 - input.form-control(type='search', placeholder="Search help library…" aria-label="Search help library…") - i.fa.fa-search.form-control-feedback-left(aria-hidden="true") + input.form-control( + type='search' + placeholder='Search help library…' + aria-label='Search help library…' + ) + i.fa.fa-search.form-control-feedback-left(aria-hidden='true') i.fa.fa-times.form-control-feedback( - style="cursor: pointer;", + style='cursor: pointer' hidden data-ol-clear-search - aria-hidden="true" + aria-hidden='true' ) button.sr-only( - type="button" + type='button' hidden data-ol-clear-search aria-label=translate('clear_search') ) - .row(role="region" aria-label="search results") - .col-md-12() + .row(role='region' aria-label='search results') + .col-md-12 div(data-ol-search-results-wrapper) - span.sr-only(aria-live="polite" data-ol-search-sr-help-message) + span.sr-only(aria-live='polite' data-ol-search-sr-help-message) div(data-ol-search-results) - .row-spaced-small.search-result.card.card-thin(hidden data-ol-search-no-results) + .row-spaced-small.search-result.card.card-thin( + hidden + data-ol-search-no-results + ) p #{translate("no_search_results")} diff --git a/services/web/app/views/_mixins/foot_scripts.pug b/services/web/app/views/_mixins/foot_scripts.pug index c6b65e81c7..717c46cdd9 100644 --- a/services/web/app/views/_mixins/foot_scripts.pug +++ b/services/web/app/views/_mixins/foot_scripts.pug @@ -1,6 +1,11 @@ -mixin foot-scripts() +mixin foot-scripts each file in entrypointScripts(entrypoint) - script(type="text/javascript", nonce=scriptNonce, src=file, defer=deferScripts) - if (settings.devToolbar.enabled) - each file in entrypointScripts("devToolbar") - script(type="text/javascript", nonce=scriptNonce, src=file, defer=deferScripts) + script(type='text/javascript' nonce=scriptNonce src=file defer=deferScripts) + if settings.devToolbar.enabled + each file in entrypointScripts('devToolbar') + script( + type='text/javascript' + nonce=scriptNonce + src=file + defer=deferScripts + ) diff --git a/services/web/app/views/_mixins/formMessages.pug b/services/web/app/views/_mixins/formMessages.pug index 2fab7e40d8..a14bb2196a 100644 --- a/services/web/app/views/_mixins/formMessages.pug +++ b/services/web/app/views/_mixins/formMessages.pug @@ -1,41 +1,35 @@ include ./material_symbol -mixin formMessages() - div( - data-ol-form-messages='', - role="alert" - ) +mixin formMessages + div(data-ol-form-messages='' role='alert') mixin formMessagesNewStyle(extraClass = 'form-messages-bottom-margin') - - const attrs = extraClass ? { 'class': extraClass } : {} - div( - data-ol-form-messages-new-style='', - role="alert" - )&attributes(attrs) + - const attrs = extraClass ? {class: extraClass} : {} + div(data-ol-form-messages-new-style='' role='alert')&attributes(attrs) mixin customFormMessage(key, kind) if kind === 'success' - div.alert.alert-success( - hidden, - data-ol-custom-form-message=key, - role="alert" - aria-live="polite" + .alert.alert-success( + hidden + data-ol-custom-form-message=key + role='alert' + aria-live='polite' ) block else if kind === 'danger' - div.alert.alert-danger( - hidden, - data-ol-custom-form-message=key, - role="alert" - aria-live="assertive" + .alert.alert-danger( + hidden + data-ol-custom-form-message=key + role='alert' + aria-live='assertive' ) block else - div.alert.alert-warning( - hidden, - data-ol-custom-form-message=key, - role="alert" - aria-live="polite" + .alert.alert-warning( + hidden + data-ol-custom-form-message=key + role='alert' + aria-live='polite' ) block @@ -43,56 +37,50 @@ mixin customFormMessageNewStyle(key, kind, extraClass = 'mb-3') - extraClass = extraClass ? ' ' + extraClass : '' if kind === 'success' div( - class="notification notification-type-success" + extraClass, - hidden, - data-ol-custom-form-message=key, - role="alert" - aria-live="polite" + class='notification notification-type-success' + extraClass + hidden + data-ol-custom-form-message=key + role='alert' + aria-live='polite' ) - div.notification-icon - +material-symbol("check_circle") - div.notification-content.text-left + .notification-icon + +material-symbol('check_circle') + .notification-content.text-left block else if kind === 'danger' div( - class="notification notification-type-error" + extraClass, - hidden, - data-ol-custom-form-message=key, - role="alert" - aria-live="polite" + class='notification notification-type-error' + extraClass + hidden + data-ol-custom-form-message=key + role='alert' + aria-live='polite' ) - div.notification-icon - +material-symbol("error") - div.notification-content.text-left + .notification-icon + +material-symbol('error') + .notification-content.text-left block else div( - class="notification notification-type-warning" + extraClass, - hidden, - data-ol-custom-form-message=key, - role="alert" - aria-live="polite" + class='notification notification-type-warning' + extraClass + hidden + data-ol-custom-form-message=key + role='alert' + aria-live='polite' ) - div.notification-icon - +material-symbol("warning") - div.notification-content.text-left + .notification-icon + +material-symbol('warning') + .notification-content.text-left block mixin customValidationMessage(key) - div.invalid-feedback.mt-2( - hidden, - data-ol-custom-form-message=key - ) - i.fa.fa-fw.fa-warning.me-1(aria-hidden="true") + .invalid-feedback.mt-2(hidden data-ol-custom-form-message=key) + i.fa.fa-fw.fa-warning.me-1(aria-hidden='true') div block mixin customValidationMessageNewStyle(key) - div.notification.notification-type-error( - hidden, - data-ol-custom-form-message=key - ) - div.notification-icon - +material-symbol("error") - div.notification-content.text-left.small + .notification.notification-type-error(hidden data-ol-custom-form-message=key) + .notification-icon + +material-symbol('error') + .notification-content.text-left.small block diff --git a/services/web/app/views/_mixins/links.pug b/services/web/app/views/_mixins/links.pug index 566c90ee50..a919cdd74e 100644 --- a/services/web/app/views/_mixins/links.pug +++ b/services/web/app/views/_mixins/links.pug @@ -8,7 +8,8 @@ mixin linkAdvisors(linkText, linkClass, track) - 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" + a( + href='/advisors' class=linkClass ? linkClass : '' event-tracking-ga=gaCategory event-tracking=gaAction @@ -20,24 +21,36 @@ mixin linkAdvisors(linkText, linkClass, track) span #{linkText ? linkText : 'advisor programme'} mixin linkBenefits(linkText, linkClass) - a(href=(settings.siteUrl ? settings.siteUrl : '') + "/for/authors" class=linkClass ? linkClass : '') + a( + href=(settings.siteUrl ? settings.siteUrl : '') + '/for/authors' + class=linkClass ? linkClass : '' + ) | #{linkText ? linkText : 'benefits'} - + mixin linkBlog(linkText, linkClass, slug) if slug - a(href=(settings.siteUrl ? settings.siteUrl : '') + "/blog/" + slug class=linkClass ? linkClass : '') + a( + href=(settings.siteUrl ? settings.siteUrl : '') + '/blog/' + slug + class=linkClass ? linkClass : '' + ) | #{linkText ? linkText : 'blog'} mixin linkContact(linkText, linkClass) - a(href=(settings.siteUrl ? settings.siteUrl : '') + "/contact" class=linkClass ? linkClass : '') + a( + href=(settings.siteUrl ? settings.siteUrl : '') + '/contact' + class=linkClass ? linkClass : '' + ) | #{linkText ? linkText : 'contact'} mixin linkDash(linkText, linkClass) - a(href="/project" class=linkClass ? linkClass : '') + a(href='/project' class=linkClass ? linkClass : '') | #{linkText ? linkText : 'project dashboard'} mixin linkEducation(linkText, linkClass) - a(href=(settings.siteUrl ? settings.siteUrl : '') + "/for/edu" class=linkClass ? linkClass : '') + a( + href=(settings.siteUrl ? settings.siteUrl : '') + '/for/edu' + class=linkClass ? linkClass : '' + ) | #{linkText ? linkText : 'teaching toolkit'} mixin linkInvite(linkText, linkClass, track) @@ -48,7 +61,8 @@ mixin linkInvite(linkText, linkClass, track) - var mbSegmentation = track && track.segmentation ? track.segmentation : null - var trigger = track && track.trigger ? track.trigger : null - a(href="/user/bonus" + a( + href='/user/bonus' class=linkClass ? linkClass : '' event-tracking-ga=gaCategory event-tracking=gaAction @@ -60,7 +74,7 @@ mixin linkInvite(linkText, linkClass, track) span #{linkText ? linkText : 'invite your friends'} mixin linkPlansAndPricing(linkText, linkClass) - a(href="/user/subscription/plans" class=linkClass ? linkClass : '') + a(href='/user/subscription/plans' class=linkClass ? linkClass : '') | #{linkText ? linkText : 'plans and pricing'} mixin linkPrintNewTab(linkText, linkClass, icon, track) @@ -71,7 +85,8 @@ mixin linkPrintNewTab(linkText, linkClass, icon, track) - var mbSegmentation = track && track.segmentation ? track.segmentation : null - var trigger = track && track.trigger ? track.trigger : null - a(href='?media=print' + a( + href='?media=print' class=linkClass ? linkClass : '' event-tracking-ga=gaCategory event-tracking=gaAction @@ -79,20 +94,26 @@ mixin linkPrintNewTab(linkText, linkClass, icon, track) event-tracking-trigger=trigger event-tracking-mb=mb event-segmentation=mbSegmentation - target="_BLANK", - rel="noopener noreferrer" + target='_BLANK' + rel='noopener noreferrer' ) if icon - i(class="fa fa-print") + i(class='fa fa-print') |   span #{linkText ? linkText : 'print'} mixin linkSignIn(linkText, linkClass, redirect) - a(href=`/login${redirect ? '?redir=' + redirect : ''}` class=linkClass ? linkClass : '') + a( + href=`/login${redirect ? '?redir=' + redirect : ''}` + class=linkClass ? linkClass : '' + ) | #{linkText ? linkText : 'sign in'} mixin linkSignUp(linkText, linkClass, redirect) - a(href=`/register${redirect ? '?redir=' + redirect : ''}` class=linkClass ? linkClass : '') + a( + href=`/register${redirect ? '?redir=' + redirect : ''}` + class=linkClass ? linkClass : '' + ) | #{linkText ? linkText : 'sign up'} mixin linkTweet(linkText, linkClass, tweetText, track) @@ -103,23 +124,33 @@ mixin linkTweet(linkText, linkClass, tweetText, track) - 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 + 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" + 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 : '') + a( + href=(settings.siteUrl ? settings.siteUrl : '') + '/for/universities' + class=linkClass ? linkClass : '' + ) | #{linkText ? linkText : 'universities'} mixin linkWithArrow({text, href, eventTracking, eventSegmentation, eventTrackingTrigger}) - a.link-with-arrow(href=href event-tracking=eventTracking event-segmentation=eventSegmentation, event-tracking-trigger=eventTrackingTrigger event-tracking-mb) + a.link-with-arrow( + href=href + event-tracking=eventTracking + event-segmentation=eventSegmentation + event-tracking-trigger=eventTrackingTrigger + event-tracking-mb + ) | #{text} - +material-symbol("arrow_right_alt") + +material-symbol('arrow_right_alt') diff --git a/services/web/app/views/_mixins/material_symbol.pug b/services/web/app/views/_mixins/material_symbol.pug index 1e97425faf..e4e8925f93 100644 --- a/services/web/app/views/_mixins/material_symbol.pug +++ b/services/web/app/views/_mixins/material_symbol.pug @@ -1,7 +1,8 @@ mixin material-symbol(icon, extraClass = null) - extraClass = extraClass ? ' ' + extraClass : '' - span(aria-hidden="true", translate="no", class="material-symbols" + extraClass)&attributes(attributes) #{icon} - + span(aria-hidden='true' translate='no' class='material-symbols' + extraClass)&attributes(attributes) + | #{icon} + mixin material-symbol-outlined(icon, extraClass = null) - extraClass = extraClass ? ' ' + extraClass : '' +material-symbol(icon, 'material-symbols-outlined' + extraClass)&attributes(attributes) diff --git a/services/web/app/views/_mixins/navbar.pug b/services/web/app/views/_mixins/navbar.pug index f3482d3b54..0ea2e4e3a0 100644 --- a/services/web/app/views/_mixins/navbar.pug +++ b/services/web/app/views/_mixins/navbar.pug @@ -1,23 +1,23 @@ mixin nav-item - li(role="none")&attributes(attributes) + li(role='none')&attributes(attributes) block mixin nav-link - a(role="menuitem").nav-link&attributes(attributes) + a.nav-link(role='menuitem')&attributes(attributes) block mixin dropdown-menu - ul(role="menu").dropdown-menu&attributes(attributes) + ul.dropdown-menu(role='menu')&attributes(attributes) block mixin dropdown-menu-item - li(role="none") + li(role='none') block mixin dropdown-menu-link-item +dropdown-menu-item - a(role="menuitem").dropdown-item&attributes(attributes) + a.dropdown-item(role='menuitem')&attributes(attributes) block - + mixin dropdown-menu-divider - li(role="separator").dropdown-divider.d-none.d-lg-block + li.dropdown-divider.d-none.d-lg-block(role='separator') diff --git a/services/web/app/views/_mixins/notification.pug b/services/web/app/views/_mixins/notification.pug index fb0db79630..482dd540c5 100644 --- a/services/web/app/views/_mixins/notification.pug +++ b/services/web/app/views/_mixins/notification.pug @@ -3,25 +3,19 @@ include ./material_symbol mixin notificationIcon(type) if type === 'info' - +material-symbol("info") + +material-symbol('info') else if type === 'success' - +material-symbol("check_circle") + +material-symbol('check_circle') else if type === 'error' - +material-symbol("error") + +material-symbol('error') else if type === 'warning' - +material-symbol("warning") - + +material-symbol('warning') mixin notification(options) - var {ariaLive, id, type, title, content, disclaimer, className} = options - var classNames = `notification notification-type-${type} ${className ? className : ''} ${isActionBelowContent ? 'notification-cta-below-content' : ''}` - div( - aria-live=ariaLive, - role="alert", - id=id, - class=classNames - ) + div(aria-live=ariaLive role='alert' id=id class=classNames) .notification-icon +notificationIcon(type) .notification-content-and-cta diff --git a/services/web/app/views/_mixins/pagination.pug b/services/web/app/views/_mixins/pagination.pug index ef5f62bd0e..a2422e8c5d 100644 --- a/services/web/app/views/_mixins/pagination.pug +++ b/services/web/app/views/_mixins/pagination.pug @@ -10,77 +10,74 @@ mixin pagination(pages, page_path, max_btns) - 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/" + - var next_index = 0 + - var full_page_path = page_path + '/page/' - nav(role="navigation" aria-label=(translate("pagination_navigation"))) + nav(role='navigation' aria-label=translate('pagination_navigation')) ul.pagination if pages.current_page > 1 li - a( - aria-label=translate("go_to_first_page") - href=page_path - ) - span(aria-hidden="true") << + a(aria-label=translate('go_to_first_page') href=page_path) + span(aria-hidden='true') << | | First li a( - aria-label=translate("go_to_previous_page") + aria-label=translate('go_to_previous_page') href=full_page_path + (parseInt(pages.current_page, 10) - 1) - rel="prev" + rel='prev' ) - span(aria-hidden="true") < + span(aria-hidden='true') < | | Prev if pages.current_page - max_btns > 1 - li(aria-hidden="true") + li(aria-hidden='true') span … while prev_page < pages.current_page li a( - aria-label=translate("go_to_page_x", {page: prev_page}) + aria-label=translate('go_to_page_x', {page: prev_page}) href=full_page_path + prev_page ) #{prev_page} - prev_page++ - li(class="active") + li(class='active') span( - aria-label=translate("current_page_page", {page: pages.current_page}) - aria-current="true" + aria-label=translate('current_page_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=translate("go_to_page_x", {page: next_page}) + aria-label=translate('go_to_page_x', {page: next_page}) href=full_page_path + next_page ) #{next_page} - next_page++ - next_index++ - if next_page <= pages.total_pages - li.ellipses(aria-hidden="true") + if next_page <= pages.total_pages + li.ellipses(aria-hidden='true') span … li a( - aria-label=translate("go_to_next_page") + aria-label=translate('go_to_next_page') href=full_page_path + (parseInt(pages.current_page, 10) + 1) - rel="next" + rel='next' ) | Next | - span(aria-hidden="true") > + span(aria-hidden='true') > li a( - aria-label=translate("go_to_last_page") + aria-label=translate('go_to_last_page') href=full_page_path + pages.total_pages ) | Last | - span(aria-hidden="true") >> + span(aria-hidden='true') >> diff --git a/services/web/app/views/_mixins/previous_page_link.pug b/services/web/app/views/_mixins/previous_page_link.pug index 1b646a04d2..9809409d4e 100644 --- a/services/web/app/views/_mixins/previous_page_link.pug +++ b/services/web/app/views/_mixins/previous_page_link.pug @@ -2,5 +2,5 @@ include ./material_symbol mixin previous-page-link(href, text) a.previous-page-link(href=href) - +material-symbol-rounded("arrow_left_alt") + +material-symbol-rounded('arrow_left_alt') | #{text} diff --git a/services/web/app/views/_mixins/quote.pug b/services/web/app/views/_mixins/quote.pug index 573e0b6b0c..a414c4dcec 100644 --- a/services/web/app/views/_mixins/quote.pug +++ b/services/web/app/views/_mixins/quote.pug @@ -3,10 +3,10 @@ mixin quoteLargeTextCentered(quote, person, position, affiliation, link, picture .quote !{quote} if pictureUrl .quote-img - -var pictureAlt=`Photo of ${person}` + - var pictureAlt = `Photo of ${person}` img(src=pictureUrl alt=pictureAlt) footer - div.quote-person + .quote-person strong #{person} if person && position div #{position} @@ -29,27 +29,27 @@ mixin quoteLeftGreenBorder({quote, person, position, affiliation, link}) mixin collinsQuote1 .card.card-dark-green-bg - -var quote = 'Overleaf is indispensable for us. We use it in our research, thesis writing, project proposals, and manuscripts for publication. When it comes to writing, it’s our main tool.' - -var quotePerson = 'Christopher Collins' - -var quotePersonPosition = 'Associate Professor and Lab Director, Ontario Tech University' - -var quotePersonImg = buildImgPath("advocates/collins.jpg") + - var quote = 'Overleaf is indispensable for us. We use it in our research, thesis writing, project proposals, and manuscripts for publication. When it comes to writing, it’s our main tool.' + - var quotePerson = 'Christopher Collins' + - var quotePersonPosition = 'Associate Professor and Lab Director, Ontario Tech University' + - var quotePersonImg = buildImgPath('advocates/collins.jpg') .card-body +quoteLargeTextCentered(quote, quotePerson, quotePersonPosition, null, null, quotePersonImg) mixin collinsQuote2 .card.card-dark-green-bg - -var quote = 'We are writing collaboratively right up until the last minute. We are faced with deadlines all the time, and Overleaf gives us the ability to polish right up until the last possible second.' - -var quotePerson = 'Christopher Collins' - -var quotePersonPosition = 'Associate Professor and Lab Director, Ontario Tech University' - -var quotePersonImg = buildImgPath("advocates/collins.jpg") + - var quote = 'We are writing collaboratively right up until the last minute. We are faced with deadlines all the time, and Overleaf gives us the ability to polish right up until the last possible second.' + - var quotePerson = 'Christopher Collins' + - var quotePersonPosition = 'Associate Professor and Lab Director, Ontario Tech University' + - var quotePersonImg = buildImgPath('advocates/collins.jpg') .card-body +quoteLargeTextCentered(quote, quotePerson, quotePersonPosition, null, null, quotePersonImg) mixin bennettQuote1 .card.card-dark-green-bg - -var quote = 'With Overleaf, we now have a process for developing technical documentation which has virtually eliminated the time required to properly format and layout documents.' - -var quotePerson = 'Andrew Bennett' - -var quotePersonPosition = 'Software Architect, Symplectic' - -var quotePersonImg = buildImgPath("advocates/bennett.jpg") + - var quote = 'With Overleaf, we now have a process for developing technical documentation which has virtually eliminated the time required to properly format and layout documents.' + - var quotePerson = 'Andrew Bennett' + - var quotePersonPosition = 'Software Architect, Symplectic' + - var quotePersonImg = buildImgPath('advocates/bennett.jpg') .card-body +quoteLargeTextCentered(quote, quotePerson, quotePersonPosition, null, null, quotePersonImg) diff --git a/services/web/app/views/_mixins/recaptcha.pug b/services/web/app/views/_mixins/recaptcha.pug index 24e0c501ea..ec5604c825 100644 --- a/services/web/app/views/_mixins/recaptcha.pug +++ b/services/web/app/views/_mixins/recaptcha.pug @@ -1,2 +1,2 @@ -mixin recaptchaConditions() +mixin recaptchaConditions .recaptcha-branding !{translate("recaptcha_conditions", {}, [{}, {name: 'a', attrs: {href: 'https://policies.google.com/privacy', rel: 'noopener noreferrer', target: '_blank'}}, {name: 'a', attrs: {href: 'https://policies.google.com/terms', rel: 'noopener noreferrer', target: '_blank'}}])} diff --git a/services/web/app/views/_mixins/reconfirm_affiliation-marketing.pug b/services/web/app/views/_mixins/reconfirm_affiliation-marketing.pug index c42a3b439a..f54dd5d4ba 100644 --- a/services/web/app/views/_mixins/reconfirm_affiliation-marketing.pug +++ b/services/web/app/views/_mixins/reconfirm_affiliation-marketing.pug @@ -1,14 +1,11 @@ mixin reconfirmAffiliationNotification-marketing(userEmail, location) - form( - data-ol-async-form - action='/user/emails/send-reconfirmation' - ) - input(name="_csrf" type="hidden" value=csrfToken) - input(name="email" type="hidden" value=userEmail.email) - +formMessages() + form(data-ol-async-form action='/user/emails/send-reconfirmation') + input(name='_csrf' type='hidden' value=csrfToken) + input(name='email' type='hidden' value=userEmail.email) + +formMessages .reconfirm-notification - div(data-ol-not-sent style="width:100%;") + div(data-ol-not-sent style='width: 100%') i.fa.fa-warning - var ssoEnabled = userEmail.affiliation && userEmail.affiliation.institution && userEmail.affiliation.institution.ssoEnabled @@ -18,16 +15,16 @@ mixin reconfirmAffiliationNotification-marketing(userEmail, location) data-ol-slow-link href=`${settings.saml.ukamf.initPath}?university_id=${institutionId}&reconfirm=${location}` ) - span(data-ol-inflight="idle") #{translate("confirm_affiliation")} - span(hidden data-ol-inflight="pending") #{translate("pending")}… + span(data-ol-inflight='idle') #{translate("confirm_affiliation")} + span(hidden data-ol-inflight='pending') #{translate("pending")}… else button.btn-reconfirm.btn.btn-sm.btn-info( - type="submit" + type='submit' data-ol-disabled-inflight ) - span(data-ol-inflight="idle") #{translate("confirm_affiliation")} - span(hidden data-ol-inflight="pending") #{translate("pending")}… + span(data-ol-inflight='idle') #{translate("confirm_affiliation")} + span(hidden data-ol-inflight='pending') #{translate("pending")}… | !{translate("are_you_still_at", {institutionName: userEmail.affiliation.institution.name}, ['strong'])}  @@ -39,22 +36,19 @@ mixin reconfirmAffiliationNotification-marketing(userEmail, location) | !{translate("please_reconfirm_institutional_email", {}, [{name: 'a', attrs: {href: '/user/settings?remove=' + userEmail.email}}])} |   - a(href="/learn/how-to/Institutional_Email_Reconfirmation" target="_blank") #{translate("learn_more")} + a(href='/learn/how-to/Institutional_Email_Reconfirmation' target='_blank') #{translate("learn_more")} div(hidden data-ol-sent) | !{translate("please_check_your_inbox_to_confirm", {institutionName: userEmail.affiliation.institution.name}, ['strong'])} |   - button.btn-inline-link( - type="submit" - data-ol-disabled-inflight - ) - span(data-ol-inflight="idle") #{translate('resend_confirmation_email')} - span(hidden data-ol-inflight="pending") #{translate("pending")}… + button.btn-inline-link(type='submit' data-ol-disabled-inflight) + span(data-ol-inflight='idle') #{translate('resend_confirmation_email')} + span(hidden data-ol-inflight='pending') #{translate("pending")}… mixin reconfirmedAffiliationNotification-marketing(userEmail) .alert.alert-info .reconfirm-notification - div(style="width:100%;") + div(style='width: 100%') //- extra div for flex styling | !{translate("your_affiliation_is_confirmed", {institutionName: userEmail.affiliation.institution.name}, ['strong'])} | diff --git a/services/web/app/views/_mixins/terms_of_service.pug b/services/web/app/views/_mixins/terms_of_service.pug index 0fc3887b42..b0b5aaf81d 100644 --- a/services/web/app/views/_mixins/terms_of_service.pug +++ b/services/web/app/views/_mixins/terms_of_service.pug @@ -1,3 +1,3 @@ mixin termsOfServiceAgreement - div.tos-agreement-notice + .tos-agreement-notice | !{translate("by_registering_you_agree_to_our_terms_of_service", {}, [{name: 'a', attrs: {href: '/legal#Terms', target: '_blank'}}, {name: 'a', attrs: {href: '/legal#Privacy', target: '_blank'}}])} diff --git a/services/web/app/views/admin/index.pug b/services/web/app/views/admin/index.pug index aaf2228cbc..7a284d1f41 100644 --- a/services/web/app/views/admin/index.pug +++ b/services/web/app/views/admin/index.pug @@ -2,7 +2,7 @@ extends ../layout-marketing include ../_mixins/bookmarkable_tabset block content - .content.content-alt#main-content + #main-content.content.content-alt .container .row .col-sm-12 @@ -12,7 +12,7 @@ block content h1 Admin Panel .ol-tabs(data-ol-bookmarkable-tabset) .nav-tabs-container - ul.nav.nav-tabs.align-left(role="tablist") + ul.nav.nav-tabs.align-left(role='tablist') +bookmarkable-tabset-header('system-messages', 'System Messages', true) +bookmarkable-tabset-header('open-sockets', 'Open Sockets') +bookmarkable-tabset-header('open-close-editor', 'Open/Close Editor') @@ -20,29 +20,28 @@ block content +bookmarkable-tabset-header('tpds', 'TPDS/Dropbox Management') .tab-content - .tab-pane.active( - role="tabpanel" - id='system-messages' - ) + .tab-pane.active(role='tabpanel' id='system-messages') each message in systemMessages ul.system-messages li.system-message.row-spaced #{message.content} hr - form(method='post', action='/admin/messages') - input(name="_csrf", type="hidden", value=csrfToken) + form(method='post' action='/admin/messages') + input(name='_csrf' type='hidden' value=csrfToken) .form-group - label.form-label(for="content") - input.form-control(name="content", type="text", placeholder="Message…", required) - button.btn.btn-primary(type="submit") Post Message + label.form-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 + form(method='post' action='/admin/messages/clear') + input(name='_csrf' type='hidden' value=csrfToken) + button.btn.btn-danger(type='submit') Clear all messages - .tab-pane( - role="tabpanel" - id='open-sockets' - ) + .tab-pane(role='tabpanel' id='open-sockets') .row-spaced ul each agents, url in openSockets @@ -51,52 +50,56 @@ block content each agent in agents li #{agent} - .tab-pane( - role="tabpanel" - id='open-close-editor' - ) + .tab-pane(role='tabpanel' id='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 + 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 + 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 + 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-pane( - role="tabpanel" - id='tpds' - ) + .tab-pane(role='tabpanel' id='tpds') h3 Flush project to TPDS .row - form.col-xs-6(method='post',action='/admin/flushProjectToTpds') - input(name="_csrf", type="hidden", value=csrfToken) + form.col-xs-6(method='post' action='/admin/flushProjectToTpds') + input(name='_csrf' type='hidden' value=csrfToken) .form-group label.form-label(for='project_id') project_id - input.form-control(type='text', name='project_id', placeholder='project_id', required) + input.form-control( + name='project_id' + type='text' + 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.col-xs-6(method='post' action='/admin/pollDropboxForUser') + input(name='_csrf' type='hidden' value=csrfToken) .form-group label.form-label(for='user_id') user_id - input.form-control(type='text', name='user_id', placeholder='user_id', required) + input.form-control( + name='user_id' + type='text' + placeholder='user_id' + required + ) .form-group button.btn-primary.btn(type='submit') Poll diff --git a/services/web/app/views/beta_program/opt_in.pug b/services/web/app/views/beta_program/opt_in.pug index 3122dc127f..c30cb78d9a 100644 --- a/services/web/app/views/beta_program/opt_in.pug +++ b/services/web/app/views/beta_program/opt_in.pug @@ -2,14 +2,14 @@ extends ../layout-marketing include ../_mixins/back_to_btns block content - main.content.content-alt#main-content + main#main-content.content.content-alt .container.beta-opt-in-wrapper .row .col-lg-10.offset-lg-1.col-xl-8.offset-xl-2 .card .card-body .page-header - h1 + h1 | #{translate("sharelatex_beta_program")} .beta-opt-in .container-fluid @@ -28,7 +28,9 @@ block content ul li | #{translate("beta_program_badge_description")}  - span.badge.bg-warning-light-bg.text-warning(aria-label=translate("beta_feature_badge")) + span.badge.bg-warning-light-bg.text-warning( + aria-label=translate('beta_feature_badge') + ) span.badge-content β li !{translate("you_will_be_able_to_contact_us_any_time_to_share_your_feedback", {}, ['strong'])}. li !{translate("we_may_also_contact_you_from_time_to_time_by_email_with_a_survey", {}, ['strong'])}. @@ -40,37 +42,30 @@ block content if user.betaProgram form( data-ol-regular-form - method="post" - action="/beta/opt-out" + method='post' + action='/beta/opt-out' novalidate ) - input(type="hidden", name="_csrf", value=csrfToken) + input(name='_csrf' type='hidden' value=csrfToken) .form-group - a( - href="https://forms.gle/CFEsmvZQTAwHCd3X9" - target="_blank" - rel="noopener noreferrer" - ).btn.btn-primary.btn-lg #{translate("give_feedback")} + a.btn.btn-primary.btn-lg( + href='https://forms.gle/CFEsmvZQTAwHCd3X9' + target='_blank' + rel='noopener noreferrer' + ) #{translate("give_feedback")} .form-group button.btn.btn-secondary-info.btn-secondary.btn-sm( - type="submit" + type='submit' data-ol-disabled-inflight ) - span(data-ol-inflight="idle") #{translate("beta_program_opt_out_action")} - span(hidden data-ol-inflight="pending") #{translate("processing")}… + span(data-ol-inflight='idle') #{translate("beta_program_opt_out_action")} + span(hidden data-ol-inflight='pending') #{translate("processing")}… else - form( - data-ol-regular-form - method="post", - action="/beta/opt-in" - ) - input(type="hidden", name="_csrf", value=csrfToken) + form(data-ol-regular-form method='post' action='/beta/opt-in') + input(name='_csrf' type='hidden' value=csrfToken) .form-group - button.btn.btn-primary( - type="submit" - data-ol-disabled-inflight - ) - span(data-ol-inflight="idle") #{translate("beta_program_opt_in_action")} - span(hidden data-ol-inflight="pending") #{translate("joining")}… + button.btn.btn-primary(type='submit' data-ol-disabled-inflight) + span(data-ol-inflight='idle') #{translate("beta_program_opt_in_action")} + span(hidden data-ol-inflight='pending') #{translate("joining")}… .page-separator - +back-to-btns() + +back-to-btns diff --git a/services/web/app/views/general/400.pug b/services/web/app/views/general/400.pug index fcc3007e6f..5dad09910f 100644 --- a/services/web/app/views/general/400.pug +++ b/services/web/app/views/general/400.pug @@ -1,29 +1,29 @@ extends ../layout/layout-no-js block vars - - metadata = { title: 'Something went wrong' } + - metadata = {title: 'Something went wrong'} block body body.full-height - main.content.content-alt.full-height#main-content + main#main-content.content.content-alt.full-height .container.full-height .error-container.full-height .error-details p.error-status Something went wrong, sorry. p.error-description | There was a problem with your request. - if(message) + if message | | The error is: - if(message) + if message p.error-box | #{message} p.error-description | Please go back and try again. | If the problem persists, please contact us at | - a(href="mailto:" + settings.adminEmail) #{settings.adminEmail} + a(href='mailto:' + settings.adminEmail) #{settings.adminEmail} | . p.error-actions - a.error-btn(href="javascript:history.back()") Back - a.btn.btn-secondary(href="/") Home + a.error-btn(href='javascript:history.back()') Back + a.btn.btn-secondary(href='/') Home diff --git a/services/web/app/views/general/404.pug b/services/web/app/views/general/404.pug index f76eac6997..ce92d6d56e 100644 --- a/services/web/app/views/general/404.pug +++ b/services/web/app/views/general/404.pug @@ -1,11 +1,11 @@ extends ../layout-marketing block content - main.content.content-alt#main-content + main#main-content.content.content-alt .container .error-container .error-details p.error-status Not found p.error-description #{translate("cant_find_page")} p.error-actions - a.error-btn(href="/") Home + a.error-btn(href='/') Home diff --git a/services/web/app/views/general/500.pug b/services/web/app/views/general/500.pug index 41e7440e0d..22d6ceb35c 100644 --- a/services/web/app/views/general/500.pug +++ b/services/web/app/views/general/500.pug @@ -1,11 +1,11 @@ extends ../layout/layout-no-js block vars - - metadata = { title: 'Something went wrong' } + - metadata = {title: 'Something went wrong'} block body body.full-height - main.content.content-alt.full-height#main-content + main#main-content.content.content-alt.full-height .container.full-height .error-container.full-height .error-details @@ -13,11 +13,11 @@ block body 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} + a(href='http://' + settings.statusPageUrl) #{settings.statusPageUrl} | | or contact us at | - a(href="mailto:" + settings.adminEmail) #{settings.adminEmail} + a(href='mailto:' + settings.adminEmail) #{settings.adminEmail} | . p.error-actions - a.error-btn(href="/") Home + a.error-btn(href='/') Home diff --git a/services/web/app/views/general/closed.pug b/services/web/app/views/general/closed.pug index b3f8ea2c04..3c1196a4cc 100644 --- a/services/web/app/views/general/closed.pug +++ b/services/web/app/views/general/closed.pug @@ -1,7 +1,7 @@ extends ../layout-marketing block content - main.content#main-content + main#main-content.content .container .row .col-lg-8.offset-lg-2.text-center diff --git a/services/web/app/views/general/post-gateway.pug b/services/web/app/views/general/post-gateway.pug index b17e61cb41..c6bbc92d01 100644 --- a/services/web/app/views/general/post-gateway.pug +++ b/services/web/app/views/general/post-gateway.pug @@ -15,12 +15,8 @@ block content .card-body p.text-center #{translate('processing_your_request')} - form( - data-ol-regular-form - data-ol-auto-submit - method="POST" - ) - input(name="_csrf" type="hidden" value=csrfToken) - input(hidden name="viaGateway" type="submit" value="true") - for name in Object.keys(form_data) - input(name=name type="hidden" value=form_data[name]) + form(data-ol-regular-form data-ol-auto-submit method='POST') + input(name='_csrf' type='hidden' value=csrfToken) + input(name='viaGateway' hidden type='submit' value='true') + each name in Object.keys(form_data) + input(name=name type='hidden' value=form_data[name]) diff --git a/services/web/app/views/general/unsupported-browser.pug b/services/web/app/views/general/unsupported-browser.pug index a2c2216315..1fd2c42bdb 100644 --- a/services/web/app/views/general/unsupported-browser.pug +++ b/services/web/app/views/general/unsupported-browser.pug @@ -1,11 +1,11 @@ extends ../layout/layout-no-js block vars - - metadata = { title: 'Unsupported browser' } + - metadata = {title: 'Unsupported browser'} block body body.full-height - main.content.content-alt.full-height#main-content + main#main-content.content.content-alt.full-height .container.full-height .error-container.full-height .error-details @@ -15,7 +15,7 @@ block body br | If you think you're seeing this message in error, | - a(href="mailto:" + settings.adminEmail) please let us know + a(href='mailto:' + settings.adminEmail) please let us know | . if fromURL @@ -33,7 +33,7 @@ block body p | Support for beta or developer-preview browser versions cannot be guaranteed. Please | - a(href="mailto:" + settings.adminEmail) get in touch + a(href='mailto:' + settings.adminEmail) get in touch | | if you encounter any issues while using the service with beta or developer-preview releases of supported browsers. p @@ -41,5 +41,5 @@ block body p | If you cannot upgrade to one of the supported browsers, | - a(href="mailto:" + settings.adminEmail) please let us know + a(href='mailto:' + settings.adminEmail) please let us know | . diff --git a/services/web/app/views/layout-base.pug b/services/web/app/views/layout-base.pug index 0493281353..b590618387 100644 --- a/services/web/app/views/layout-base.pug +++ b/services/web/app/views/layout-base.pug @@ -2,8 +2,8 @@ include ./_mixins/foot_scripts doctype html html( - lang=(currentLngCode || 'en') - class=(fixedSizeDocument ? 'fixed-size-document' : undefined) + lang=currentLngCode || 'en' + class=fixedSizeDocument ? 'fixed-size-document' : undefined ) - metadata = metadata || {} - let bootstrap5PageStatus = 'enabled' // One of 'disabled' and 'enabled' @@ -22,91 +22,128 @@ html( include ./_metadata.pug - const bootstrapVersion = bootstrap5PageStatus !== 'disabled' && (bootstrap5Override || bootstrap5PageSplitTest === '' || splitTestVariants[bootstrap5PageSplitTest] === 'enabled') ? 5 : 3 - + //- Stylesheet - link(rel='stylesheet', href=buildCssPath(getCssThemeModifier(userSettings, brandVariation, enableIeeeBranding), bootstrapVersion), id="main-stylesheet") + link( + rel='stylesheet' + href=buildCssPath(getCssThemeModifier(userSettings, brandVariation, enableIeeeBranding), bootstrapVersion) + id='main-stylesheet' + ) block css each file in entrypointStyles(entrypoint) - link(rel='stylesheet', href=file) + link(rel='stylesheet' href=file) block _headLinks - if (typeof suppressRelAlternateLinks == "undefined") + if typeof suppressRelAlternateLinks == 'undefined' if settings.i18n.subdomainLang each subdomainDetails in settings.i18n.subdomainLang if !subdomainDetails.hide - link(rel="alternate", href=subdomainDetails.url + currentUrl, hreflang=subdomainDetails.lngCode) + link( + rel='alternate' + href=subdomainDetails.url + currentUrl + hreflang=subdomainDetails.lngCode + ) - if (entrypoint !== 'marketing') - link(rel="preload", href=buildJsPath(currentLngCode + "-json.js"), as="script", nonce=scriptNonce) + if entrypoint !== 'marketing' + link( + rel='preload' + href=buildJsPath(currentLngCode + '-json.js') + as='script' + nonce=scriptNonce + ) //- Scripts - if (typeof suppressGoogleAnalytics == "undefined") + if typeof suppressGoogleAnalytics == 'undefined' include _google_analytics block meta - meta(name="ol-csrfToken" content=csrfToken) + 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-mathJaxPath" content=mathJaxPath) - meta(name="ol-dictionariesRoot" content=dictionariesRoot) + meta(name='ol-baseAssetPath' content=buildBaseAssetPath()) + meta(name='ol-mathJaxPath' content=mathJaxPath) + meta(name='ol-dictionariesRoot' content=dictionariesRoot) - meta(name="ol-usersEmail" content=getUserEmail()) - meta(name="ol-ab" data-type="json" content={}) - meta(name="ol-user_id" content=getLoggedInUserId()) + meta(name='ol-usersEmail' content=getUserEmail()) + 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 - }) + 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) - meta(name="ol-splitTestVariants" data-type="json" content=splitTestVariants || {}) - meta(name="ol-splitTestInfo" data-type="json" content=splitTestInfo || {}) + meta(name='ol-ExposedSettings' data-type='json' content=ExposedSettings) + meta( + name='ol-splitTestVariants' + data-type='json' + content=splitTestVariants || {} + ) + meta(name='ol-splitTestInfo' data-type='json' content=splitTestInfo || {}) - 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.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, + } + ) - meta(name="ol-isManagedAccount" data-type="boolean" content=isManagedAccount) + meta( + name='ol-isManagedAccount' + data-type='boolean' + content=isManagedAccount + ) each restriction in userRestrictions || [] - meta(name='ol-cannot-' + restriction data-type="boolean" content=true) - meta(name="ol-bootstrapVersion" data-type="json" content=bootstrapVersion) + meta(name='ol-cannot-' + restriction data-type='boolean' content) + meta(name='ol-bootstrapVersion' data-type='json' content=bootstrapVersion) block head-scripts - body(class={ - 'thin-footer': showThinFooter, - 'website-redesign': isWebsiteRedesign === true || websiteRedesignOverride, - 'application-page': isApplicationPage - }, data-theme="default") - if(settings.recaptcha && settings.recaptcha.siteKeyV3) - script(type="text/javascript", nonce=scriptNonce, src="https://www.recaptcha.net/recaptcha/api.js?render=" + settings.recaptcha.siteKeyV3, defer=deferScripts) + body( + class={ + 'thin-footer': showThinFooter, + 'website-redesign': isWebsiteRedesign === true || websiteRedesignOverride, + 'application-page': isApplicationPage, + } + data-theme='default' + ) + if settings.recaptcha && settings.recaptcha.siteKeyV3 + script( + type='text/javascript' + nonce=scriptNonce + src='https://www.recaptcha.net/recaptcha/api.js?render=' + settings.recaptcha.siteKeyV3 + defer=deferScripts + ) - if (typeof suppressSkipToContent == "undefined") - a(class="skip-to-content" href="#main-content") #{translate('skip_to_content')} + if typeof suppressSkipToContent == 'undefined' + a(class='skip-to-content' href='#main-content') #{translate('skip_to_content')} block body - if (settings.devToolbar.enabled) - div#dev-toolbar + if settings.devToolbar.enabled + #dev-toolbar block foot-scripts +foot-scripts include _customer_io - script(type="text/javascript", nonce=scriptNonce). - window.addEventListener('DOMContentLoaded', function() { + script(type='text/javascript' nonce=scriptNonce). + window.addEventListener('DOMContentLoaded', function () { //- 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) { + 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"; + window.location.search += '&nocdn=true' } }) diff --git a/services/web/app/views/layout-marketing.pug b/services/web/app/views/layout-marketing.pug index 20126beda3..b54c30f033 100644 --- a/services/web/app/views/layout-marketing.pug +++ b/services/web/app/views/layout-marketing.pug @@ -7,7 +7,7 @@ block entrypointVar - entrypoint = 'marketing' block body - if (typeof suppressNavbar === "undefined") + if typeof suppressNavbar === 'undefined' if bootstrapVersion === 5 include layout/navbar-marketing-bootstrap-5 else @@ -15,7 +15,7 @@ block body block content - if (typeof suppressFooter === "undefined") + if typeof suppressFooter === 'undefined' if showThinFooter if bootstrapVersion === 5 include layout/thin-footer-bootstrap-5 @@ -24,13 +24,13 @@ block body else include layout/fat-footer - if (typeof(suppressCookieBanner) == 'undefined') + if typeof suppressCookieBanner == 'undefined' include _cookie_banner if bootstrapVersion === 5 - != moduleIncludes("contactModal-marketing-bootstrap-5", locals) + != moduleIncludes('contactModal-marketing-bootstrap-5', locals) else - != moduleIncludes("contactModal-marketing", locals) + != moduleIncludes('contactModal-marketing', locals) block prepend foot-scripts +bootstrap-js(bootstrapVersion) diff --git a/services/web/app/views/layout-react.pug b/services/web/app/views/layout-react.pug index be875b29f8..94ff3ba247 100644 --- a/services/web/app/views/layout-react.pug +++ b/services/web/app/views/layout-react.pug @@ -7,10 +7,10 @@ include ./_mixins/bootstrap_js block entrypointVar - entrypoint = 'marketing' - + block isApplicationPageVar - isApplicationPage = true - + block append meta - const canDisplayAdminMenu = hasAdminAccess() - const canDisplayAdminRedirect = canRedirectToAdminDomain() @@ -22,44 +22,52 @@ block append meta - const enableUpgradeButton = projectDashboardReact && usersBestSubscription && (usersBestSubscription.type === 'free' || usersBestSubscription.type === 'standalone-ai-add-on') - const showSignUpLink = hasFeature('registration-page') - meta(name="ol-navbar" data-type="json" content={ - customLogo: settings.nav.custom_logo, - title: nav.title, - canDisplayAdminMenu, - canDisplayAdminRedirect, - canDisplaySplitTestMenu, - canDisplaySurveyMenu, - canDisplayScriptLogMenu, - enableUpgradeButton, - suppressNavbarRight: !!suppressNavbarRight, - suppressNavContentLinks: !!suppressNavContentLinks, - showSubscriptionLink: nav.showSubscriptionLink, - showSignUpLink: showSignUpLink, - currentUrl: currentUrl, - sessionUser: sessionUser ? { email: sessionUser.email} : undefined, - adminUrl: settings.adminUrl, - items: cloneAndTranslateText(nav.header_extras) - }) - meta(name="ol-footer" data-type="json" content={ - showThinFooter: showThinFooter, - showPoweredBy: !hasFeature('saas') && !settings.nav.hide_powered_by, - subdomainLang: settings.i18n.subdomainLang, - translatedLanguages: settings.translatedLanguages, - leftItems: cloneAndTranslateText(settings.nav.left_footer), - rightItems: settings.nav.right_footer - }) + meta( + name='ol-navbar' + data-type='json' + content={ + customLogo: settings.nav.custom_logo, + title: nav.title, + canDisplayAdminMenu, + canDisplayAdminRedirect, + canDisplaySplitTestMenu, + canDisplaySurveyMenu, + canDisplayScriptLogMenu, + enableUpgradeButton, + suppressNavbarRight: !!suppressNavbarRight, + suppressNavContentLinks: !!suppressNavContentLinks, + showSubscriptionLink: nav.showSubscriptionLink, + showSignUpLink: showSignUpLink, + currentUrl: currentUrl, + sessionUser: sessionUser ? {email: sessionUser.email} : undefined, + adminUrl: settings.adminUrl, + items: cloneAndTranslateText(nav.header_extras), + } + ) + meta( + name='ol-footer' + data-type='json' + content={ + showThinFooter: showThinFooter, + showPoweredBy: !hasFeature('saas') && !settings.nav.hide_powered_by, + subdomainLang: settings.i18n.subdomainLang, + translatedLanguages: settings.translatedLanguages, + leftItems: cloneAndTranslateText(settings.nav.left_footer), + rightItems: settings.nav.right_footer, + } + ) block body - if (typeof suppressNavbar === "undefined") + if typeof suppressNavbar === 'undefined' include layout/navbar-marketing-react-bootstrap-5 block content - if (typeof suppressFooter === "undefined") + if typeof suppressFooter === 'undefined' if showThinFooter include layout/thin-footer-bootstrap-5 else include layout/fat-footer-react-bootstrap-5 - if (typeof suppressCookieBanner === "undefined") + if typeof suppressCookieBanner === 'undefined' include _cookie_banner diff --git a/services/web/app/views/layout-website-redesign.pug b/services/web/app/views/layout-website-redesign.pug index d04d4b1202..5d7bce6c4d 100644 --- a/services/web/app/views/layout-website-redesign.pug +++ b/services/web/app/views/layout-website-redesign.pug @@ -7,7 +7,7 @@ block entrypointVar - entrypoint = 'marketing' block body - if (typeof(suppressNavbar) == "undefined") + if typeof suppressNavbar == 'undefined' if bootstrapVersion === 5 include layout/navbar-marketing-bootstrap-5 else @@ -17,23 +17,23 @@ block body //- bootstrapVersion needed here, because plans.pug uses both BS version //- If the `plans-page-bs5` split test has been completed, remove bootstrapVersion logic - if (typeof(suppressFooter) == "undefined") + if typeof suppressFooter == 'undefined' if showThinFooter if bootstrapVersion === 5 include layout/thin-footer-bootstrap-5 - else + else include layout/thin-footer else include layout/fat-footer-website-redesign - if (typeof(suppressCookieBanner) == 'undefined') + if typeof suppressCookieBanner == 'undefined' include _cookie_banner block contactModal if bootstrapVersion === 5 - != moduleIncludes("contactModal-marketing-bootstrap-5", locals) + != moduleIncludes('contactModal-marketing-bootstrap-5', locals) else - != moduleIncludes("contactModal-marketing", locals) + != moduleIncludes('contactModal-marketing', locals) block prepend foot-scripts +bootstrap-js(bootstrapVersion) diff --git a/services/web/app/views/layout/fat-footer-base.pug b/services/web/app/views/layout/fat-footer-base.pug index 2e3dd2074f..e8380939f6 100644 --- a/services/web/app/views/layout/fat-footer-base.pug +++ b/services/web/app/views/layout/fat-footer-base.pug @@ -1,9 +1,9 @@ .fat-footer-base .fat-footer-base-section.fat-footer-base-meta - .fat-footer-base-item + .fat-footer-base-item .fat-footer-base-copyright © #{new Date().getFullYear()} Overleaf - a(href="/legal") #{translate('privacy_and_terms')} - a(href="https://www.digital-science.com/security-certifications/") #{translate('compliance')} + a(href='/legal') #{translate('privacy_and_terms')} + a(href='https://www.digital-science.com/security-certifications/') #{translate('compliance')} ul.fat-footer-base-item.list-unstyled.fat-footer-base-language if bootstrapVersion === 5 include language-picker-bootstrap-5 @@ -11,22 +11,53 @@ include language-picker .fat-footer-base-section.fat-footer-base-social .fat-footer-base-item - a.fat-footer-social.x-logo(href="https://x.com/overleaf") - svg(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1227" height="25") - path(d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z") + a.fat-footer-social.x-logo(href='https://x.com/overleaf') + svg( + xmlns='http://www.w3.org/2000/svg' + viewBox='0 0 1200 1227' + height='25' + ) + path( + d='M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z' + ) span.visually-hidden #{translate("app_on_x", {social: "X"})} - a.fat-footer-social.facebook-logo(href="https://www.facebook.com/overleaf.editor") - svg(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 666.66668 666.66717" height="25") + a.fat-footer-social.facebook-logo( + href='https://www.facebook.com/overleaf.editor' + ) + svg( + xmlns='http://www.w3.org/2000/svg' + viewBox='0 0 666.66668 666.66717' + height='25' + ) defs - clipPath(id="a" clipPathUnits="userSpaceOnUse") - path(d="M0 700h700V0H0Z") - g(clip-path="url(#a)" transform="matrix(1.33333 0 0 -1.33333 -133.333 800)") - path.background(d="M0 0c0 138.071-111.929 250-250 250S-500 138.071-500 0c0-117.245 80.715-215.622 189.606-242.638v166.242h-51.552V0h51.552v32.919c0 85.092 38.508 124.532 122.048 124.532 15.838 0 43.167-3.105 54.347-6.211V81.986c-5.901.621-16.149.932-28.882.932-40.993 0-56.832-15.528-56.832-55.9V0h81.659l-14.028-76.396h-67.631v-171.773C-95.927-233.218 0-127.818 0 0" fill="#0866ff" transform="translate(600 350)") - path.text(d="m0 0 14.029 76.396H-67.63v27.019c0 40.372 15.838 55.899 56.831 55.899 12.733 0 22.981-.31 28.882-.931v69.253c-11.18 3.106-38.509 6.212-54.347 6.212-83.539 0-122.048-39.441-122.048-124.533V76.396h-51.552V0h51.552v-166.242a250.559 250.559 0 0 1 60.394-7.362c10.254 0 20.358.632 30.288 1.831V0Z" fill="#fff" transform="translate(447.918 273.604)") + clipPath(id='a' clipPathUnits='userSpaceOnUse') + path(d='M0 700h700V0H0Z') + g( + clip-path='url(#a)' + transform='matrix(1.33333 0 0 -1.33333 -133.333 800)' + ) + path.background( + d='M0 0c0 138.071-111.929 250-250 250S-500 138.071-500 0c0-117.245 80.715-215.622 189.606-242.638v166.242h-51.552V0h51.552v32.919c0 85.092 38.508 124.532 122.048 124.532 15.838 0 43.167-3.105 54.347-6.211V81.986c-5.901.621-16.149.932-28.882.932-40.993 0-56.832-15.528-56.832-55.9V0h81.659l-14.028-76.396h-67.631v-171.773C-95.927-233.218 0-127.818 0 0' + fill='#0866ff' + transform='translate(600 350)' + ) + path.text( + d='m0 0 14.029 76.396H-67.63v27.019c0 40.372 15.838 55.899 56.831 55.899 12.733 0 22.981-.31 28.882-.931v69.253c-11.18 3.106-38.509 6.212-54.347 6.212-83.539 0-122.048-39.441-122.048-124.533V76.396h-51.552V0h51.552v-166.242a250.559 250.559 0 0 1 60.394-7.362c10.254 0 20.358.632 30.288 1.831V0Z' + fill='#fff' + transform='translate(447.918 273.604)' + ) span.visually-hidden #{translate("app_on_x", {social: "Facebook"})} - a.fat-footer-social.linkedin-logo(href="https://www.linkedin.com/company/writelatex-limited") - svg(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72" height="25") - g(fill="none" fill-rule="evenodd") - path.background(fill="#0B66C3" d="M8 72h56a8 8 0 0 0 8-8V8a8 8 0 0 0-8-8H8a8 8 0 0 0-8 8v56a8 8 0 0 0 8 8") - path.text(fill="#FFF" d="M62 62H51.316V43.802c0-4.99-1.896-7.777-5.845-7.777-4.296 0-6.54 2.901-6.54 7.777V62H28.632V27.333H38.93v4.67s3.096-5.729 10.453-5.729c7.353 0 12.617 4.49 12.617 13.777zM16.35 22.794c-3.508 0-6.35-2.864-6.35-6.397C10 12.864 12.842 10 16.35 10c3.507 0 6.347 2.864 6.347 6.397 0 3.533-2.84 6.397-6.348 6.397ZM11.032 62h10.736V27.333H11.033V62") + a.fat-footer-social.linkedin-logo( + href='https://www.linkedin.com/company/writelatex-limited' + ) + svg(xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 72' height='25') + g(fill='none' fill-rule='evenodd') + path.background( + fill='#0B66C3' + d='M8 72h56a8 8 0 0 0 8-8V8a8 8 0 0 0-8-8H8a8 8 0 0 0-8 8v56a8 8 0 0 0 8 8' + ) + path.text( + fill='#FFF' + d='M62 62H51.316V43.802c0-4.99-1.896-7.777-5.845-7.777-4.296 0-6.54 2.901-6.54 7.777V62H28.632V27.333H38.93v4.67s3.096-5.729 10.453-5.729c7.353 0 12.617 4.49 12.617 13.777zM16.35 22.794c-3.508 0-6.35-2.864-6.35-6.397C10 12.864 12.842 10 16.35 10c3.507 0 6.347 2.864 6.347 6.397 0 3.533-2.84 6.397-6.348 6.397ZM11.032 62h10.736V27.333H11.033V62' + ) span.visually-hidden #{translate("app_on_x", {social: "LinkedIn"})} diff --git a/services/web/app/views/layout/fat-footer-website-redesign.pug b/services/web/app/views/layout/fat-footer-website-redesign.pug index cd68e1daf6..ba6286a296 100644 --- a/services/web/app/views/layout/fat-footer-website-redesign.pug +++ b/services/web/app/views/layout/fat-footer-website-redesign.pug @@ -1,84 +1,87 @@ footer.fat-footer.hidden-print.website-redesign-fat-footer - .fat-footer-container(role="navigation" aria-label=translate('footer_navigation')) + .fat-footer-container( + role='navigation' + aria-label=translate('footer_navigation') + ) .fat-footer-sections(class=hideFatFooter ? 'hidden' : undefined) - .footer-section#footer-brand - a(href='/', aria-label=settings.appName).footer-brand - + #footer-brand.footer-section + a.footer-brand(href='/' aria-label=settings.appName) + .footer-section h2.footer-section-heading #{translate('About')} - + ul.list-unstyled li - a(href="/about") #{translate('footer_about_us')} + a(href='/about') #{translate('footer_about_us')} li - a(href="/about/values") #{translate('our_values')} + a(href='/about/values') #{translate('our_values')} li - a(href="https://digitalscience.pinpointhq.com/") #{translate('careers')} + a(href='https://digitalscience.pinpointhq.com/') #{translate('careers')} li - a(href="/for/press") !{translate('press_and_awards')} + a(href='/for/press') !{translate('press_and_awards')} li - a(href="/blog") #{translate('blog')} - + a(href='/blog') #{translate('blog')} + .footer-section h2.footer-section-heading #{translate('learn')} - + ul.list-unstyled li - a(href="/learn/latex/Learn_LaTeX_in_30_minutes") #{translate('latex_in_thirty_minutes')} + a(href='/learn/latex/Learn_LaTeX_in_30_minutes') #{translate('latex_in_thirty_minutes')} li - a(href="/latex/templates") #{translate('templates')} + a(href='/latex/templates') #{translate('templates')} li - a(href="/events/webinars") #{translate('webinars')} + a(href='/events/webinars') #{translate('webinars')} li - a(href="/learn/latex/Tutorials") #{translate('tutorials')} + a(href='/learn/latex/Tutorials') #{translate('tutorials')} li - a(href="/learn/latex/Inserting_Images") #{translate('how_to_insert_images')} + a(href='/learn/latex/Inserting_Images') #{translate('how_to_insert_images')} li - a(href="/learn/latex/Tables") #{translate('how_to_create_tables')} - + a(href='/learn/latex/Tables') #{translate('how_to_create_tables')} + .footer-section h2.footer-section-heading !{translate('footer_plans_and_pricing')} - + ul.list-unstyled li - a(href="/learn/how-to/Overleaf_premium_features") #{translate('premium_features')} + a(href='/learn/how-to/Overleaf_premium_features') #{translate('premium_features')} li - a(href="/user/subscription/plans?itm_referrer=footer-for-indv-groups") !{translate('for_individuals_and_groups')} + a(href='/user/subscription/plans?itm_referrer=footer-for-indv-groups') !{translate('for_individuals_and_groups')} li - a(href="/for/enterprises") #{translate('for_business')} + a(href='/for/enterprises') #{translate('for_business')} li - a(href="/for/universities") #{translate('for_universities')} + a(href='/for/universities') #{translate('for_universities')} li a( data-ol-for-students-link - href="/user/subscription/plans?itm_referrer=footer-for-students#student-annual" + href='/user/subscription/plans?itm_referrer=footer-for-students#student-annual' ) #{translate('for_students')} li - a(href="/for/government") #{translate('for_government')} - + a(href='/for/government') #{translate('for_government')} + .footer-section h2.footer-section-heading #{translate('get_involved')} - + ul.list-unstyled li - a(href="/for/community/advisors") #{translate('become_an_advisor')} + a(href='/for/community/advisors') #{translate('become_an_advisor')} li - a(href="https://forms.gle/67PSpN1bLnjGCmPQ9") #{translate('let_us_know_what_you_think')} + a(href='https://forms.gle/67PSpN1bLnjGCmPQ9') #{translate('let_us_know_what_you_think')} if user li - a(href="/beta/participate") #{translate('join_beta_program')} - + a(href='/beta/participate') #{translate('join_beta_program')} + .footer-section h2.footer-section-heading #{translate('help')} - + ul.list-unstyled li - a(href="/about/why-latex") #{translate('why_latex')} + a(href='/about/why-latex') #{translate('why_latex')} li - a(href="/learn") #{translate('Documentation')} + a(href='/learn') #{translate('Documentation')} li - a(href="/contact") #{translate('footer_contact_us')} + a(href='/contact') #{translate('footer_contact_us')} li - a(href="https://status.overleaf.com/") #{translate('website_status')} - + a(href='https://status.overleaf.com/') #{translate('website_status')} + include fat-footer-base diff --git a/services/web/app/views/layout/fat-footer.pug b/services/web/app/views/layout/fat-footer.pug index d319a217cb..c7c19bd4a3 100644 --- a/services/web/app/views/layout/fat-footer.pug +++ b/services/web/app/views/layout/fat-footer.pug @@ -1,84 +1,87 @@ footer.fat-footer.hidden-print - .fat-footer-container(role="navigation" aria-label=translate('footer_navigation')) + .fat-footer-container( + role='navigation' + aria-label=translate('footer_navigation') + ) .fat-footer-sections(class=hideFatFooter ? 'hidden' : undefined) - .footer-section#footer-brand - a(href='/', aria-label=settings.appName).footer-brand - + #footer-brand.footer-section + a.footer-brand(href='/' aria-label=settings.appName) + .footer-section h2.footer-section-heading #{translate('About')} - + ul.list-unstyled li - a(href="/about") #{translate('footer_about_us')} + a(href='/about') #{translate('footer_about_us')} li - a(href="/about/values") #{translate('our_values')} + a(href='/about/values') #{translate('our_values')} li - a(href="https://digitalscience.pinpointhq.com/") #{translate('careers')} + a(href='https://digitalscience.pinpointhq.com/') #{translate('careers')} li - a(href="/for/press") !{translate('press_and_awards')} + a(href='/for/press') !{translate('press_and_awards')} li - a(href="/blog") #{translate('blog')} - + a(href='/blog') #{translate('blog')} + .footer-section h2.footer-section-heading #{translate('learn')} - + ul.list-unstyled li - a(href="/learn/latex/Learn_LaTeX_in_30_minutes") #{translate('latex_in_thirty_minutes')} + a(href='/learn/latex/Learn_LaTeX_in_30_minutes') #{translate('latex_in_thirty_minutes')} li - a(href="/latex/templates") #{translate('templates')} + a(href='/latex/templates') #{translate('templates')} li - a(href="/events/webinars") #{translate('webinars')} + a(href='/events/webinars') #{translate('webinars')} li - a(href="/learn/latex/Tutorials") #{translate('tutorials')} + a(href='/learn/latex/Tutorials') #{translate('tutorials')} li - a(href="/learn/latex/Inserting_Images") #{translate('how_to_insert_images')} + a(href='/learn/latex/Inserting_Images') #{translate('how_to_insert_images')} li - a(href="/learn/latex/Tables") #{translate('how_to_create_tables')} - + a(href='/learn/latex/Tables') #{translate('how_to_create_tables')} + .footer-section h2.footer-section-heading !{translate('footer_plans_and_pricing')} - + ul.list-unstyled li - a(href="/learn/how-to/Overleaf_premium_features") #{translate('premium_features')} + a(href='/learn/how-to/Overleaf_premium_features') #{translate('premium_features')} li - a(href="/user/subscription/plans?itm_referrer=footer-for-indv-groups") !{translate('for_individuals_and_groups')} + a(href='/user/subscription/plans?itm_referrer=footer-for-indv-groups') !{translate('for_individuals_and_groups')} li - a(href="/for/enterprises") #{translate('for_enterprise')} + a(href='/for/enterprises') #{translate('for_enterprise')} li - a(href="/for/universities") #{translate('for_universities')} + a(href='/for/universities') #{translate('for_universities')} li a( data-ol-for-students-link - href="/user/subscription/plans?itm_referrer=footer-for-students#student-annual" + href='/user/subscription/plans?itm_referrer=footer-for-students#student-annual' ) #{translate('for_students')} li - a(href="/for/government") #{translate('for_government')} - + a(href='/for/government') #{translate('for_government')} + .footer-section h2.footer-section-heading #{translate('get_involved')} - + ul.list-unstyled li - a(href="/for/community/advisors") #{translate('become_an_advisor')} + a(href='/for/community/advisors') #{translate('become_an_advisor')} li - a(href="https://forms.gle/67PSpN1bLnjGCmPQ9") #{translate('let_us_know_what_you_think')} + a(href='https://forms.gle/67PSpN1bLnjGCmPQ9') #{translate('let_us_know_what_you_think')} if user li - a(href="/beta/participate") #{translate('join_beta_program')} - + a(href='/beta/participate') #{translate('join_beta_program')} + .footer-section h2.footer-section-heading #{translate('help')} - + ul.list-unstyled li - a(href="/about/why-latex") #{translate('why_latex')} + a(href='/about/why-latex') #{translate('why_latex')} li - a(href="/learn") #{translate('Documentation')} + a(href='/learn') #{translate('Documentation')} li - a(href="/contact") #{translate('footer_contact_us')} + a(href='/contact') #{translate('footer_contact_us')} li - a(href="https://status.overleaf.com/") #{translate('website_status')} - + a(href='https://status.overleaf.com/') #{translate('website_status')} + include fat-footer-base diff --git a/services/web/app/views/layout/language-picker-bootstrap-5.pug b/services/web/app/views/layout/language-picker-bootstrap-5.pug index 44997a8ca4..09f8f58c7d 100644 --- a/services/web/app/views/layout/language-picker-bootstrap-5.pug +++ b/services/web/app/views/layout/language-picker-bootstrap-5.pug @@ -1,27 +1,35 @@ include ../_mixins/material_symbol -li.dropdown.dropup.subdued(dropdown).language-picker +li.dropdown.dropup.subdued.language-picker(dropdown) button#language-picker-toggle.btn.btn-link.btn-inline-link( - dropdown-toggle, - data-ol-lang-selector-tooltip, - data-bs-toggle="dropdown", - aria-haspopup="true", - aria-expanded="false", - aria-label="Select " + translate('language'), + dropdown-toggle + data-ol-lang-selector-tooltip + data-bs-toggle='dropdown' + aria-haspopup='true' + aria-expanded='false' + aria-label='Select ' + translate('language') tooltip=translate('language') title=translate('language') ) - +material-symbol("translate") + +material-symbol('translate') |   span.language-picker-text #{settings.translatedLanguages[currentLngCode]} - ul.dropdown-menu.dropdown-menu-sm-width(role="menu" aria-labelledby="language-picker-toggle") + ul.dropdown-menu.dropdown-menu-sm-width( + role='menu' + aria-labelledby='language-picker-toggle' + ) li.dropdown-header #{translate("language")} each subdomainDetails, subdomain in settings.i18n.subdomainLang if !subdomainDetails.hide - let isActive = subdomainDetails.lngCode === currentLngCode li.lng-option - a.menu-indent(href=subdomainDetails.url+currentUrlWithQueryParams, role="menuitem", class=isActive ? 'dropdown-item active' : 'dropdown-item', aria-selected=isActive ? 'true' : 'false') + a.menu-indent( + href=subdomainDetails.url + currentUrlWithQueryParams + role='menuitem' + class=isActive ? 'dropdown-item active' : 'dropdown-item' + aria-selected=isActive ? 'true' : 'false' + ) | #{settings.translatedLanguages[subdomainDetails.lngCode]} if subdomainDetails.lngCode === currentLngCode - +material-symbol("check", "dropdown-item-trailing-icon") + +material-symbol('check', 'dropdown-item-trailing-icon') diff --git a/services/web/app/views/layout/language-picker.pug b/services/web/app/views/layout/language-picker.pug index d26d8a8bf7..e88ff716ec 100644 --- a/services/web/app/views/layout/language-picker.pug +++ b/services/web/app/views/layout/language-picker.pug @@ -1,13 +1,13 @@ -li.dropdown.dropup.subdued(dropdown).language-picker - a.dropdown-toggle#language-picker-toggle( - href="#", - dropdown-toggle, - data-ol-lang-selector-tooltip, - data-toggle="dropdown", - role="button" - aria-haspopup="true", - aria-expanded="false", - aria-label="Select " + translate('language'), +li.dropdown.dropup.subdued.language-picker(dropdown) + a#language-picker-toggle.dropdown-toggle( + href='#' + dropdown-toggle + data-ol-lang-selector-tooltip + data-toggle='dropdown' + role='button' + aria-haspopup='true' + aria-expanded='false' + aria-label='Select ' + translate('language') tooltip=translate('language') title=translate('language') ) @@ -15,10 +15,13 @@ li.dropdown.dropup.subdued(dropdown).language-picker | | #{settings.translatedLanguages[currentLngCode]} - ul.dropdown-menu(role="menu" aria-labelledby="language-picker-toggle") + ul.dropdown-menu(role='menu' aria-labelledby='language-picker-toggle') li.dropdown-header #{translate("language")} each subdomainDetails, subdomain in settings.i18n.subdomainLang if !subdomainDetails.hide li.lng-option - a.menu-indent(href=subdomainDetails.url+currentUrlWithQueryParams role="menuitem") + a.menu-indent( + href=subdomainDetails.url + currentUrlWithQueryParams + role='menuitem' + ) | #{settings.translatedLanguages[subdomainDetails.lngCode]} diff --git a/services/web/app/views/layout/layout-no-js.pug b/services/web/app/views/layout/layout-no-js.pug index b5bf3cc434..76a31b72a5 100644 --- a/services/web/app/views/layout/layout-no-js.pug +++ b/services/web/app/views/layout/layout-no-js.pug @@ -1,18 +1,20 @@ doctype html -html(lang="en") - +html(lang='en') - metadata = metadata || {} block vars head - if (metadata && metadata.title) + 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") + meta( + name='viewport' + content='width=device-width, initial-scale=1.0, user-scalable=yes' + ) - link(rel="icon", href="/favicon.ico") + link(rel='icon' href='/favicon.ico') if buildCssPath - link(rel="stylesheet", href=buildCssPath('', 5)) + link(rel='stylesheet' href=buildCssPath('', 5)) block body diff --git a/services/web/app/views/layout/navbar-marketing-bootstrap-5.pug b/services/web/app/views/layout/navbar-marketing-bootstrap-5.pug index c581ab29ce..29b12a056d 100644 --- a/services/web/app/views/layout/navbar-marketing-bootstrap-5.pug +++ b/services/web/app/views/layout/navbar-marketing-bootstrap-5.pug @@ -1,26 +1,32 @@ include ../_mixins/navbar -nav.navbar.navbar-default.navbar-main.navbar-expand-lg(class={ - 'website-redesign-navbar': isWebsiteRedesign -}) +nav.navbar.navbar-default.navbar-main.navbar-expand-lg( + class={ + 'website-redesign-navbar': isWebsiteRedesign, + } +) .container-fluid.navbar-container .navbar-header 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).navbar-title #{nav.title} + a.navbar-brand( + href='/' + aria-label=settings.appName + style='background-image:url("' + settings.nav.custom_logo + '")' + ) + else if nav.title + a.navbar-title(href='/' aria-label=settings.appName) #{nav.title} else - a(href='/', aria-label=settings.appName).navbar-brand + a.navbar-brand(href='/' aria-label=settings.appName) - var enableUpgradeButton = projectDashboardReact && usersBestSubscription && (usersBestSubscription.type === 'free' || usersBestSubscription.type === 'standalone-ai-add-on') - if (enableUpgradeButton) + if enableUpgradeButton a.btn.btn-primary.me-2.d-md-none( - href="/user/subscription/plans" - event-tracking="upgrade-button-click" - event-tracking-mb="true" - event-tracking-label="upgrade" - event-tracking-trigger="click" - event-segmentation='{"source": "dashboard-top", "project-dashboard-react": "enabled", "is-dashboard-sidebar-hidden": "true", "is-screen-width-less-than-768px": "true"}' + href='/user/subscription/plans' + event-tracking='upgrade-button-click' + event-tracking-mb='true' + event-tracking-label='upgrade' + event-tracking-trigger='click' + event-segmentation={source: 'dashboard-top', projectDashboardReact: 'enabled', isDashboardSidebarHidden: 'true', isScreenWidthLessThan768px: 'true'} ) #{translate("upgrade")} - var canDisplayAdminMenu = hasAdminAccess() @@ -29,45 +35,45 @@ nav.navbar.navbar-default.navbar-main.navbar-expand-lg(class={ - var canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu - var canDisplayScriptLogMenu = hasFeature('saas') && canDisplayAdminMenu - if (typeof suppressNavbarRight === "undefined") + if typeof suppressNavbarRight === 'undefined' button.navbar-toggler.collapsed( - type="button", - data-bs-toggle="collapse", - data-bs-target="#navbar-main-collapse" - aria-controls="navbar-main-collapse" - aria-expanded="false" - aria-label="Toggle " + translate('navigation') + type='button' + data-bs-toggle='collapse' + data-bs-target='#navbar-main-collapse' + aria-controls='navbar-main-collapse' + aria-expanded='false' + aria-label='Toggle ' + translate('navigation') ) - i.fa.fa-bars(aria-hidden="true") + i.fa.fa-bars(aria-hidden='true') - .navbar-collapse.collapse#navbar-main-collapse - ul.nav.navbar-nav.navbar-right.ms-auto(role="menubar") - if (canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu) + #navbar-main-collapse.navbar-collapse.collapse + ul.nav.navbar-nav.navbar-right.ms-auto(role='menubar') + if canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu +nav-item.dropdown.subdued button.dropdown-toggle( - aria-haspopup="true", - aria-expanded="false", - data-bs-toggle="dropdown" - role="menuitem" - event-tracking="menu-expand" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={"item": "admin", "location": "top-menu"} + aria-haspopup='true' + aria-expanded='false' + data-bs-toggle='dropdown' + role='menuitem' + event-tracking='menu-expand' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={item: 'admin', location: 'top-menu'} ) | Admin +dropdown-menu.dropdown-menu-end if canDisplayAdminMenu - +dropdown-menu-link-item()(href="/admin") Manage Site - +dropdown-menu-link-item()(href="/admin/user") Manage Users - +dropdown-menu-link-item()(href="/admin/project") Project URL Lookup + +dropdown-menu-link-item(href='/admin') Manage Site + +dropdown-menu-link-item(href='/admin/user') Manage Users + +dropdown-menu-link-item(href='/admin/project') Project URL Lookup if canDisplayAdminRedirect - +dropdown-menu-link-item()(href=settings.adminUrl) Switch to Admin + +dropdown-menu-link-item(href=settings.adminUrl) Switch to Admin if canDisplaySplitTestMenu - +dropdown-menu-link-item()(href="/admin/split-test") Manage Feature Flags + +dropdown-menu-link-item(href='/admin/split-test') Manage Feature Flags if canDisplaySurveyMenu - +dropdown-menu-link-item()(href="/admin/survey") Manage Surveys + +dropdown-menu-link-item(href='/admin/survey') Manage Surveys if canDisplayScriptLogMenu - +dropdown-menu-link-item()(href="/admin/script-logs") View Script Logs + +dropdown-menu-link-item(href='/admin/script-logs') View Script Logs // loop over header_extras each item in nav.header_extras @@ -86,14 +92,14 @@ nav.navbar.navbar-default.navbar-main.navbar-expand-lg(class={ if item.dropdown +nav-item.dropdown(class=item.class) button.dropdown-toggle( - aria-haspopup="true", - aria-expanded="false", - data-bs-toggle="dropdown" - role="menuitem" - event-tracking="menu-expand" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={"item": item.trackingKey, "location": "top-menu"} + aria-haspopup='true' + aria-expanded='false' + data-bs-toggle='dropdown' + role='menuitem' + event-tracking='menu-expand' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={item: item.trackingKey, location: 'top-menu'} ) | !{translate(item.text)} +dropdown-menu.dropdown-menu-end @@ -101,31 +107,41 @@ nav.navbar.navbar-default.navbar-main.navbar-expand-lg(class={ if child.divider +dropdown-menu-divider else if child.isContactUs - +dropdown-menu-link-item()(data-ol-open-contact-form-modal="contact-us" data-bs-target="#contactUsModal" href data-bs-toggle="modal" event-tracking="menu-click" event-tracking-mb="true" event-tracking-trigger="click" event-segmentation={"item": "contact", "location": "top-menu"}) + +dropdown-menu-link-item( + data-ol-open-contact-form-modal='contact-us' + data-bs-target='#contactUsModal' + href + data-bs-toggle='modal' + event-tracking='menu-click' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={item: 'contact', location: 'top-menu'} + ) span | #{translate("contact_us")} else if child.url - +dropdown-menu-link-item()( - href=child.url, - class=child.class, - event-tracking="menu-click" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={ item: child.trackingKey, location: 'top-menu' } + +dropdown-menu-link-item( + href=child.url + class=child.class + event-tracking='menu-click' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={item: child.trackingKey, location: 'top-menu'} ) !{translate(child.text)} else - +dropdown-menu-item !{translate(child.text)} + +dropdown-menu-item + | !{translate(child.text)} else +nav-item(class=item.class) if item.url +nav-link( - href=item.url, - class=item.class, - event-tracking="menu-click" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={ item: item.trackingKey, location: 'top-menu' } + href=item.url + class=item.class + event-tracking='menu-click' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={item: item.trackingKey, location: 'top-menu'} ) !{translate(item.text)} else | !{translate(item.text)} @@ -136,48 +152,48 @@ nav.navbar.navbar-default.navbar-main.navbar-expand-lg(class={ if hasFeature('registration-page') +nav-item.primary +nav-link( - href="/register" - event-tracking="menu-click" - event-tracking-action="clicked" - event-tracking-trigger="click" - event-tracking-mb="true" - event-segmentation={ page: currentUrl, item: 'register', location: 'top-menu' } + href='/register' + event-tracking='menu-click' + event-tracking-action='clicked' + event-tracking-trigger='click' + event-tracking-mb='true' + event-segmentation={page: currentUrl, item: 'register', location: 'top-menu'} ) #{translate('sign_up')} // login link +nav-item +nav-link( - href="/login" - event-tracking="menu-click" - event-tracking-action="clicked" - event-tracking-trigger="click" - event-tracking-mb="true" - event-segmentation={ page: currentUrl, item: 'login', location: 'top-menu' } + href='/login' + event-tracking='menu-click' + event-tracking-action='clicked' + event-tracking-trigger='click' + event-tracking-mb='true' + event-segmentation={page: currentUrl, item: 'login', location: 'top-menu'} ) #{translate('log_in')} // projects link and account menu if getSessionUser() +nav-item - +nav-link(href="/project") #{translate('Projects')} + +nav-link(href='/project') #{translate('Projects')} +nav-item.dropdown button.dropdown-toggle( - aria-haspopup="true", - aria-expanded="false", - data-bs-toggle="dropdown" - role="menuitem" - event-tracking="menu-expand" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={"item": "account", "location": "top-menu"} + aria-haspopup='true' + aria-expanded='false' + data-bs-toggle='dropdown' + role='menuitem' + event-tracking='menu-expand' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={item: 'account', location: 'top-menu'} ) | #{translate('Account')} +dropdown-menu.dropdown-menu-end +dropdown-menu-item - div.disabled.dropdown-item #{getSessionUser().email} + .disabled.dropdown-item #{getSessionUser().email} +dropdown-menu-divider - +dropdown-menu-link-item()(href="/user/settings") #{translate('account_settings')} + +dropdown-menu-link-item(href='/user/settings') #{translate('account_settings')} if nav.showSubscriptionLink - +dropdown-menu-link-item()(href="/user/subscription") #{translate('subscription')} + +dropdown-menu-link-item(href='/user/subscription') #{translate('subscription')} +dropdown-menu-divider +dropdown-menu-item //- @@ -185,14 +201,10 @@ nav.navbar.navbar-default.navbar-main.navbar-expand-lg(class={ this is that if the button is inside the form, screen readers will not count it in the total number of menu items. button.btn-link.text-left.dropdown-menu-button.dropdown-item( - role="menuitem", - tabindex="-1" - form="logOutForm" + role='menuitem' + tabindex='-1' + form='logOutForm' ) | #{translate('log_out')} - form( - method="POST", - action="/logout", - id="logOutForm" - ) - input(name='_csrf', type='hidden', value=csrfToken) + form(method='POST' action='/logout' id='logOutForm') + input(name='_csrf' type='hidden' value=csrfToken) diff --git a/services/web/app/views/layout/navbar-marketing.pug b/services/web/app/views/layout/navbar-marketing.pug index c5e9f2e0bf..4d374bd8ad 100644 --- a/services/web/app/views/layout/navbar-marketing.pug +++ b/services/web/app/views/layout/navbar-marketing.pug @@ -1,32 +1,44 @@ -nav.navbar.navbar-default.navbar-main(class={ - 'website-redesign-navbar': isWebsiteRedesign -}) +nav.navbar.navbar-default.navbar-main( + class={ + 'website-redesign-navbar': isWebsiteRedesign, + } +) .container-fluid .navbar-header - if (typeof(suppressNavbarRight) == "undefined") + if typeof suppressNavbarRight == 'undefined' button.navbar-toggle.collapsed( - type="button", - data-toggle="collapse", - data-target="#navbar-main-collapse" - aria-label="Toggle " + translate('navigation') + type='button' + data-toggle='collapse' + data-target='#navbar-main-collapse' + aria-label='Toggle ' + translate('navigation') ) - i.fa.fa-bars(aria-hidden="true") + i.fa.fa-bars(aria-hidden='true') - var enableUpgradeButton = projectDashboardReact && usersBestSubscription && (usersBestSubscription.type === 'free' || usersBestSubscription.type === 'standalone-ai-add-on') - if (enableUpgradeButton) + if enableUpgradeButton + //- prettier-ignore a.btn.btn-primary.pull-right.me-2.visible-xs( - href="/user/subscription/plans" - event-tracking="upgrade-button-click" - event-tracking-mb="true" - event-tracking-label="upgrade" - event-tracking-trigger="click" - event-segmentation='{"source": "dashboard-top", "project-dashboard-react": "enabled", "is-dashboard-sidebar-hidden": "true", "is-screen-width-less-than-768px": "true"}' + href='/user/subscription/plans' + event-tracking='upgrade-button-click' + event-tracking-mb='true' + event-tracking-label='upgrade' + event-tracking-trigger='click' + event-segmentation={ + source: "dashboard-top", + "project-dashboard-react": "enabled", + "is-dashboard-sidebar-hidden": "true", + "is-screen-width-less-than-768px": "true" + } ) #{translate("upgrade")} 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).navbar-title #{nav.title} + a.navbar-brand( + href='/' + aria-label=settings.appName + style='background-image:url("' + settings.nav.custom_logo + '")' + ) + else if nav.title + a.navbar-title(href='/' aria-label=settings.appName) #{nav.title} else - a(href='/', aria-label=settings.appName).navbar-brand + a.navbar-brand(href='/' aria-label=settings.appName) - var canDisplayAdminMenu = hasAdminAccess() - var canDisplayAdminRedirect = canRedirectToAdminDomain() @@ -34,44 +46,44 @@ nav.navbar.navbar-default.navbar-main(class={ - var canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu - var canDisplayScriptLogMenu = hasFeature('saas') && canDisplayAdminMenu - if (typeof(suppressNavbarRight) == "undefined") - .navbar-collapse.collapse#navbar-main-collapse + if typeof suppressNavbarRight == 'undefined' + #navbar-main-collapse.navbar-collapse.collapse ul.nav.navbar-nav.navbar-right - if (canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu) + if canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu li.dropdown.subdued a.dropdown-toggle( - href="#", - role="button", - aria-haspopup="true", - aria-expanded="false", - data-toggle="dropdown" - event-tracking="menu-expand" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={"item": "admin", "location": "top-menu"} + href='#' + role='button' + aria-haspopup='true' + aria-expanded='false' + data-toggle='dropdown' + event-tracking='menu-expand' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={item: 'admin', location: 'top-menu'} ) | Admin span.caret ul.dropdown-menu if canDisplayAdminMenu li - a(href="/admin") Manage Site + a(href='/admin') Manage Site li - a(href="/admin/user") Manage Users + a(href='/admin/user') Manage Users li - a(href="/admin/project") Project URL Lookup + a(href='/admin/project') Project URL Lookup if canDisplayAdminRedirect li a(href=settings.adminUrl) Switch to Admin if canDisplaySplitTestMenu li - a(href="/admin/split-test") Manage Feature Flags + a(href='/admin/split-test') Manage Feature Flags if canDisplaySurveyMenu li - a(href="/admin/survey") Manage Surveys + a(href='/admin/survey') Manage Surveys if canDisplayScriptLogMenu li - a(href="/admin/script-logs") View Script Logs + a(href='/admin/script-logs') View Script Logs // loop over header_extras each item in nav.header_extras @@ -90,15 +102,15 @@ nav.navbar.navbar-default.navbar-main(class={ if item.dropdown li.dropdown(class=item.class) a.dropdown-toggle( - href="#", - role="button", - aria-haspopup="true", - aria-expanded="false", - data-toggle="dropdown" - event-tracking="menu-expand" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={"item": item.trackingKey, "location": "top-menu"} + href='#' + role='button' + aria-haspopup='true' + aria-expanded='false' + data-toggle='dropdown' + event-tracking='menu-expand' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={item: item.trackingKey, location: 'top-menu'} ) | !{translate(item.text)} span.caret @@ -108,18 +120,25 @@ nav.navbar.navbar-default.navbar-main(class={ li.divider else if child.isContactUs li - a(data-ol-open-contact-form-modal="contact-us" href event-tracking="menu-click" event-tracking-mb="true" event-tracking-trigger="click" event-segmentation={"item": "contact", "location": "top-menu"}) + a( + data-ol-open-contact-form-modal='contact-us' + href + event-tracking='menu-click' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={item: 'contact', location: 'top-menu'} + ) span | #{translate("contact_us")} else li if child.url a( - href=child.url, - class=child.class, - event-tracking="menu-click" - event-tracking-mb="true" - event-tracking-trigger="click" + href=child.url + class=child.class + event-tracking='menu-click' + event-tracking-mb='true' + event-tracking-trigger='click' event-segmentation={item: item.trackingKey, location: 'top-menu'} ) !{translate(child.text)} else @@ -128,12 +147,12 @@ nav.navbar.navbar-default.navbar-main(class={ li(class=item.class) if item.url a( - href=item.url, - class=item.class, - event-tracking="menu-click" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={ item: item.trackingKey, location: 'top-menu' } + href=item.url + class=item.class + event-tracking='menu-click' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={item: item.trackingKey, location: 'top-menu'} ) !{translate(item.text)} else | !{translate(item.text)} @@ -144,54 +163,54 @@ nav.navbar.navbar-default.navbar-main(class={ if hasFeature('registration-page') li.primary a( - href="/register" - event-tracking="menu-click" - event-tracking-action="clicked" - event-tracking-trigger="click" - event-tracking-mb="true" - event-segmentation={ page: currentUrl, item: 'register', location: 'top-menu' } + href='/register' + event-tracking='menu-click' + event-tracking-action='clicked' + event-tracking-trigger='click' + event-tracking-mb='true' + event-segmentation={page: currentUrl, item: 'register', location: 'top-menu'} ) #{translate('sign_up')} // login link li a( - href="/login" - event-tracking="menu-click" - event-tracking-action="clicked" - event-tracking-trigger="click" - event-tracking-mb="true" - event-segmentation={ page: currentUrl, item: 'login', location: 'top-menu' } + href='/login' + event-tracking='menu-click' + event-tracking-action='clicked' + event-tracking-trigger='click' + event-tracking-mb='true' + event-segmentation={page: currentUrl, item: 'login', location: 'top-menu'} ) #{translate('log_in')} // projects link and account menu if getSessionUser() li - a(href="/project") #{translate('Projects')} + a(href='/project') #{translate('Projects')} li.dropdown a.dropdown-toggle( - href="#", - role="button", - aria-haspopup="true", - aria-expanded="false", - data-toggle="dropdown" - event-tracking="menu-expand" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={"item": "account", "location": "top-menu"} + href='#' + role='button' + aria-haspopup='true' + aria-expanded='false' + data-toggle='dropdown' + event-tracking='menu-expand' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={item: 'account', location: 'top-menu'} ) | #{translate('Account')} span.caret ul.dropdown-menu li - div.subdued #{getSessionUser().email} + .subdued #{getSessionUser().email} li.divider.hidden-xs.hidden-sm li - a(href="/user/settings") #{translate('account_settings')} + a(href='/user/settings') #{translate('account_settings')} if nav.showSubscriptionLink li - a(href="/user/subscription") #{translate('subscription')} + 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) + 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/layout/navbar-website-redesign.pug b/services/web/app/views/layout/navbar-website-redesign.pug index 8ea71861c0..0a8337c2f5 100644 --- a/services/web/app/views/layout/navbar-website-redesign.pug +++ b/services/web/app/views/layout/navbar-website-redesign.pug @@ -1,30 +1,39 @@ nav.navbar.navbar-default.navbar-main.website-redesign-navbar .container-fluid .navbar-header - if (typeof(suppressNavbarRight) == "undefined") + if typeof suppressNavbarRight == 'undefined' button.navbar-toggle.collapsed( - type="button", - data-toggle="collapse", - data-target="#navbar-main-collapse" - aria-label="Toggle " + translate('navigation') + type='button' + data-toggle='collapse' + data-target='#navbar-main-collapse' + aria-label='Toggle ' + translate('navigation') ) - i.fa.fa-bars(aria-hidden="true") + i.fa.fa-bars(aria-hidden='true') - var enableUpgradeButton = projectDashboardReact && usersBestSubscription && (usersBestSubscription.type === 'free' || usersBestSubscription.type === 'standalone-ai-add-on') - if (enableUpgradeButton) + if enableUpgradeButton a.btn.btn-primary.pull-right.me-2.visible-xs( - href="/user/subscription/plans" - event-tracking="upgrade-button-click" - event-tracking-mb="true" - event-tracking-label="upgrade" - event-tracking-trigger="click" - event-segmentation='{"source": "dashboard-top", "project-dashboard-react": "enabled", "is-dashboard-sidebar-hidden": "true", "is-screen-width-less-than-768px": "true"}' + href='/user/subscription/plans' + event-tracking='upgrade-button-click' + event-tracking-mb='true' + event-tracking-label='upgrade' + event-tracking-trigger='click' + event-segmentation={ + source: 'dashboard-top', + projectDashboardReact: 'enabled', + isDashboardSidebarHidden: 'true', + isScreenWidthLessThan768px: 'true', + } ) #{translate("upgrade")} 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).navbar-title #{nav.title} + a.navbar-brand( + href='/' + aria-label=settings.appName + style='background-image:url("' + settings.nav.custom_logo + '")' + ) + else if nav.title + a.navbar-title(href='/' aria-label=settings.appName) #{nav.title} else - a(href='/', aria-label=settings.appName).navbar-brand + a.navbar-brand(href='/' aria-label=settings.appName) - var canDisplayAdminMenu = hasAdminAccess() - var canDisplayAdminRedirect = canRedirectToAdminDomain() @@ -32,44 +41,44 @@ nav.navbar.navbar-default.navbar-main.website-redesign-navbar - var canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu - var canDisplayScriptLogMenu = hasFeature('saas') && canDisplayAdminMenu - if (typeof(suppressNavbarRight) == "undefined") - .navbar-collapse.collapse#navbar-main-collapse + if typeof suppressNavbarRight == 'undefined' + #navbar-main-collapse.navbar-collapse.collapse ul.nav.navbar-nav.navbar-right - if (canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu) + if canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu li.dropdown.subdued a.dropdown-toggle( - href="#", - role="button", - aria-haspopup="true", - aria-expanded="false", - data-toggle="dropdown" - event-tracking="menu-expand" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={"item": "admin", "location": "top-menu"} + href='#' + role='button' + aria-haspopup='true' + aria-expanded='false' + data-toggle='dropdown' + event-tracking='menu-expand' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={item: 'admin', location: 'top-menu'} ) | Admin span.caret ul.dropdown-menu if canDisplayAdminMenu li - a(href="/admin") Manage Site + a(href='/admin') Manage Site li - a(href="/admin/user") Manage Users + a(href='/admin/user') Manage Users li - a(href="/admin/project") Project URL Lookup + a(href='/admin/project') Project URL Lookup if canDisplayAdminRedirect li a(href=settings.adminUrl) Switch to Admin if canDisplaySplitTestMenu li - a(href="/admin/split-test") Manage Feature Flags + a(href='/admin/split-test') Manage Feature Flags if canDisplaySurveyMenu li - a(href="/admin/survey") Manage Surveys + a(href='/admin/survey') Manage Surveys if canDisplayScriptLogMenu li - a(href="/admin/script-logs") View Script Logs + a(href='/admin/script-logs') View Script Logs // loop over header_extras each item in nav.header_extras @@ -88,15 +97,15 @@ nav.navbar.navbar-default.navbar-main.website-redesign-navbar if item.dropdown li.dropdown(class=item.class) a.dropdown-toggle( - href="#", - role="button", - aria-haspopup="true", - aria-expanded="false", - data-toggle="dropdown" - event-tracking="menu-expand" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={"item": item.trackingKey, "location": "top-menu"} + href='#' + role='button' + aria-haspopup='true' + aria-expanded='false' + data-toggle='dropdown' + event-tracking='menu-expand' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={item: item.trackingKey, location: 'top-menu'} ) | !{translate(item.text)} span.caret @@ -106,18 +115,25 @@ nav.navbar.navbar-default.navbar-main.website-redesign-navbar li.divider else if child.isContactUs li - a(data-ol-open-contact-form-modal="contact-us" href event-tracking="menu-click" event-tracking-mb="true" event-tracking-trigger="click" event-segmentation={"item": "contact", "location": "top-menu"}) + a( + data-ol-open-contact-form-modal='contact-us' + href + event-tracking='menu-click' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={item: 'contact', location: 'top-menu'} + ) span | #{translate("contact_us")} else li if child.url a( - href=child.url, - class=child.class, - event-tracking="menu-click" - event-tracking-mb="true" - event-tracking-trigger="click" + href=child.url + class=child.class + event-tracking='menu-click' + event-tracking-mb='true' + event-tracking-trigger='click' event-segmentation={item: child.trackingKey, location: 'top-menu'} ) !{translate(child.text)} else @@ -126,12 +142,12 @@ nav.navbar.navbar-default.navbar-main.website-redesign-navbar li(class=item.class) if item.url a( - href=item.url, - class=item.class, - event-tracking="menu-click" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={ item: item.trackingKey, location: 'top-menu' } + href=item.url + class=item.class + event-tracking='menu-click' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={item: item.trackingKey, location: 'top-menu'} ) !{translate(item.text)} else | !{translate(item.text)} @@ -142,54 +158,54 @@ nav.navbar.navbar-default.navbar-main.website-redesign-navbar if hasFeature('registration-page') li.primary a( - href="/register" - event-tracking="menu-click" - event-tracking-action="clicked" - event-tracking-trigger="click" - event-tracking-mb="true" - event-segmentation={ page: currentUrl, item: 'register', location: 'top-menu' } + href='/register' + event-tracking='menu-click' + event-tracking-action='clicked' + event-tracking-trigger='click' + event-tracking-mb='true' + event-segmentation={page: currentUrl, item: 'register', location: 'top-menu'} ) #{translate('sign_up')} // login link li.secondary a( - href="/login" - event-tracking="menu-click" - event-tracking-action="clicked" - event-tracking-trigger="click" - event-tracking-mb="true" - event-segmentation={ page: currentUrl, item: 'login', location: 'top-menu' } + href='/login' + event-tracking='menu-click' + event-tracking-action='clicked' + event-tracking-trigger='click' + event-tracking-mb='true' + event-segmentation={page: currentUrl, item: 'login', location: 'top-menu'} ) #{translate('log_in')} // projects link and account menu if getSessionUser() li.secondary - a(href="/project") #{translate('Projects')} + a(href='/project') #{translate('Projects')} li.secondary.dropdown a.dropdown-toggle( - href="#", - role="button", - aria-haspopup="true", - aria-expanded="false", - data-toggle="dropdown" - event-tracking="menu-expand" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={"item": "account", "location": "top-menu"} + href='#' + role='button' + aria-haspopup='true' + aria-expanded='false' + data-toggle='dropdown' + event-tracking='menu-expand' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={item: 'account', location: 'top-menu'} ) | #{translate('Account')} span.caret ul.dropdown-menu li - div.subdued #{getSessionUser().email} + .subdued #{getSessionUser().email} li.divider.hidden-xs.hidden-sm li - a(href="/user/settings") #{translate('account_settings')} + a(href='/user/settings') #{translate('account_settings')} if nav.showSubscriptionLink li - a(href="/user/subscription") #{translate('subscription')} + 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) + 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/layout/thin-footer-bootstrap-5.pug b/services/web/app/views/layout/thin-footer-bootstrap-5.pug index 1f06a054fc..4c29933ab1 100644 --- a/services/web/app/views/layout/thin-footer-bootstrap-5.pug +++ b/services/web/app/views/layout/thin-footer-bootstrap-5.pug @@ -25,7 +25,7 @@ footer.site-footer each item in nav.left_footer li if item.url - a(href=item.url, class=item.class) !{translate(item.text)} + a(href=item.url class=item.class) !{translate(item.text)} else | !{item.text} @@ -33,6 +33,6 @@ footer.site-footer each item in nav.right_footer li if item.url - a(href=item.url, class=item.class, aria-label=item.label) !{item.text} + a(href=item.url class=item.class aria-label=item.label) !{item.text} else | !{item.text} diff --git a/services/web/app/views/layout/thin-footer.pug b/services/web/app/views/layout/thin-footer.pug index 6eeecf628a..879e337983 100644 --- a/services/web/app/views/layout/thin-footer.pug +++ b/services/web/app/views/layout/thin-footer.pug @@ -27,7 +27,7 @@ footer.site-footer each item in nav.left_footer li if item.url - a(href=item.url, class=item.class) !{translate(item.text)} + a(href=item.url class=item.class) !{translate(item.text)} else | !{item.text} @@ -35,6 +35,6 @@ footer.site-footer each item in nav.right_footer li if item.url - a(href=item.url, class=item.class, aria-label=item.label) !{item.text} + a(href=item.url class=item.class aria-label=item.label) !{item.text} else | !{item.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 index b1b5ae1e25..c84288a21a 100644 --- a/services/web/app/views/project/editor/new_from_template.pug +++ b/services/web/app/views/project/editor/new_from_template.pug @@ -1,36 +1,36 @@ extends ../../layout-marketing block vars - - var suppressFooter = true - - var suppressCookieBanner = true - - var suppressSkipToContent = true + - var suppressFooter = true + - var suppressCookieBanner = true + - var suppressSkipToContent = true block content - .editor.full-size - .loading-screen() - .loading-screen-brand-container - .loading-screen-brand( - style="height: 20%;" - ) + .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 . + h3.loading-screen-label() #{translate("Opening template")} + span.loading-screen-ellip . + span.loading-screen-ellip . + span.loading-screen-ellip . - form( - data-ol-regular-form - data-ol-auto-submit - method='POST' - action='/project/new/template/' - ) - 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) - input(hidden type="submit") + form( + data-ol-regular-form + data-ol-auto-submit + method='POST' + action='/project/new/template/' + ) + 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) + input(hidden type="submit") diff --git a/services/web/app/views/project/ide-react-detached.pug b/services/web/app/views/project/ide-react-detached.pug index 8109da7f74..ca1a178bbf 100644 --- a/services/web/app/views/project/ide-react-detached.pug +++ b/services/web/app/views/project/ide-react-detached.pug @@ -11,7 +11,7 @@ block vars - metadata.robotsNoindexNofollow = true block content - #pdf-preview-detached-root() + #pdf-preview-detached-root block append meta include editor/_meta diff --git a/services/web/app/views/project/ide-react.pug b/services/web/app/views/project/ide-react.pug index bc30f69202..8af9de8296 100644 --- a/services/web/app/views/project/ide-react.pug +++ b/services/web/app/views/project/ide-react.pug @@ -15,7 +15,7 @@ block content main#ide-root .loading-screen .loading-screen-brand-container - .loading-screen-brand(style="height: 20%;") + .loading-screen-brand(style='height: 20%') h3.loading-screen-label #{translate("loading")} span.loading-screen-ellip . span.loading-screen-ellip . @@ -25,4 +25,9 @@ block append meta include editor/_meta block prepend foot-scripts - script(type="text/javascript", nonce=scriptNonce, src=(wsUrl || '/socket.io') + '/socket.io.js', defer=deferScripts) + script( + type='text/javascript' + nonce=scriptNonce + src=(wsUrl || '/socket.io') + '/socket.io.js' + defer=deferScripts + ) diff --git a/services/web/app/views/project/invite/not-valid.pug b/services/web/app/views/project/invite/not-valid.pug index b4cbc1be1b..3722ab7919 100644 --- a/services/web/app/views/project/invite/not-valid.pug +++ b/services/web/app/views/project/invite/not-valid.pug @@ -1,18 +1,18 @@ extends ../../layout-marketing block content - main.content.content-alt#main-content + main#main-content.content.content-alt .container .row .col-md-8.col-md-offset-2.offset-md-2 .card.project-invite-invalid .card-body .page-header.text-center - h1 #{translate("invite_not_valid")} + h1 #{translate("invite_not_valid")} .row.text-center .col-12.col-md-12 p | #{translate("invite_not_valid_description")}. .row.text-center.actions .col-12.col-md-12 - a.btn.btn-secondary-info.btn-secondary(href="/project") #{translate("back_to_your_projects")} + a.btn.btn-secondary-info.btn-secondary(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 index a18518c716..503ec78796 100644 --- a/services/web/app/views/project/invite/show.pug +++ b/services/web/app/views/project/invite/show.pug @@ -1,7 +1,7 @@ extends ../../layout-marketing block content - main.content.content-alt#main-content + main#main-content.content.content-alt .container .row .col-12.col-md-8.col-md-offset-2.offset-md-2 @@ -20,16 +20,16 @@ block content .col-12.col-md-12 form.form( data-ol-regular-form - method="POST", - action="/project/"+invite.projectId+"/invite/token/"+token+"/accept" + method='POST' + action='/project/' + invite.projectId + '/invite/token/' + token + '/accept' ) - input(name='_csrf', type='hidden', value=csrfToken) - input(name='token', type='hidden', value=token) + input(name='_csrf' type='hidden' value=csrfToken) + input(name='token' type='hidden' value=token) .form-group.text-center button.btn.btn-lg.btn-primary( - type="submit" + type='submit' data-ol-disabled-inflight ) - span(data-ol-inflight="idle") #{translate("join_project")} - span(hidden data-ol-inflight="pending") #{translate("joining")}… + span(data-ol-inflight='idle') #{translate("join_project")} + span(hidden data-ol-inflight='pending') #{translate("joining")}… .form-group.text-center diff --git a/services/web/app/views/project/list-react.pug b/services/web/app/views/project/list-react.pug index 60e7d0c0fc..fa7bc24c09 100644 --- a/services/web/app/views/project/list-react.pug +++ b/services/web/app/views/project/list-react.pug @@ -9,35 +9,83 @@ block vars - const suppressFooter = true block append meta - meta(name="ol-usersBestSubscription" data-type="json" content=usersBestSubscription) - meta(name="ol-notifications" data-type="json" content=notifications) - meta(name="ol-notificationsInstitution" data-type="json" content=notificationsInstitution) - meta(name="ol-userEmails" data-type="json" content=userEmails) - meta(name="ol-allInReconfirmNotificationPeriods" data-type="json" content=allInReconfirmNotificationPeriods) - meta(name="ol-user" data-type="json" content=user) - meta(name="ol-userAffiliations" data-type="json" content=userAffiliations) - meta(name="ol-reconfirmedViaSAML" content=reconfirmedViaSAML) - meta(name="ol-survey" data-type="json" content=survey) - meta(name="ol-tags" data-type="json" content=tags) - meta(name="ol-portalTemplates" data-type="json" content=portalTemplates) - meta(name="ol-prefetchedProjectsBlob" data-type="json" content=prefetchedProjectsBlob) - if (suggestedLanguageSubdomainConfig) - meta(name="ol-suggestedLanguage" data-type="json" content=Object.assign(suggestedLanguageSubdomainConfig, { - lngName: translate(suggestedLanguageSubdomainConfig.lngCode), - imgUrl: buildImgPath("flags/24/" + suggestedLanguageSubdomainConfig.lngCode + ".png") - })) - meta(name="ol-currentUrl" data-type="string" content=currentUrl) - meta(name="ol-showGroupsAndEnterpriseBanner" data-type="boolean" content=showGroupsAndEnterpriseBanner) - meta(name="ol-groupsAndEnterpriseBannerVariant" data-type="string" content=groupsAndEnterpriseBannerVariant) - meta(name="ol-showInrGeoBanner" data-type="boolean" content=showInrGeoBanner) - meta(name="ol-showBrlGeoBanner" data-type="boolean" content=showBrlGeoBanner) - meta(name="ol-recommendedCurrency" data-type="string" content=recommendedCurrency) - meta(name="ol-showLATAMBanner" data-type="boolean" content=showLATAMBanner) - meta(name="ol-groupSubscriptionsPendingEnrollment" data-type="json" content=groupSubscriptionsPendingEnrollment) - meta(name="ol-hasIndividualPaidSubscription" data-type="boolean" content=hasIndividualPaidSubscription) - meta(name="ol-groupSsoSetupSuccess" data-type="boolean" content=groupSsoSetupSuccess) - meta(name="ol-showUSGovBanner" data-type="boolean" content=showUSGovBanner) - meta(name="ol-usGovBannerVariant" data-type="string" content=usGovBannerVariant) + meta( + name='ol-usersBestSubscription' + data-type='json' + content=usersBestSubscription + ) + meta(name='ol-notifications' data-type='json' content=notifications) + meta( + name='ol-notificationsInstitution' + data-type='json' + content=notificationsInstitution + ) + meta(name='ol-userEmails' data-type='json' content=userEmails) + meta( + name='ol-allInReconfirmNotificationPeriods' + data-type='json' + content=allInReconfirmNotificationPeriods + ) + meta(name='ol-user' data-type='json' content=user) + meta(name='ol-userAffiliations' data-type='json' content=userAffiliations) + meta(name='ol-reconfirmedViaSAML' content=reconfirmedViaSAML) + meta(name='ol-survey' data-type='json' content=survey) + meta(name='ol-tags' data-type='json' content=tags) + meta(name='ol-portalTemplates' data-type='json' content=portalTemplates) + meta( + name='ol-prefetchedProjectsBlob' + data-type='json' + content=prefetchedProjectsBlob + ) + if suggestedLanguageSubdomainConfig + meta( + name='ol-suggestedLanguage' + data-type='json' + content=Object.assign(suggestedLanguageSubdomainConfig, { + lngName: translate(suggestedLanguageSubdomainConfig.lngCode), + imgUrl: buildImgPath('flags/24/' + suggestedLanguageSubdomainConfig.lngCode + '.png'), + }) + ) + meta(name='ol-currentUrl' data-type='string' content=currentUrl) + meta( + name='ol-showGroupsAndEnterpriseBanner' + data-type='boolean' + content=showGroupsAndEnterpriseBanner + ) + meta( + name='ol-groupsAndEnterpriseBannerVariant' + data-type='string' + content=groupsAndEnterpriseBannerVariant + ) + meta(name='ol-showInrGeoBanner' data-type='boolean' content=showInrGeoBanner) + meta(name='ol-showBrlGeoBanner' data-type='boolean' content=showBrlGeoBanner) + meta( + name='ol-recommendedCurrency' + data-type='string' + content=recommendedCurrency + ) + meta(name='ol-showLATAMBanner' data-type='boolean' content=showLATAMBanner) + meta( + name='ol-groupSubscriptionsPendingEnrollment' + data-type='json' + content=groupSubscriptionsPendingEnrollment + ) + meta( + name='ol-hasIndividualPaidSubscription' + data-type='boolean' + content=hasIndividualPaidSubscription + ) + meta( + name='ol-groupSsoSetupSuccess' + data-type='boolean' + content=groupSsoSetupSuccess + ) + meta(name='ol-showUSGovBanner' data-type='boolean' content=showUSGovBanner) + meta( + name='ol-usGovBannerVariant' + data-type='string' + content=usGovBannerVariant + ) block content #project-list-root diff --git a/services/web/app/views/project/token/access-react.pug b/services/web/app/views/project/token/access-react.pug index eabfd18eb6..80b91f1a99 100644 --- a/services/web/app/views/project/token/access-react.pug +++ b/services/web/app/views/project/token/access-react.pug @@ -7,11 +7,11 @@ block vars - var suppressFooter = true - var suppressCookieBanner = true - var suppressSkipToContent = true - + block append meta - meta(name="ol-postUrl" data-type="string" content=postUrl) - meta(name="ol-user" data-type="json" content=user) + meta(name='ol-postUrl' data-type='string' content=postUrl) + meta(name='ol-user' data-type='json' content=user) block content - .content.content-alt#main-content - div#token-access-page + #main-content.content.content-alt + #token-access-page diff --git a/services/web/app/views/project/token/sharing-updates.pug b/services/web/app/views/project/token/sharing-updates.pug index 66d8ac9077..d1818be0af 100644 --- a/services/web/app/views/project/token/sharing-updates.pug +++ b/services/web/app/views/project/token/sharing-updates.pug @@ -9,9 +9,9 @@ block vars - var suppressSkipToContent = true block append meta - meta(name="ol-user" data-type="json" content=user) - meta(name="ol-project_id" data-type="string" content=projectId) + meta(name='ol-user' data-type='json' content=user) + meta(name='ol-project_id' data-type='string' content=projectId) block content - .content.content-alt#main-content - div#sharing-updates-page + #main-content.content.content-alt + #sharing-updates-page diff --git a/services/web/app/views/subscriptions/add-seats.pug b/services/web/app/views/subscriptions/add-seats.pug index bcbf5be666..6fa644ee46 100644 --- a/services/web/app/views/subscriptions/add-seats.pug +++ b/services/web/app/views/subscriptions/add-seats.pug @@ -4,13 +4,17 @@ block entrypointVar - entrypoint = 'pages/user/subscription/group-management/add-seats' block append meta - meta(name="ol-user" data-type="json" content=user) - meta(name="ol-groupName", data-type="string", content=groupName) - meta(name="ol-subscriptionId", data-type="string", content=subscriptionId) - meta(name="ol-totalLicenses", data-type="number", content=totalLicenses) - meta(name="ol-isProfessional", data-type="boolean", content=isProfessional) - meta(name="ol-isCollectionMethodManual", data-type="boolean", content=isCollectionMethodManual) + meta(name='ol-user' data-type='json' content=user) + meta(name='ol-groupName' data-type='string' content=groupName) + meta(name='ol-subscriptionId' data-type='string' content=subscriptionId) + meta(name='ol-totalLicenses' data-type='number' content=totalLicenses) + meta(name='ol-isProfessional' data-type='boolean' content=isProfessional) + meta( + name='ol-isCollectionMethodManual' + data-type='boolean' + content=isCollectionMethodManual + ) block content - main.content.content-alt#main-content + main#main-content.content.content-alt #add-seats-root diff --git a/services/web/app/views/subscriptions/canceled-subscription-react.pug b/services/web/app/views/subscriptions/canceled-subscription-react.pug index 3a89234fd9..a33732e1f6 100644 --- a/services/web/app/views/subscriptions/canceled-subscription-react.pug +++ b/services/web/app/views/subscriptions/canceled-subscription-react.pug @@ -4,7 +4,7 @@ block entrypointVar - entrypoint = 'pages/user/subscription/canceled-subscription' block append meta - meta(name="ol-user" data-type="json" content=user) + meta(name='ol-user' data-type='json' content=user) block content - main.content.content-alt#subscription-canceled-root + main#subscription-canceled-root.content.content-alt diff --git a/services/web/app/views/subscriptions/dashboard-react.pug b/services/web/app/views/subscriptions/dashboard-react.pug index 2b6251f2a3..b253097d25 100644 --- a/services/web/app/views/subscriptions/dashboard-react.pug +++ b/services/web/app/views/subscriptions/dashboard-react.pug @@ -1,36 +1,91 @@ extends ../layout-react block entrypointVar - - entrypoint = 'pages/user/subscription/dashboard' + - entrypoint = 'pages/user/subscription/dashboard' block head-scripts - script(type="text/javascript", nonce=scriptNonce, src="https://js.recurly.com/v4/recurly.js") + script( + type='text/javascript' + nonce=scriptNonce + src='https://js.recurly.com/v4/recurly.js' + ) block append meta - meta(name="ol-subscription" data-type="json" content=personalSubscription) - meta(name="ol-userCanExtendTrial" data-type="boolean" content=userCanExtendTrial) - meta(name="ol-managedGroupSubscriptions" data-type="json" content=managedGroupSubscriptions) - meta(name="ol-memberGroupSubscriptions" data-type="json" content=memberGroupSubscriptions) - meta(name="ol-managedInstitutions" data-type="json" content=managedInstitutions) - meta(name="ol-managedPublishers" data-type="json" content=managedPublishers) - meta(name="ol-planCodesChangingAtTermEnd" data-type="json", content=planCodesChangingAtTermEnd) - meta(name="ol-currentInstitutionsWithLicence" data-type="json" content=currentInstitutionsWithLicence) - meta(name="ol-hasSubscription" data-type="boolean" content=hasSubscription) - meta(name="ol-fromPlansPage" data-type="boolean" content=fromPlansPage) - meta(name="ol-plans" data-type="json" content=plans) - meta(name="ol-groupSettingsAdvertisedFor" data-type="json" content=groupSettingsAdvertisedFor) - meta(name="ol-canUseFlexibleLicensing" data-type="boolean", content=canUseFlexibleLicensing) - meta(name="ol-showGroupDiscount" data-type="boolean", content=showGroupDiscount) - meta(name="ol-groupSettingsEnabledFor" data-type="json" content=groupSettingsEnabledFor) - meta(name="ol-hasAiAssistViaWritefull" data-type="boolean", content=hasAiAssistViaWritefull) - meta(name="ol-aiAssistViaWritefullSource" data-type="string", content=aiAssistViaWritefullSource) - meta(name="ol-user" data-type="json" content=user) - if (personalSubscription && personalSubscription.payment) - meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey) - meta(name="ol-stripeUKApiKey" content=settings.apis.stripeUK.publishableKey) - meta(name="ol-recommendedCurrency" content=personalSubscription.payment.currency) - meta(name="ol-groupPlans" data-type="json" content=groupPlans) + meta(name='ol-subscription' data-type='json' content=personalSubscription) + meta( + name='ol-userCanExtendTrial' + data-type='boolean' + content=userCanExtendTrial + ) + meta( + name='ol-managedGroupSubscriptions' + data-type='json' + content=managedGroupSubscriptions + ) + meta( + name='ol-memberGroupSubscriptions' + data-type='json' + content=memberGroupSubscriptions + ) + meta( + name='ol-managedInstitutions' + data-type='json' + content=managedInstitutions + ) + meta(name='ol-managedPublishers' data-type='json' content=managedPublishers) + meta( + name='ol-planCodesChangingAtTermEnd' + data-type='json' + content=planCodesChangingAtTermEnd + ) + meta( + name='ol-currentInstitutionsWithLicence' + data-type='json' + content=currentInstitutionsWithLicence + ) + meta(name='ol-hasSubscription' data-type='boolean' content=hasSubscription) + meta(name='ol-fromPlansPage' data-type='boolean' content=fromPlansPage) + meta(name='ol-plans' data-type='json' content=plans) + meta( + name='ol-groupSettingsAdvertisedFor' + data-type='json' + content=groupSettingsAdvertisedFor + ) + meta( + name='ol-canUseFlexibleLicensing' + data-type='boolean' + content=canUseFlexibleLicensing + ) + meta( + name='ol-showGroupDiscount' + data-type='boolean' + content=showGroupDiscount + ) + meta( + name='ol-groupSettingsEnabledFor' + data-type='json' + content=groupSettingsEnabledFor + ) + meta( + name='ol-hasAiAssistViaWritefull' + data-type='boolean' + content=hasAiAssistViaWritefull + ) + meta( + name='ol-aiAssistViaWritefullSource' + data-type='string' + content=aiAssistViaWritefullSource + ) + meta(name='ol-user' data-type='json' content=user) + if personalSubscription && personalSubscription.payment + meta(name='ol-recurlyApiKey' content=settings.apis.recurly.publicKey) + meta(name='ol-stripeUKApiKey' content=settings.apis.stripeUK.publishableKey) + meta( + name='ol-recommendedCurrency' + content=personalSubscription.payment.currency + ) + meta(name='ol-groupPlans' data-type='json' content=groupPlans) block content - main.content.content-alt#main-content - #subscription-dashboard-root + main#main-content.content.content-alt + #subscription-dashboard-root diff --git a/services/web/app/views/subscriptions/manually-collected-subscription.pug b/services/web/app/views/subscriptions/manually-collected-subscription.pug index ba6bf73473..a693ac749f 100644 --- a/services/web/app/views/subscriptions/manually-collected-subscription.pug +++ b/services/web/app/views/subscriptions/manually-collected-subscription.pug @@ -4,8 +4,8 @@ block entrypointVar - entrypoint = 'pages/user/subscription/group-management/manually-collected-subscription' block append meta - meta(name="ol-user" data-type="json" content=user) - meta(name="ol-groupName", data-type="string", content=groupName) + meta(name='ol-user' data-type='json' content=user) + meta(name='ol-groupName' data-type='string' content=groupName) block content - main.content.content-alt#manually-collected-subscription-root + main#manually-collected-subscription-root.content.content-alt diff --git a/services/web/app/views/subscriptions/missing-billing-information.pug b/services/web/app/views/subscriptions/missing-billing-information.pug index 416bac65f5..d0a0e05ae8 100644 --- a/services/web/app/views/subscriptions/missing-billing-information.pug +++ b/services/web/app/views/subscriptions/missing-billing-information.pug @@ -4,8 +4,8 @@ block entrypointVar - entrypoint = 'pages/user/subscription/group-management/missing-billing-information' block append meta - meta(name="ol-user" data-type="json" content=user) - meta(name="ol-groupName", data-type="string", content=groupName) + meta(name='ol-user' data-type='json' content=user) + meta(name='ol-groupName' data-type='string' content=groupName) block content - main.content.content-alt#missing-billing-information-root + main#missing-billing-information-root.content.content-alt diff --git a/services/web/app/views/subscriptions/plans/_faq_new.pug b/services/web/app/views/subscriptions/plans/_faq_new.pug index 3c926fb22d..760f27b1a4 100644 --- a/services/web/app/views/subscriptions/plans/_faq_new.pug +++ b/services/web/app/views/subscriptions/plans/_faq_new.pug @@ -9,7 +9,7 @@ include ../../_mixins/material_symbol .row.row-spaced-extra-large .col-md-12.faq-heading-container h2 - +eyebrow(translate("frequently_asked_questions")) + +eyebrow(translate('frequently_asked_questions')) | #{translate("your_questions_answered")} .row @@ -18,74 +18,62 @@ include ../../_mixins/material_symbol class={ 'plans-faq-tabs': bootstrapVersion === 5, 'ol-tabs': bootstrapVersion === 5, - 'ol-tabs-scrollable': bootstrapVersion === 3 + 'ol-tabs-scrollable': bootstrapVersion === 3, } ) .nav-tabs-container - ul.nav.nav-tabs(role="tablist") + ul.nav.nav-tabs(role='tablist') //- In the bs5 version of plans page, the `active` class need to be added to the `a` tag instead of the parent `li` tag //- If the `plans-page-bs5` split test has been completed, remove the `active` class from the `li` tag since we're not using it anymore //- If the `plans-page-bs5` split test has been completed, remove the `data-toggle` because it is not needed anymore (bs5 uses `data-bs-toggle`) - li( - role="presentation" - class={ - active: bootstrapVersion === 3 - } - ) + li(role='presentation' class={ + active: bootstrapVersion === 3, + }) a( - role="tab" - data-toggle="tab" - data-bs-toggle="tab" + role='tab' + data-toggle='tab' + data-bs-toggle='tab' href='#' + managingYourSubscription aria-controls=managingYourSubscription class={ - active: bootstrapVersion === 5 + active: bootstrapVersion === 5, } ) | #{translate('managing_your_subscription')} - li(role="presentation") + li(role='presentation') a( - role="tab" - data-toggle="tab" - data-bs-toggle="tab" + role='tab' + data-toggle='tab' + data-bs-toggle='tab' href='#' + overleafIndividualPlans aria-controls=overleafIndividualPlans ) | #{translate('overleaf_individual_plans')} - li(role="presentation") + li(role='presentation') a( - role="tab" - data-toggle="tab" - data-bs-toggle="tab" + role='tab' + data-toggle='tab' + data-bs-toggle='tab' href='#' + overleafGroupPlans aria-controls=overleafGroupPlans ) | #{translate('overleaf_group_plans')} .tab-content - .tab-pane.active( - role="tabpanel" - id=managingYourSubscription - ) - +managingYourSubscription() - .tab-pane( - role="tabpanel" - id=overleafIndividualPlans - ) - +overleafIndividualPlans() - .tab-pane( - role="tabpanel" - id=overleafGroupPlans - ) - +overleafGroupPlans() + .tab-pane.active(role='tabpanel' id=managingYourSubscription) + +managingYourSubscription + .tab-pane(role='tabpanel' id=overleafIndividualPlans) + +overleafIndividualPlans + .tab-pane(role='tabpanel' id=overleafGroupPlans) + +overleafGroupPlans .row .col-xs-12.plans-faq-support span #{translate('still_have_questions')} button( - data-ol-open-contact-form-modal="general" - data-bs-toggle=bootstrapVersion === 5 ? "modal" : undefined - data-bs-target=bootstrapVersion === 5 ? "#contactUsModal" : undefined + data-ol-open-contact-form-modal='general' + data-bs-toggle=bootstrapVersion === 5 ? 'modal' : undefined + data-bs-target=bootstrapVersion === 5 ? '#contactUsModal' : undefined ) - span(style="margin-right: 4px") #{translate('contact_support')} - +material-symbol-rounded("arrow_right_alt", "icon-md") + span(style='margin-right: 4px') #{translate('contact_support')} + +material-symbol-rounded('arrow_right_alt', 'icon-md') diff --git a/services/web/app/views/subscriptions/plans/_plans_faq_tabs.pug b/services/web/app/views/subscriptions/plans/_plans_faq_tabs.pug index a598f4774c..59eac8efac 100644 --- a/services/web/app/views/subscriptions/plans/_plans_faq_tabs.pug +++ b/services/web/app/views/subscriptions/plans/_plans_faq_tabs.pug @@ -1,357 +1,353 @@ //- If the `plans-page-bs5` split test has been completed, remove the `data-toggle` and `data-target` because it is not needed anymore (bs5 uses `data-bs-toggle` and `data-bs-target`) include ../../_mixins/material_symbol - -mixin managingYourSubscription() + +mixin managingYourSubscription .ol-accordions-container .custom-accordion-item button.custom-accordion-header.collapsed( - type="button" - data-toggle="collapse" - data-target="#managingYourSubscriptionQ1" - data-bs-toggle="collapse" - data-bs-target="#managingYourSubscriptionQ1" - aria-expanded="false" - aria-controls="managingYourSubscriptionQ1" + type='button' + data-toggle='collapse' + data-target='#managingYourSubscriptionQ1' + data-bs-toggle='collapse' + data-bs-target='#managingYourSubscriptionQ1' + aria-expanded='false' + aria-controls='managingYourSubscriptionQ1' ) | Can I change plans or cancel later? span.custom-accordion-icon - +material-symbol-outlined("keyboard_arrow_down") - .collapse(id="managingYourSubscriptionQ1") + +material-symbol-outlined('keyboard_arrow_down') + .collapse(id='managingYourSubscriptionQ1') .custom-accordion-body - span Yes, you can do this at any time by going to - strong Account > Subscription + span Yes, you can do this at any time by going to + strong Account > Subscription span when logged in to Overleaf. You can change plans, switch between monthly and annual billing options, or cancel to downgrade to the free plan. When canceling, your subscription will continue until the end of the billing period. .custom-accordion-item button.custom-accordion-header.collapsed( - type="button" - data-toggle="collapse" - data-target="#managingYourSubscriptionQ2" - data-bs-toggle="collapse" - data-bs-target="#managingYourSubscriptionQ2" - aria-expanded="false" - aria-controls="managingYourSubscriptionQ2" + type='button' + data-toggle='collapse' + data-target='#managingYourSubscriptionQ2' + data-bs-toggle='collapse' + data-bs-target='#managingYourSubscriptionQ2' + aria-expanded='false' + aria-controls='managingYourSubscriptionQ2' ) | If I change or cancel my Overleaf plan, will I lose my projects? span.custom-accordion-icon - +material-symbol-outlined("keyboard_arrow_down") - .collapse(id="managingYourSubscriptionQ2") + +material-symbol-outlined('keyboard_arrow_down') + .collapse(id='managingYourSubscriptionQ2') .custom-accordion-body | No. Changing or canceling your plan won’t affect your projects, the only change will be to the features available to you. You can see which features are available only on paid plans in the comparison table. .custom-accordion-item button.custom-accordion-header.collapsed( - type="button" - data-toggle="collapse" - data-target="#managingYourSubscriptionQ3" - data-bs-toggle="collapse" - data-bs-target="#managingYourSubscriptionQ3" - aria-expanded="false" - aria-controls="managingYourSubscriptionQ3" + type='button' + data-toggle='collapse' + data-target='#managingYourSubscriptionQ3' + data-bs-toggle='collapse' + data-bs-target='#managingYourSubscriptionQ3' + aria-expanded='false' + aria-controls='managingYourSubscriptionQ3' ) | Can I pay by invoice or purchase order? span.custom-accordion-icon - +material-symbol-outlined("keyboard_arrow_down") - .collapse(id="managingYourSubscriptionQ3") + +material-symbol-outlined('keyboard_arrow_down') + .collapse(id='managingYourSubscriptionQ3') .custom-accordion-body | This is possible when you’re purchasing a group subscription for five or more people, or a site license. For individual subscriptions, we can only accept payment online via credit card, debit card, or PayPal. .custom-accordion-item button.custom-accordion-header.collapsed( - type="button" - data-toggle="collapse" - data-target="#managingYourSubscriptionQ4" - data-bs-toggle="collapse" - data-bs-target="#managingYourSubscriptionQ4" - aria-expanded="false" - aria-controls="managingYourSubscriptionQ4" + type='button' + data-toggle='collapse' + data-target='#managingYourSubscriptionQ4' + data-bs-toggle='collapse' + data-bs-target='#managingYourSubscriptionQ4' + aria-expanded='false' + aria-controls='managingYourSubscriptionQ4' ) | How do I view/update the credit card being charged for my subscription? span.custom-accordion-icon - +material-symbol-outlined("keyboard_arrow_down") - .collapse(id="managingYourSubscriptionQ4") + +material-symbol-outlined('keyboard_arrow_down') + .collapse(id='managingYourSubscriptionQ4') .custom-accordion-body - | You can view and update the card on file by going to Account > + | You can view and update the card on file by going to Account > a.inline-green-link( - target="_blank" - href="/user/subscription" - event-tracking="plans-page-click" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={ button: 'contact', location: 'faq' } + target='_blank' + href='/user/subscription' + event-tracking='plans-page-click' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={button: 'contact', location: 'faq'} ) span Subscription | . - - - -mixin overleafIndividualPlans() +mixin overleafIndividualPlans .ol-accordions-container .custom-accordion-item button.custom-accordion-header.collapsed( - type="button" - data-toggle="collapse" - data-target="#overleafIndividualPlansQ1" - data-bs-toggle="collapse" - data-bs-target="#overleafIndividualPlansQ1" - aria-expanded="false" - aria-controls="overleafIndividualPlansQ1" + type='button' + data-toggle='collapse' + data-target='#overleafIndividualPlansQ1' + data-bs-toggle='collapse' + data-bs-target='#overleafIndividualPlansQ1' + aria-expanded='false' + aria-controls='overleafIndividualPlansQ1' ) | How does the free trial work? span.custom-accordion-icon - +material-symbol-outlined("keyboard_arrow_down") - .collapse(id="overleafIndividualPlansQ1") + +material-symbol-outlined('keyboard_arrow_down') + .collapse(id='overleafIndividualPlansQ1') .custom-accordion-body - span You get full access to your chosen plan during your 7-day free trial, and there’s no obligation to continue beyond the trial. Your card will be charged at the end of your trial unless you cancel before then. To cancel, go to - strong Account > + span You get full access to your chosen plan during your 7-day free trial, and there’s no obligation to continue beyond the trial. Your card will be charged at the end of your trial unless you cancel before then. To cancel, go to + strong Account > + | a.inline-green-link( - target="_blank" - href="/user/subscription" - event-tracking="plans-page-click" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={ button: 'contact', location: 'faq' } + target='_blank' + href='/user/subscription' + event-tracking='plans-page-click' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={button: 'contact', location: 'faq'} ) span Subscription - span when logged in to Overleaf (the trial will continue for the full 7 days). + span when logged in to Overleaf (the trial will continue for the full 7 days). .custom-accordion-item button.custom-accordion-header.collapsed( - type="button" - data-toggle="collapse" - data-target="#overleafIndividualPlansQ2" - data-bs-toggle="collapse" - data-bs-target="#overleafIndividualPlansQ2" - aria-expanded="false" - aria-controls="overleafIndividualPlansQ2" + type='button' + data-toggle='collapse' + data-target='#overleafIndividualPlansQ2' + data-bs-toggle='collapse' + data-bs-target='#overleafIndividualPlansQ2' + aria-expanded='false' + aria-controls='overleafIndividualPlansQ2' ) | What’s a collaborator on an Overleaf individual subscription? span.custom-accordion-icon - +material-symbol-outlined("keyboard_arrow_down") - .collapse(id="overleafIndividualPlansQ2") + +material-symbol-outlined('keyboard_arrow_down') + .collapse(id='overleafIndividualPlansQ2') .custom-accordion-body - | A collaborator is someone you invite to work with you on a project. So, for example, on our Standard plan you can have up to 10 people collaborating with you on any given project. + | A collaborator is someone you invite to work with you on a project. So, for example, on our Standard plan you can have up to 10 people collaborating with you on any given project. .custom-accordion-item button.custom-accordion-header.collapsed( - type="button" - data-toggle="collapse" - data-target="#overleafIndividualPlansQ3" - data-bs-toggle="collapse" - data-bs-target="#overleafIndividualPlansQ3" - aria-expanded="false" - aria-controls="overleafIndividualPlansQ3" + type='button' + data-toggle='collapse' + data-target='#overleafIndividualPlansQ3' + data-bs-toggle='collapse' + data-bs-target='#overleafIndividualPlansQ3' + aria-expanded='false' + aria-controls='overleafIndividualPlansQ3' ) | The individual Standard plan has 10 project collaborators, does it mean that 10 people will be upgraded? span.custom-accordion-icon - +material-symbol-outlined("keyboard_arrow_down") - .collapse(id="overleafIndividualPlansQ3") + +material-symbol-outlined('keyboard_arrow_down') + .collapse(id='overleafIndividualPlansQ3') .custom-accordion-body - span No. Only the subscriber’s account will be upgraded. An individual Standard subscription allows you to invite 10 people per project to edit the project with you. Your collaborators can access features such as the full document history and extended compile time, but - strong only + span No. Only the subscriber’s account will be upgraded. An individual Standard subscription allows you to invite 10 people per project to edit the project with you. Your collaborators can access features such as the full document history and extended compile time, but + strong only span for the project(s) they’re working on with you. If your collaborators want access to those features on their own projects, they will need to purchase their own subscription. (If you work with the same people regularly, you might find a group subscription more cost effective.) .custom-accordion-item button.custom-accordion-header.collapsed( - type="button" - data-toggle="collapse" - data-target="#overleafIndividualPlansQ4" - data-bs-toggle="collapse" - data-bs-target="#overleafIndividualPlansQ4" - aria-expanded="false" - aria-controls="overleafIndividualPlansQ4" + type='button' + data-toggle='collapse' + data-target='#overleafIndividualPlansQ4' + data-bs-toggle='collapse' + data-bs-target='#overleafIndividualPlansQ4' + aria-expanded='false' + aria-controls='overleafIndividualPlansQ4' ) | Do collaborators also have access to the editing and collaboration features I’ve paid for? span.custom-accordion-icon - +material-symbol-outlined("keyboard_arrow_down") - .collapse(id="overleafIndividualPlansQ4") + +material-symbol-outlined('keyboard_arrow_down') + .collapse(id='overleafIndividualPlansQ4') .custom-accordion-body - span If you have an Overleaf subscription, then your project collaborators will have access to features like real-time track changes and document history, but - strong only + span If you have an Overleaf subscription, then your project collaborators will have access to features like real-time track changes and document history, but + strong only span for the project(s) they’re working on with you. If your collaborators want access to those features on their own projects, they will need to purchase their own subscription. (If you work with the same people regularly, you might find a group subscription more cost effective.) .custom-accordion-item button.custom-accordion-header.collapsed( - type="button" - data-toggle="collapse" - data-target="#overleafIndividualPlansQ5" - data-bs-toggle="collapse" - data-bs-target="#overleafIndividualPlansQ5" - aria-expanded="false" - aria-controls="overleafIndividualPlansQ5" + type='button' + data-toggle='collapse' + data-target='#overleafIndividualPlansQ5' + data-bs-toggle='collapse' + data-bs-target='#overleafIndividualPlansQ5' + aria-expanded='false' + aria-controls='overleafIndividualPlansQ5' ) | Can I purchase an individual plan on behalf of someone else? span.custom-accordion-icon - +material-symbol-outlined("keyboard_arrow_down") - .collapse(id="overleafIndividualPlansQ5") + +material-symbol-outlined('keyboard_arrow_down') + .collapse(id='overleafIndividualPlansQ5') .custom-accordion-body - | Individual subscriptions must be purchased by the account that will be the end user. If you want to purchase a plan for someone else, you’ll need to provide them with relevant payment details to enable them to make the purchase. + | Individual subscriptions must be purchased by the account that will be the end user. If you want to purchase a plan for someone else, you’ll need to provide them with relevant payment details to enable them to make the purchase. .custom-accordion-item button.custom-accordion-header.collapsed( - type="button" - data-toggle="collapse" - data-target="#overleafIndividualPlansQ6" - data-bs-toggle="collapse" - data-bs-target="#overleafIndividualPlansQ6" - aria-expanded="false" - aria-controls="overleafIndividualPlansQ6" + type='button' + data-toggle='collapse' + data-target='#overleafIndividualPlansQ6' + data-bs-toggle='collapse' + data-bs-target='#overleafIndividualPlansQ6' + aria-expanded='false' + aria-controls='overleafIndividualPlansQ6' ) | Who is eligible for the Student plan? span.custom-accordion-icon - +material-symbol-outlined("keyboard_arrow_down") - .collapse(id="overleafIndividualPlansQ6") + +material-symbol-outlined('keyboard_arrow_down') + .collapse(id='overleafIndividualPlansQ6') .custom-accordion-body | As the name suggests, the Student plan is only for students at educational institutions. This includes graduate students. .custom-accordion-item button.custom-accordion-header.collapsed( - type="button" - data-toggle="collapse" - data-target="#overleafIndividualPlansQ7" - data-bs-toggle="collapse" - data-bs-target="#overleafIndividualPlansQ7" - aria-expanded="false" - aria-controls="overleafIndividualPlansQ7" + type='button' + data-toggle='collapse' + data-target='#overleafIndividualPlansQ7' + data-bs-toggle='collapse' + data-bs-target='#overleafIndividualPlansQ7' + aria-expanded='false' + aria-controls='overleafIndividualPlansQ7' ) | Can I transfer an individual subscription to someone else? span.custom-accordion-icon - +material-symbol-outlined("keyboard_arrow_down") - .collapse(id="overleafIndividualPlansQ7") + +material-symbol-outlined('keyboard_arrow_down') + .collapse(id='overleafIndividualPlansQ7') .custom-accordion-body - | No. Individual plans can’t be transferred. + | No. Individual plans can’t be transferred. - - - - -mixin overleafGroupPlans() +mixin overleafGroupPlans .ol-accordions-container .custom-accordion-item button.custom-accordion-header.collapsed( - type="button" - data-toggle="collapse" - data-target="#overleafGroupPlansQ1" - data-bs-toggle="collapse" - data-bs-target="#overleafGroupPlansQ1" - aria-expanded="false" - aria-controls="overleafGroupPlansQ1" + type='button' + data-toggle='collapse' + data-target='#overleafGroupPlansQ1' + data-bs-toggle='collapse' + data-bs-target='#overleafGroupPlansQ1' + aria-expanded='false' + aria-controls='overleafGroupPlansQ1' ) | What’s the difference between users and collaborators on an Overleaf group subscription? span.custom-accordion-icon - +material-symbol-outlined("keyboard_arrow_down") - .collapse(id="overleafGroupPlansQ1") + +material-symbol-outlined('keyboard_arrow_down') + .collapse(id='overleafGroupPlansQ1') .custom-accordion-body - div On any of our group plans, the number of users refers to the number of people you can invite to join your group. All of these people will have access to the plan’s paid-for features across all their projects, such as real-time track changes and document history. - div.mt-2 Collaborators are people that your group users may invite to work with them on their projects. So, for example, if you have the Group Standard plan, the users in your group can invite up to 10 people to work with them on a project. And if you have the Group Professional plan, your users can invite as many people to work with them as they want. + div On any of our group plans, the number of users refers to the number of people you can invite to join your group. All of these people will have access to the plan’s paid-for features across all their projects, such as real-time track changes and document history. + .mt-2 Collaborators are people that your group users may invite to work with them on their projects. So, for example, if you have the Group Standard plan, the users in your group can invite up to 10 people to work with them on a project. And if you have the Group Professional plan, your users can invite as many people to work with them as they want. .custom-accordion-item button.custom-accordion-header.collapsed( - type="button" - data-toggle="collapse" - data-target="#overleafGroupPlansQ2" - data-bs-toggle="collapse" - data-bs-target="#overleafGroupPlansQ2" - aria-expanded="false" - aria-controls="overleafGroupPlansQ2" + type='button' + data-toggle='collapse' + data-target='#overleafGroupPlansQ2' + data-bs-toggle='collapse' + data-bs-target='#overleafGroupPlansQ2' + aria-expanded='false' + aria-controls='overleafGroupPlansQ2' ) | What is the benefit of purchasing an Overleaf Group plan? span.custom-accordion-icon - +material-symbol-outlined("keyboard_arrow_down") - .collapse(id="overleafGroupPlansQ2") + +material-symbol-outlined('keyboard_arrow_down') + .collapse(id='overleafGroupPlansQ2') .custom-accordion-body - | Our Group subscriptions allow you to purchase access to our premium features for multiple people. They’re easy to manage, help save on paperwork, and allow groups of 5 or more to purchase via purchase order (PO). We also offer discounts on purchases of Group subscriptions for more than 20 users; just get in touch with our + | Our Group subscriptions allow you to purchase access to our premium features for multiple people. They’re easy to manage, help save on paperwork, and allow groups of 5 or more to purchase via purchase order (PO). We also offer discounts on purchases of Group subscriptions for more than 20 users; just get in touch with our a.inline-green-link( - target="_blank" - href="/for/contact-sales" - event-tracking="plans-page-click" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={ button: 'contact', location: 'faq' } + target='_blank' + href='/for/contact-sales' + event-tracking='plans-page-click' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={button: 'contact', location: 'faq'} ) span Sales team | . .custom-accordion-item button.custom-accordion-header.collapsed( - type="button" - data-toggle="collapse" - data-target="#overleafGroupPlansQ3" - data-bs-toggle="collapse" - data-bs-target="#overleafGroupPlansQ3" - aria-expanded="false" - aria-controls="overleafGroupPlansQ3" + type='button' + data-toggle='collapse' + data-target='#overleafGroupPlansQ3' + data-bs-toggle='collapse' + data-bs-target='#overleafGroupPlansQ3' + aria-expanded='false' + aria-controls='overleafGroupPlansQ3' ) | Who is eligible for the educational discount? span.custom-accordion-icon - +material-symbol-outlined("keyboard_arrow_down") - .collapse(id="overleafGroupPlansQ3") + +material-symbol-outlined('keyboard_arrow_down') + .collapse(id='overleafGroupPlansQ3') .custom-accordion-body - | The educational discount for group subscriptions is for students or faculty who are using Overleaf primarily for teaching. + | The educational discount for group subscriptions is for students or faculty who are using Overleaf primarily for teaching. .custom-accordion-item button.custom-accordion-header.collapsed( - type="button" - data-toggle="collapse" - data-target="#overleafGroupPlansQ4" - data-bs-toggle="collapse" - data-bs-target="#overleafGroupPlansQ4" - aria-expanded="false" - aria-controls="overleafGroupPlansQ4" + type='button' + data-toggle='collapse' + data-target='#overleafGroupPlansQ4' + data-bs-toggle='collapse' + data-bs-target='#overleafGroupPlansQ4' + aria-expanded='false' + aria-controls='overleafGroupPlansQ4' ) | How do I add more licenses to my group subscription, and what will it cost? span.custom-accordion-icon - +material-symbol-outlined("keyboard_arrow_down") - .collapse(id="overleafGroupPlansQ4") + +material-symbol-outlined('keyboard_arrow_down') + .collapse(id='overleafGroupPlansQ4') .custom-accordion-body - div - | You can add up to 20 licenses using the + div + | You can add up to 20 licenses using the a.inline-green-link( - target="_blank" - href="/user/subscription" - event-tracking="plans-page-click" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={ button: 'contact', location: 'faq' } + target='_blank' + href='/user/subscription' + event-tracking='plans-page-click' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={button: 'contact', location: 'faq'} ) span subscription management page - | accessed by going to Account > + | + | accessed by going to Account > a.inline-green-link( - target="_blank" - href="/user/subscription" - event-tracking="plans-page-click" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={ button: 'contact', location: 'faq' } + target='_blank' + href='/user/subscription' + event-tracking='plans-page-click' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={button: 'contact', location: 'faq'} ) span Subscription - | when logged into Overleaf. The cost per license will be prorated at the current per license rate, and will end with your existing renewal date. - div.mt-2 - | If you need more than 20 licenses added to your subscription, please + | + | when logged into Overleaf. The cost per license will be prorated at the current per license rate, and will end with your existing renewal date. + .mt-2 + | If you need more than 20 licenses added to your subscription, please a.inline-green-link( - target="_blank" - href="/for/contact-sales" - event-tracking="plans-page-click" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={ button: 'contact', location: 'faq' } + target='_blank' + href='/for/contact-sales' + event-tracking='plans-page-click' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={button: 'contact', location: 'faq'} ) span contact the Sales team | . .custom-accordion-item button.custom-accordion-header.collapsed( - type="button" - data-toggle="collapse" - data-target="#overleafGroupPlansQ5" - data-bs-toggle="collapse" - data-bs-target="#overleafGroupPlansQ5" - aria-expanded="false" - aria-controls="overleafGroupPlansQ5" + type='button' + data-toggle='collapse' + data-target='#overleafGroupPlansQ5' + data-bs-toggle='collapse' + data-bs-target='#overleafGroupPlansQ5' + aria-expanded='false' + aria-controls='overleafGroupPlansQ5' ) | How do I upgrade my plan from Group Standard to Group Professional? span.custom-accordion-icon - +material-symbol-outlined("keyboard_arrow_down") - .collapse(id="overleafGroupPlansQ5") + +material-symbol-outlined('keyboard_arrow_down') + .collapse(id='overleafGroupPlansQ5') .custom-accordion-body - | You can upgrade your plan from Group Standard to Group Professional on the + | You can upgrade your plan from Group Standard to Group Professional on the a.inline-green-link( - target="_blank" - href="/user/subscription" - event-tracking="plans-page-click" - event-tracking-mb="true" - event-tracking-trigger="click" - event-segmentation={ button: 'contact', location: 'faq' } + target='_blank' + href='/user/subscription' + event-tracking='plans-page-click' + event-tracking-mb='true' + event-tracking-trigger='click' + event-segmentation={button: 'contact', location: 'faq'} ) span subscription management page | . diff --git a/services/web/app/views/subscriptions/preview-change.pug b/services/web/app/views/subscriptions/preview-change.pug index 5330eb8684..2eaca5ac6a 100644 --- a/services/web/app/views/subscriptions/preview-change.pug +++ b/services/web/app/views/subscriptions/preview-change.pug @@ -4,10 +4,14 @@ block entrypointVar - entrypoint = 'pages/user/subscription/preview-change' block append meta - meta(name="ol-user" data-type="json" content=user) - meta(name="ol-subscriptionChangePreview" data-type="json" content=changePreview) - meta(name="ol-purchaseReferrer" data-type="string" content=purchaseReferrer) + meta(name='ol-user' data-type='json' content=user) + meta( + name='ol-subscriptionChangePreview' + data-type='json' + content=changePreview + ) + meta(name='ol-purchaseReferrer' data-type='string' content=purchaseReferrer) block content - main.content.content-alt#main-content + main#main-content.content.content-alt #subscription-preview-change diff --git a/services/web/app/views/subscriptions/subtotal-limit-exceeded.pug b/services/web/app/views/subscriptions/subtotal-limit-exceeded.pug index 4457383e93..df734232d6 100644 --- a/services/web/app/views/subscriptions/subtotal-limit-exceeded.pug +++ b/services/web/app/views/subscriptions/subtotal-limit-exceeded.pug @@ -4,8 +4,8 @@ block entrypointVar - entrypoint = 'pages/user/subscription/group-management/subtotal-limit-exceeded' block append meta - meta(name="ol-user" data-type="json" content=user) - meta(name="ol-groupName", data-type="string", content=groupName) + meta(name='ol-user' data-type='json' content=user) + meta(name='ol-groupName' data-type='string' content=groupName) block content - main.content.content-alt#subtotal-limit-exceeded-root + main#subtotal-limit-exceeded-root.content.content-alt diff --git a/services/web/app/views/subscriptions/successful-subscription-react.pug b/services/web/app/views/subscriptions/successful-subscription-react.pug index 5ce208b034..9437b93b67 100644 --- a/services/web/app/views/subscriptions/successful-subscription-react.pug +++ b/services/web/app/views/subscriptions/successful-subscription-react.pug @@ -4,9 +4,9 @@ block entrypointVar - entrypoint = 'pages/user/subscription/successful-subscription' block append meta - meta(name="ol-subscription" data-type="json" content=personalSubscription) - meta(name="ol-postCheckoutRedirect" content=postCheckoutRedirect) - meta(name="ol-user" data-type="json" content=user) + meta(name='ol-subscription' data-type='json' content=personalSubscription) + meta(name='ol-postCheckoutRedirect' content=postCheckoutRedirect) + meta(name='ol-user' data-type='json' content=user) block content - main.content.content-alt#subscription-success-root + main#subscription-success-root.content.content-alt diff --git a/services/web/app/views/subscriptions/team/group-invites.pug b/services/web/app/views/subscriptions/team/group-invites.pug index 81c70f1885..18286fd403 100644 --- a/services/web/app/views/subscriptions/team/group-invites.pug +++ b/services/web/app/views/subscriptions/team/group-invites.pug @@ -4,8 +4,8 @@ block entrypointVar - entrypoint = 'pages/user/subscription/group-invites' block append meta - meta(name="ol-teamInvites" data-type="json" content=teamInvites) - meta(name="ol-user" data-type="json" content=user) + meta(name='ol-teamInvites' data-type='json' content=teamInvites) + meta(name='ol-user' data-type='json' content=user) block content - main.content.content-alt.team-invite#group-invites-root + main#group-invites-root.content.content-alt.team-invite diff --git a/services/web/app/views/subscriptions/team/invite-managed.pug b/services/web/app/views/subscriptions/team/invite-managed.pug index d31f12656b..4010731c3f 100644 --- a/services/web/app/views/subscriptions/team/invite-managed.pug +++ b/services/web/app/views/subscriptions/team/invite-managed.pug @@ -4,16 +4,20 @@ block entrypointVar - entrypoint = 'pages/user/subscription/invite-managed' block append meta - meta(name="ol-inviteToken" content=inviteToken) - meta(name="ol-inviterName" content=inviterName) - meta(name="ol-expired" data-type="boolean" content=expired) - meta(name="ol-alreadyEnrolled" data-type="boolean" content=alreadyEnrolled) - meta(name="ol-validationStatus" data-type="json" content=validationStatus) - meta(name="ol-currentManagedUserAdminEmail" data-type="string" content=currentManagedUserAdminEmail) - meta(name="ol-groupSSOActive" data-type="boolean" content=groupSSOActive) - meta(name="ol-subscriptionId" data-type="string" content=subscriptionId) - meta(name="ol-user" data-type="json" content=user) - meta(name="ol-usersSubscription" data-type="json" content=usersSubscription) + meta(name='ol-inviteToken' content=inviteToken) + meta(name='ol-inviterName' content=inviterName) + meta(name='ol-expired' data-type='boolean' content=expired) + meta(name='ol-alreadyEnrolled' data-type='boolean' content=alreadyEnrolled) + meta(name='ol-validationStatus' data-type='json' content=validationStatus) + meta( + name='ol-currentManagedUserAdminEmail' + data-type='string' + content=currentManagedUserAdminEmail + ) + meta(name='ol-groupSSOActive' data-type='boolean' content=groupSSOActive) + meta(name='ol-subscriptionId' data-type='string' content=subscriptionId) + meta(name='ol-user' data-type='json' content=user) + meta(name='ol-usersSubscription' data-type='json' content=usersSubscription) block content - main.content.content-alt.team-invite#invite-managed-root + main#invite-managed-root.content.content-alt.team-invite diff --git a/services/web/app/views/subscriptions/team/invite.pug b/services/web/app/views/subscriptions/team/invite.pug index 1b2ecb4646..717ccef611 100644 --- a/services/web/app/views/subscriptions/team/invite.pug +++ b/services/web/app/views/subscriptions/team/invite.pug @@ -4,14 +4,22 @@ block entrypointVar - entrypoint = 'pages/user/subscription/invite' block append meta - meta(name="ol-hasIndividualPaidSubscription" data-type="boolean" content=hasIndividualPaidSubscription) - meta(name="ol-inviterName" data-type="string" content=inviterName) - meta(name="ol-inviteToken" data-type="string" content=inviteToken) - meta(name="ol-currentManagedUserAdminEmail" data-type="string" content=currentManagedUserAdminEmail) - meta(name="ol-expired" data-type="boolean" content=expired) - meta(name="ol-groupSSOActive" data-type="boolean" content=groupSSOActive) - meta(name="ol-subscriptionId" data-type="string" content=subscriptionId) - meta(name="ol-user" data-type="json" content=user) + meta( + name='ol-hasIndividualPaidSubscription' + data-type='boolean' + content=hasIndividualPaidSubscription + ) + meta(name='ol-inviterName' data-type='string' content=inviterName) + meta(name='ol-inviteToken' data-type='string' content=inviteToken) + meta( + name='ol-currentManagedUserAdminEmail' + data-type='string' + content=currentManagedUserAdminEmail + ) + meta(name='ol-expired' data-type='boolean' content=expired) + meta(name='ol-groupSSOActive' data-type='boolean' content=groupSSOActive) + meta(name='ol-subscriptionId' data-type='string' content=subscriptionId) + meta(name='ol-user' data-type='json' content=user) block content - main.content.content-alt#invite-root + main#invite-root.content.content-alt diff --git a/services/web/app/views/subscriptions/team/invite_logged_out.pug b/services/web/app/views/subscriptions/team/invite_logged_out.pug index e5930aba4f..3e471fb4c9 100644 --- a/services/web/app/views/subscriptions/team/invite_logged_out.pug +++ b/services/web/app/views/subscriptions/team/invite_logged_out.pug @@ -1,12 +1,12 @@ extends ../../layout-marketing block append meta - meta(name="ol-user" data-type="json" content=user) + meta(name='ol-user' data-type='json' content=user) block content - var colClass = bootstrapVersion === 5 ? 'col-lg-8 m-auto' : 'col-md-8 col-md-offset-2' - main.content.content-alt.team-invite#main-content + main#main-content.content.content-alt.team-invite .container .row div(class=colClass) @@ -16,15 +16,19 @@ block content // TODO: Remove `team-invite-name` once we fully migrated to Bootstrap 5 h1.text-center !{translate("invited_to_group", {inviterName: inviterName, appName: appName }, [{name: 'span', attrs: {class: 'team-invite-name'}}])} - if (accountExists) + if accountExists div p #{translate("invited_to_group_login_benefits", {appName: appName})} p #{translate("invited_to_group_login", {emailAddress: emailAddress})} p - a.btn.btn-primary(href=`/login?redir=/subscription/invites/${inviteToken}${groupSSOActive ? "&hide_sso_login=true" : ""}`) #{translate("login_to_accept_invitation")} + a.btn.btn-primary( + href=`/login?redir=/subscription/invites/${inviteToken}${groupSSOActive ? "&hide_sso_login=true" : ""}` + ) #{translate("login_to_accept_invitation")} else div p #{translate("invited_to_group_register_benefits", {appName: appName})} p #{translate("invited_to_group_register", {inviterName: inviterName})} p - a.btn.btn-primary(href=`/register?redir=/subscription/invites/${inviteToken}${groupSSOActive ? "&hide_sso_login=true" : ""}`) #{translate("register_to_accept_invitation")} + a.btn.btn-primary( + href=`/register?redir=/subscription/invites/${inviteToken}${groupSSOActive ? "&hide_sso_login=true" : ""}` + ) #{translate("register_to_accept_invitation")} diff --git a/services/web/app/views/subscriptions/upgrade-group-subscription-react.pug b/services/web/app/views/subscriptions/upgrade-group-subscription-react.pug index 4347a2a633..0c7f4ce993 100644 --- a/services/web/app/views/subscriptions/upgrade-group-subscription-react.pug +++ b/services/web/app/views/subscriptions/upgrade-group-subscription-react.pug @@ -4,10 +4,14 @@ block entrypointVar - entrypoint = 'pages/user/subscription/group-management/upgrade-group-subscription' block append meta - meta(name="ol-user" data-type="json" content=user) - meta(name="ol-subscriptionChangePreview" data-type="json" content=changePreview) - meta(name="ol-totalLicenses", data-type="number", content=totalLicenses) - meta(name="ol-groupName", data-type="string", content=groupName) + meta(name='ol-user' data-type='json' content=user) + meta( + name='ol-subscriptionChangePreview' + data-type='json' + content=changePreview + ) + meta(name='ol-totalLicenses' data-type='number' content=totalLicenses) + meta(name='ol-groupName' data-type='string' content=groupName) block content - main.content.content-alt#upgrade-group-subscription-root + main#upgrade-group-subscription-root.content.content-alt diff --git a/services/web/app/views/user/accountSuspended.pug b/services/web/app/views/user/accountSuspended.pug index 7231713416..1a03beb4df 100644 --- a/services/web/app/views/user/accountSuspended.pug +++ b/services/web/app/views/user/accountSuspended.pug @@ -6,7 +6,7 @@ block vars - metadata.robotsNoindexNofollow = true block content - main.content.content-alt#main-content + main#main-content.content.content-alt .container-custom-sm.mx-auto .card .card-body diff --git a/services/web/app/views/user/compromised_password.pug b/services/web/app/views/user/compromised_password.pug index c66a07415a..48017b0ea7 100644 --- a/services/web/app/views/user/compromised_password.pug +++ b/services/web/app/views/user/compromised_password.pug @@ -9,5 +9,5 @@ block entrypointVar - entrypoint = 'pages/compromised-password' block content - main.content.content-alt#main-content + main#main-content.content.content-alt #compromised-password diff --git a/services/web/app/views/user/confirmSecondaryEmail.pug b/services/web/app/views/user/confirmSecondaryEmail.pug index 181e58e4ce..4f143c16dc 100644 --- a/services/web/app/views/user/confirmSecondaryEmail.pug +++ b/services/web/app/views/user/confirmSecondaryEmail.pug @@ -8,7 +8,7 @@ block entrypointVar - entrypoint = 'pages/user/confirm-secondary-email' block append meta - meta(name="ol-email" content=email) + meta(name='ol-email' content=email) block content main.content.content-alt diff --git a/services/web/app/views/user/confirm_email.pug b/services/web/app/views/user/confirm_email.pug index 13e911f386..d783996076 100644 --- a/services/web/app/views/user/confirm_email.pug +++ b/services/web/app/views/user/confirm_email.pug @@ -2,56 +2,50 @@ extends ../layout-marketing include ../_mixins/notification block content - main.content.content-alt#main-content + main#main-content.content.content-alt .container .row .col-lg-8.offset-lg-2.col-xl-6.offset-xl-3 .card .card-body - .page-header(data-ol-hide-on-error-message="confirm-email-wrong-user") + .page-header(data-ol-hide-on-error-message='confirm-email-wrong-user') h1 #{translate("confirm_email")} + form(method='POST' action='/logout' id='logoutForm') + input(name='_csrf' type='hidden' value=csrfToken) + input(name='redirect' type='hidden' value=currentUrlWithQueryParams) form( - method="POST" - action="/logout" - id="logoutForm" + name='confirmEmailForm' + data-ol-async-form + data-ol-auto-submit + action='/user/emails/confirm' + method='POST' + id='confirmEmailForm' ) - input(type="hidden", name="_csrf", value=csrfToken) - input(type="hidden", name="redirect", value=currentUrlWithQueryParams) - form( - data-ol-async-form, - data-ol-auto-submit, - name="confirmEmailForm" - action="/user/emails/confirm", - method="POST", - id="confirmEmailForm", - ) - input(type="hidden", name="_csrf", value=csrfToken) - input(type="hidden", name="token", value=token) + input(name='_csrf' type='hidden' value=csrfToken) + input(name='token' type='hidden' value=token) div(data-ol-not-sent) - +formMessages() - div(data-ol-custom-form-message="confirm-email-wrong-user" hidden) + +formMessages + div(data-ol-custom-form-message='confirm-email-wrong-user' hidden) h1.h3 #{translate("we_cant_confirm_this_email")} p !{translate("to_confirm_email_address_you_must_be_logged_in_with_the_requesting_account")} p !{translate("you_are_currently_logged_in_as", {email: getUserEmail()})} .actions - button.btn-primary.btn.w-100( - form="logoutForm" - ) #{translate('log_in_with_a_different_account')} + button.btn-primary.btn.w-100(form='logoutForm') #{translate('log_in_with_a_different_account')} .actions button.btn-primary.btn.w-100( - type='submit', + type='submit' data-ol-disabled-inflight - data-ol-hide-on-error-message="confirm-email-wrong-user" + data-ol-hide-on-error-message='confirm-email-wrong-user' ) - span(data-ol-inflight="idle") + span(data-ol-inflight='idle') | #{translate('confirm')} - span(hidden data-ol-inflight="pending") - span(role='status').spinner-border.spinner-border-sm.mx-2 + span(hidden data-ol-inflight='pending') + span.spinner-border.spinner-border-sm.mx-2(role='status') div(hidden data-ol-sent) - +notification({ariaLive: 'polite', type: 'success', className: 'mb-3', content: translate("thank_you_email_confirmed")}) - div.text-center - a.btn.btn-primary(href="/user/settings") + +notification({ariaLive: 'polite', type: 'success', className: 'mb-3', content: translate('thank_you_email_confirmed')}) + .text-center + a.btn.btn-primary(href='/user/settings') | #{translate('go_to_account_settings')} diff --git a/services/web/app/views/user/email-preferences.pug b/services/web/app/views/user/email-preferences.pug index 86ebc5f841..2071f64705 100644 --- a/services/web/app/views/user/email-preferences.pug +++ b/services/web/app/views/user/email-preferences.pug @@ -2,7 +2,7 @@ extends ../layout-marketing include ../_mixins/back_to_btns block content - main.content.content-alt#main-content + main#main-content.content.content-alt .container .row .col-lg-10.offset-lg-1.col-xl-8.offset-xl-2 @@ -10,9 +10,9 @@ block content .card-body .page-header h1 #{translate("newsletter_info_title")} - + p #{translate("newsletter_info_summary")} - + - var submitAction if subscribed - submitAction = '/user/newsletter/unsubscribe' @@ -20,28 +20,28 @@ block content else - submitAction = '/user/newsletter/subscribe' p !{translate("newsletter_info_unsubscribed", {}, ['strong'])} - + form( + name='newsletterForm' data-ol-async-form data-ol-reload-on-success - name="newsletterForm" action=submitAction - method="POST" + method='POST' ) - input(name='_csrf', type='hidden', value=csrfToken) - +formMessages() + input(name='_csrf' type='hidden' value=csrfToken) + +formMessages p.actions.text-center if subscribed - button.btn-danger.btn(type='submit', data-ol-disabled-inflight) - span(data-ol-inflight="idle") #{translate("unsubscribe")} - span(hidden data-ol-inflight="pending") #{translate("saving")}… + button.btn-danger.btn(type='submit' data-ol-disabled-inflight) + span(data-ol-inflight='idle') #{translate("unsubscribe")} + span(hidden data-ol-inflight='pending') #{translate("saving")}… else - button.btn-primary.btn(type='submit', data-ol-disabled-inflight) - span(data-ol-inflight="idle") #{translate("subscribe")} - span(hidden data-ol-inflight="pending") #{translate("saving")}… - + button.btn-primary.btn(type='submit' data-ol-disabled-inflight) + span(data-ol-inflight='idle') #{translate("subscribe")} + span(hidden data-ol-inflight='pending') #{translate("saving")}… + if subscribed p #{translate("newsletter_info_note")} - + .page-separator - +back-to-btns() + +back-to-btns diff --git a/services/web/app/views/user/login.pug b/services/web/app/views/user/login.pug index 1ad77cb8b4..03112a0e16 100644 --- a/services/web/app/views/user/login.pug +++ b/services/web/app/views/user/login.pug @@ -1,7 +1,7 @@ extends ../layout-marketing block content - main.content.content-alt#main-content + main#main-content.content.content-alt .container .row .col-lg-6.offset-lg-3.col-xl-4.offset-xl-4 @@ -11,10 +11,10 @@ block content if login_support_title h1 !{login_support_title} else - h1 #{translate("log_in")} - form(data-ol-async-form, name="loginForm", action='/login', method="POST") - input(name='_csrf', type='hidden', value=csrfToken) - +formMessagesNewStyle() + h1 #{translate("log_in")} + form(name='loginForm' data-ol-async-form action='/login' method='POST') + input(name='_csrf' type='hidden' value=csrfToken) + +formMessagesNewStyle +customFormMessageNewStyle('invalid-password-retry-or-reset', 'danger') | !{translate('email_or_password_wrong_try_again_or_reset', {}, [{ name: 'a', attrs: { href: '/user/password/reset', 'aria-describedby': 'resetPasswordDescription' } }])} span.visually-hidden(id='resetPasswordDescription') @@ -23,28 +23,24 @@ block content | !{translate('password_compromised_try_again_or_use_known_device_or_reset', {}, [{name: 'a', attrs: {href: 'https://haveibeenpwned.com/passwords', rel: 'noopener noreferrer', target: '_blank'}}, {name: 'a', attrs: {href: '/user/password/reset', target: '_blank'}}])}. .form-group input.form-control( - type='email', - name='email', - required, - placeholder='email@example.com', - autofocus="true" + name='email' + type='email' + required + placeholder='email@example.com' + autofocus='true' ) .form-group input.form-control( - type='password', - name='password', - required, - placeholder='********', + name='password' + type='password' + required + placeholder='********' ) .actions - button.btn-primary.btn( - type='submit', - data-ol-disabled-inflight - ) - span(data-ol-inflight="idle") #{translate("login")} - span(hidden data-ol-inflight="pending") #{translate("logging_in")}… + button.btn-primary.btn(type='submit' data-ol-disabled-inflight) + span(data-ol-inflight='idle') #{translate("login")} + span(hidden data-ol-inflight='pending') #{translate("logging_in")}… a.float-end(href='/user/password/reset') #{translate("forgot_your_password")}? if login_support_text hr - p.text-center !{login_support_text} - + p.text-center !{login_support_text} diff --git a/services/web/app/views/user/one_time_login.pug b/services/web/app/views/user/one_time_login.pug index 648f6d93c1..e5d50c5a2d 100644 --- a/services/web/app/views/user/one_time_login.pug +++ b/services/web/app/views/user/one_time_login.pug @@ -1,7 +1,7 @@ extends ../layout-marketing block content - main.content.content-alt#main-content + main#main-content.content.content-alt .container .row .col-lg-6.offset-lg-3.col-xl-4.offset-xl-4 @@ -13,6 +13,6 @@ block content p | Please | - a(href="/login") log in + a(href='/login') log in | | to continue working on your projects. diff --git a/services/web/app/views/user/passwordReset-bs5.pug b/services/web/app/views/user/passwordReset-bs5.pug index 08e0a71b9d..f69553b849 100644 --- a/services/web/app/views/user/passwordReset-bs5.pug +++ b/services/web/app/views/user/passwordReset-bs5.pug @@ -11,51 +11,55 @@ 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") + script( + type='text/javascript' + nonce=scriptNonce + src='https://www.recaptcha.net/recaptcha/api.js?render=explicit' + ) div( - id="recaptcha" - class="g-recaptcha" + id='recaptcha' + class='g-recaptcha' data-sitekey=settings.recaptcha.siteKey - data-size="invisible" - data-badge="inline" + data-size='invisible' + data-badge='inline' ) - main#main-content(data-ol-captcha-retry-trigger-area="") - a.auth-aux-logo(href="/") - img(src=buildImgPath("ol-brand/overleaf-o-dark.svg") alt=settings.appName) + main#main-content(data-ol-captcha-retry-trigger-area='') + a.auth-aux-logo(href='/') + img(src=buildImgPath('ol-brand/overleaf-o-dark.svg') alt=settings.appName) .auth-aux-container form( + name='passwordResetForm' + captcha-action-name=showCaptcha ? 'passwordReset' : false data-ol-async-form - name="passwordResetForm" - action="/user/password/reset" - method="POST" - captcha=(showCaptcha ? '' : false) - captcha-action-name=(showCaptcha ? "passwordReset" : false) + action='/user/password/reset' + method='POST' + captcha=showCaptcha ? '' : false ) if error === 'password_reset_token_expired' h1.h3.mb-3.mt-0 #{translate("sorry_your_token_expired")} p #{translate('please_request_a_new_password_reset_email_and_follow_the_link')}. else h1.h3.mb-3.mt-0(data-ol-not-sent) #{translate("password_reset_sentence_case")} - h1.h3.mb-3.mt-0(hidden data-ol-sent) #{translate("check_your_email")} + h1.h3.mb-3.mt-0(hidden data-ol-sent) #{translate("check_your_email")} p.mb-3.pb-3(data-ol-not-sent) #{translate("enter_your_email_address_below_and_we_will_send_you_a_link_to_reset_your_password")}. div(data-ol-not-sent) - +formMessagesNewStyle() + +formMessagesNewStyle if error && error !== 'password_reset_token_expired' +notification({ariaLive: 'assertive', type: 'error', className: 'mb-3', content: translate(error)}) - div(data-ol-custom-form-message="no-password-allowed-due-to-sso" hidden) - +notification({ariaLive: 'polite', type: 'error', className: 'mb-3', content: translate("you_cant_reset_password_due_to_sso", {}, [{name: 'a', attrs: {href: '/sso-login'}}])}) - input(type="hidden" name="_csrf" value=csrfToken) + div(data-ol-custom-form-message='no-password-allowed-due-to-sso' hidden) + +notification({ariaLive: 'polite', type: 'error', className: 'mb-3', content: translate('you_cant_reset_password_due_to_sso', {}, [{name: 'a', attrs: {href: '/sso-login'}}])}) + input(name='_csrf' type='hidden' value=csrfToken) .form-group.mb-3 label.form-label(for='email') #{translate("email")} - input.form-control#email( - aria-label="email" - type='email' + input#email.form-control( name='email' + aria-label='email' + type='email' required - autocomplete="username" + autocomplete='username' autofocus ) .actions @@ -64,14 +68,14 @@ block content data-ol-disabled-inflight aria-label=translate('reset_password_sentence_case') ) - span(data-ol-inflight="idle") + span(data-ol-inflight='idle') | #{translate("reset_password_sentence_case")} - span(hidden data-ol-inflight="pending") + span(hidden data-ol-inflight='pending') | #{translate("requesting_password_reset")}… - a.btn.btn-ghost.w-100.mb-3(href="/login") #{translate("back_to_log_in")} + a.btn.btn-ghost.w-100.mb-3(href='/login') #{translate("back_to_log_in")} div(hidden data-ol-sent) p.mb-4 #{translate('password_reset_email_sent')} - a.btn.btn-primary.w-100.mb-3(href="/login") #{translate('back_to_log_in')} + a.btn.btn-primary.w-100.mb-3(href='/login') #{translate('back_to_log_in')} if showCaptcha +recaptchaConditions diff --git a/services/web/app/views/user/passwordReset.pug b/services/web/app/views/user/passwordReset.pug index ed806c32cd..e3396a5cc0 100644 --- a/services/web/app/views/user/passwordReset.pug +++ b/services/web/app/views/user/passwordReset.pug @@ -9,77 +9,81 @@ 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") + script( + type='text/javascript' + nonce=scriptNonce + src='https://www.recaptcha.net/recaptcha/api.js?render=explicit' + ) div( - id="recaptcha" - class="g-recaptcha" + id='recaptcha' + class='g-recaptcha' data-sitekey=settings.recaptcha.siteKey - data-size="invisible" - data-badge="inline" + data-size='invisible' + data-badge='inline' ) - main.content.content-alt#main-content(data-ol-captcha-retry-trigger-area="") + main#main-content.content.content-alt(data-ol-captcha-retry-trigger-area='') .container-custom-sm.mx-auto .card form( + name='passwordResetForm' + captcha-action-name=showCaptcha ? 'passwordReset' : false data-ol-async-form - name="passwordResetForm" - action="/user/password/reset", - method="POST", - captcha=(showCaptcha ? '' : false), - captcha-action-name=(showCaptcha ? "passwordReset" : false), + action='/user/password/reset' + method='POST' + captcha=showCaptcha ? '' : false ) if error === 'password_reset_token_expired' h3.mt-0.mb-2 #{translate("sorry_your_token_expired")} p #{translate('please_request_a_new_password_reset_email_and_follow_the_link')}. else h3.mt-0.mb-2(data-ol-not-sent) #{translate("password_reset")} - h3.mt-0.mb-2(hidden data-ol-sent) #{translate("check_your_email")} + h3.mt-0.mb-2(hidden data-ol-sent) #{translate("check_your_email")} p(data-ol-not-sent) #{translate("enter_your_email_address_below_and_we_will_send_you_a_link_to_reset_your_password")}. div(data-ol-not-sent) - +formMessages() + +formMessages if error && error !== 'password_reset_token_expired' - div.alert.alert-danger.mb-2( - role="alert" - aria-live="assertive" - ) + .alert.alert-danger.mb-2(role='alert' aria-live='assertive') | #{translate(error)} - div(data-ol-custom-form-message="no-password-allowed-due-to-sso" hidden) - .notification.notification-type-error(aria-live="polite" style="margin-bottom: 10px;") + div(data-ol-custom-form-message='no-password-allowed-due-to-sso' hidden) + .notification.notification-type-error( + aria-live='polite' + style='margin-bottom: 10px' + ) .notification-icon - +material-symbol-rounded("error") + +material-symbol-rounded('error') .notification-content-and-cta .notification-content p | !{translate("you_cant_reset_password_due_to_sso", {}, [{name: 'a', attrs: {href: '/sso-login'}}])} - input(type="hidden", name="_csrf", value=csrfToken) + input(name='_csrf' type='hidden' value=csrfToken) .form-group.mb-3 label(for='email') #{translate("email")} - input.form-control#email( - aria-label="email" - type='email', - name='email', - placeholder=translate("enter_your_email_address"), - required, - autocomplete="username", + input#email.form-control( + name='email' + aria-label='email' + type='email' + placeholder=translate('enter_your_email_address') + required + autocomplete='username' autofocus ) .actions button.btn.btn-primary.w-100( - type='submit', - data-ol-disabled-inflight, + type='submit' + data-ol-disabled-inflight aria-label=translate('request_password_reset_to_reconfirm') ) - span(data-ol-inflight="idle") + span(data-ol-inflight='idle') | #{translate("request_password_reset")} - span(hidden data-ol-inflight="pending") + span(hidden data-ol-inflight='pending') | #{translate("requesting_password_reset")}… div(hidden data-ol-sent) p.mb-4 #{translate('password_reset_email_sent')} - a(href="/login") #{translate('back_to_log_in')} + a(href='/login') #{translate('back_to_log_in')} if showCaptcha +recaptchaConditions diff --git a/services/web/app/views/user/primaryEmailCheck-bs5.pug b/services/web/app/views/user/primaryEmailCheck-bs5.pug index b25136927a..f45f15d0ca 100644 --- a/services/web/app/views/user/primaryEmailCheck-bs5.pug +++ b/services/web/app/views/user/primaryEmailCheck-bs5.pug @@ -7,36 +7,36 @@ block vars block content main#main-content .auth-aux-container - img.w-50.d-block(src=buildImgPath("ol-brand/overleaf.svg") alt=settings.appName) + img.w-50.d-block( + src=buildImgPath('ol-brand/overleaf.svg') + alt=settings.appName + ) h1.h3.mb-3 #{translate("keep_your_account_safe")} div(data-ol-multi-submit) p.small.mb-4 | !{translate("primary_email_check_question", { email: getUserEmail() }, ["strong"])} form( data-ol-async-form - action="/user/emails/primary-email-check" - method="POST" + action='/user/emails/primary-email-check' + method='POST' ) - input(name='_csrf', type='hidden', value=csrfToken) - +formMessagesNewStyle() + input(name='_csrf' type='hidden' value=csrfToken) + +formMessagesNewStyle - button.btn.btn-primary.w-100.mb-3( - type='submit' - data-ol-disabled-inflight - ) - span(data-ol-inflight="idle") #{translate("yes_that_is_correct")} - span(hidden data-ol-inflight="pending") #{translate("confirming")}… + button.btn.btn-primary.w-100.mb-3(type='submit' data-ol-disabled-inflight) + span(data-ol-inflight='idle') #{translate("yes_that_is_correct")} + span(hidden data-ol-inflight='pending') #{translate("confirming")}… a.btn.btn-secondary.w-100.mb-4( - href="/user/settings#add-email" + href='/user/settings#add-email' data-ol-slow-link - event-tracking="primary-email-check-change-email" - event-tracking-mb="true" - event-tracking-trigger="click" + event-tracking='primary-email-check-change-email' + event-tracking-mb='true' + event-tracking-trigger='click' ) - span(data-ol-inflight="idle") #{translate("no_update_email")} - span(hidden data-ol-inflight="pending") #{translate("redirecting")}… + span(data-ol-inflight='idle') #{translate("no_update_email")} + span(hidden data-ol-inflight='pending') #{translate("redirecting")}… p.small.mb-2 - | #{translate("keep_your_email_updated")} + | #{translate("keep_your_email_updated")} p.small - | !{translate("learn_more_about_emails", {}, [{name: 'a', attrs: {href: '/learn/how-to/Keeping_your_account_secure', 'event-tracking': 'primary-email-check-learn-more', 'event-tracking-mb': 'true', 'event-tracking-trigger': 'click' }}])} + | !{translate("learn_more_about_emails", {}, [{name: 'a', attrs: {href: '/learn/how-to/Keeping_your_account_secure', 'event-tracking': 'primary-email-check-learn-more', 'event-tracking-mb': 'true', 'event-tracking-trigger': 'click' }}])} diff --git a/services/web/app/views/user/reconfirm-bs5.pug b/services/web/app/views/user/reconfirm-bs5.pug index fce9a44295..e8e640d10c 100644 --- a/services/web/app/views/user/reconfirm-bs5.pug +++ b/services/web/app/views/user/reconfirm-bs5.pug @@ -5,46 +5,50 @@ block vars - isWebsiteRedesign = true block content - - var email = reconfirm_email ? reconfirm_email : "" + - 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") + script( + type='text/javascript' + nonce=scriptNonce + src='https://www.recaptcha.net/recaptcha/api.js?render=explicit' + ) div( - id="recaptcha" - class="g-recaptcha" + id='recaptcha' + class='g-recaptcha' data-sitekey=settings.recaptcha.siteKey - data-size="invisible" - data-badge="inline" + data-size='invisible' + data-badge='inline' ) - main#main-content(data-ol-captcha-retry-trigger-area="") - .container.auth-aux-container(style="max-width: 420px;") + main#main-content(data-ol-captcha-retry-trigger-area='') + .container.auth-aux-container(style='max-width: 420px') form( + name='reconfirmAccountForm' + captcha-action-name=showCaptcha ? 'passwordReset' : false data-ol-async-form - name="reconfirmAccountForm" - action="/user/reconfirm" - method="POST" + action='/user/reconfirm' + method='POST' aria-label=translate('request_reconfirmation_email') - captcha=(showCaptcha ? '' : false) - captcha-action-name=(showCaptcha ? "passwordReset" : false) + captcha=showCaptcha ? '' : false ) h1.h5.mb-3 #{translate("reconfirm_account")} p #{translate('reconfirm_explained')} - | + | a(href=`mailto:${settings.adminEmail}`) #{settings.adminEmail} | . - - div(data-ol-not-sent) - +formMessagesNewStyle() - input(type="hidden" name="_csrf" value=csrfToken) + div(data-ol-not-sent) + +formMessagesNewStyle + + input(name='_csrf' type='hidden' value=csrfToken) .form-group.mb-3 label.form-label(for='email') #{translate("please_enter_email")} input.form-control( - aria-label="email" - type='email' name='email' + aria-label='email' + type='email' placeholder='email@example.com' required autofocus @@ -52,20 +56,17 @@ block content ) .actions button.btn.btn-primary.w-100( - style="white-space: normal;" + style='white-space: normal' type='submit' data-ol-disabled-inflight aria-label=translate('request_password_reset_to_reconfirm') ) - span(data-ol-inflight="idle") + span(data-ol-inflight='idle') | #{translate('request_password_reset_to_reconfirm')} - span(hidden data-ol-inflight="pending") + span(hidden data-ol-inflight='pending') | #{translate('request_password_reset_to_reconfirm')}… div(hidden data-ol-sent) - div.alert.alert-success( - role="alert" - aria-live="polite" - ) + .alert.alert-success(role='alert' aria-live='polite') span #{translate('password_reset_email_sent')} if showCaptcha diff --git a/services/web/app/views/user/reconfirm.pug b/services/web/app/views/user/reconfirm.pug index 23b77d278d..8cfe2ec218 100644 --- a/services/web/app/views/user/reconfirm.pug +++ b/services/web/app/views/user/reconfirm.pug @@ -5,20 +5,24 @@ block vars - bootstrap5PageStatus = 'disabled' block content - - var email = reconfirm_email ? reconfirm_email : "" + - 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") + script( + type='text/javascript' + nonce=scriptNonce + src='https://www.recaptcha.net/recaptcha/api.js?render=explicit' + ) div( - id="recaptcha" - class="g-recaptcha" + id='recaptcha' + class='g-recaptcha' data-sitekey=settings.recaptcha.siteKey - data-size="invisible" - data-badge="inline" + data-size='invisible' + data-badge='inline' ) - main.content.content-alt#main-content(data-ol-captcha-retry-trigger-area="") + main#main-content.content.content-alt(data-ol-captcha-retry-trigger-area='') .container .row .col-sm-12.col-md-6.col-md-offset-3 @@ -28,44 +32,41 @@ block content a(href=`mailto:${settings.adminEmail}`) #{settings.adminEmail} | . form( + name='reconfirmAccountForm' + captcha-action-name=showCaptcha ? 'passwordReset' : false data-ol-async-form - name="reconfirmAccountForm" - action="/user/reconfirm", - method="POST", + action='/user/reconfirm' + method='POST' aria-label=translate('request_reconfirmation_email') - captcha=(showCaptcha ? '' : false), - captcha-action-name=(showCaptcha ? "passwordReset" : false) + captcha=showCaptcha ? '' : false ) div(data-ol-not-sent) - +formMessages() - - input(type="hidden", name="_csrf", value=csrfToken) + +formMessages + + input(name='_csrf' type='hidden' value=csrfToken) .form-group label(for='email') #{translate("please_enter_email")} input.form-control( - aria-label="email" - type='email', - name='email', - placeholder='email@example.com', - required, + name='email' + aria-label='email' + type='email' + placeholder='email@example.com' + required autofocus value=email ) .actions button.btn.btn-primary( - type='submit', - data-ol-disabled-inflight, + type='submit' + data-ol-disabled-inflight aria-label=translate('request_password_reset_to_reconfirm') ) - span(data-ol-inflight="idle") + span(data-ol-inflight='idle') | #{translate('request_password_reset_to_reconfirm')} - span(hidden data-ol-inflight="pending") + span(hidden data-ol-inflight='pending') | #{translate('request_password_reset_to_reconfirm')}… div(hidden data-ol-sent) - div.alert.alert-success( - role="alert" - aria-live="polite" - ) + .alert.alert-success(role='alert' aria-live='polite') span #{translate('password_reset_email_sent')} .row .col-sm-12.col-md-6.col-md-offset-3 diff --git a/services/web/app/views/user/register.pug b/services/web/app/views/user/register.pug index c35f3c04f0..8aa40e8b35 100644 --- a/services/web/app/views/user/register.pug +++ b/services/web/app/views/user/register.pug @@ -4,7 +4,7 @@ block vars - bootstrap5PageStatus = 'disabled' block content - main.content.content-alt#main-content + main#main-content.content.content-alt .container .row .registration_message @@ -15,12 +15,12 @@ block content | #{translate("join_sl_to_view_project")}. div | #{translate("already_have_sl_account")} - a(href="/login") #{translate("login_here")} + a(href='/login') #{translate("login_here")} else if newTemplateData.templateName !== undefined h1 #{translate("register_to_edit_template", {templateName:newTemplateData.templateName})} div #{translate("already_have_sl_account")} - a(href="/login") #{translate("login_here")} + a(href='/login') #{translate("login_here")} .row .col-md-8.col-md-offset-2.col-lg-6.col-lg-offset-3 diff --git a/services/web/app/views/user/sessions.pug b/services/web/app/views/user/sessions.pug index ffd65a3548..744b804687 100644 --- a/services/web/app/views/user/sessions.pug +++ b/services/web/app/views/user/sessions.pug @@ -1,15 +1,15 @@ extends ../layout-marketing block content - main.content.content-alt#main-content + main#main-content.content.content-alt .container .row .col-lg-10.offset-lg-1.col-xl-8.offset-xl-2 .card.clear-user-sessions .card-body .page-header - h1 #{translate("your_sessions")} - + h1 #{translate("your_sessions")} + if currentSession.ip_address && currentSession.session_created h3 #{translate("current_session")} div @@ -21,47 +21,43 @@ block content tr td #{currentSession.ip_address} td #{moment(currentSession.session_created).utc().format('Do MMM YYYY, h:mm a')} UTC - + h3 #{translate("other_sessions")} div p.small | !{translate("clear_sessions_description")} - - form( - data-ol-async-form - action='/user/sessions/clear' - method='POST' - ) + + form(data-ol-async-form action='/user/sessions/clear' method='POST') input(name='_csrf' type='hidden' value=csrfToken) div(data-ol-not-sent) if sessions.length == 0 p.text-center | #{translate("no_other_sessions")} - + if sessions.length > 0 table.table.table-striped thead tr th #{translate("ip_address")} th #{translate("session_created_at")} - for session in sessions + each session in sessions tr td #{session.ip_address} td #{moment(session.session_created).utc().format('Do MMM YYYY, h:mm a')} UTC - + p.actions .text-center button.btn.btn-lg.btn-primary( - type="submit" + type='submit' data-ol-disable-inflight ) - span(data-ol-inflight="idle") #{translate('clear_sessions')} - span(hidden data-ol-inflight="pending") #{translate("processing")}… - + span(data-ol-inflight='idle') #{translate('clear_sessions')} + span(hidden data-ol-inflight='pending') #{translate("processing")}… + div(hidden data-ol-sent) p.text-center | #{translate("no_other_sessions")} - + p.text-success.text-center | #{translate('clear_sessions_success')} .page-separator diff --git a/services/web/app/views/user/setPassword-bs5.pug b/services/web/app/views/user/setPassword-bs5.pug index 83c3a531bb..5081d22409 100644 --- a/services/web/app/views/user/setPassword-bs5.pug +++ b/services/web/app/views/user/setPassword-bs5.pug @@ -7,28 +7,25 @@ block vars block content main#main-content - a.auth-aux-logo(href="/") - img(src=buildImgPath("ol-brand/overleaf-o-dark.svg") alt=settings.appName) + a.auth-aux-logo(href='/') + img(src=buildImgPath('ol-brand/overleaf-o-dark.svg') alt=settings.appName) .auth-aux-container form( + name='passwordResetForm' data-ol-async-form - name="passwordResetForm" - action="/user/password/set" - method="POST" - data-ol-hide-on-error="token-expired" + action='/user/password/set' + method='POST' + data-ol-hide-on-error='token-expired' ) - div( - hidden - data-ol-sent - ) + div(hidden data-ol-sent) h1.h3.mb-3.mt-0 #{translate("password_updated")} p.mb-4 #{translate("your_password_has_been_successfully_changed")}. a.btn.btn-primary.w-100(href='/login') #{translate("log_in_now")} div(data-ol-not-sent) h1.h3.mb-3.mt-0 #{translate("reset_your_password")} - p(data-ol-hide-on-error-message="token-expired") #{translate("create_a_new_password_for_your_account")}. - +formMessagesNewStyle() + p(data-ol-hide-on-error-message='token-expired') #{translate("create_a_new_password_for_your_account")}. + +formMessagesNewStyle +customFormMessageNewStyle('password-contains-email', 'danger') | #{translate('invalid_password_contains_email')}. @@ -41,18 +38,21 @@ block content +customFormMessageNewStyle('token-expired', 'danger') | #{translate('password_reset_token_expired')} br - a(href="/user/password/reset") + a(href='/user/password/reset') | #{translate('request_new_password_reset_email')} - input(type="hidden" name="_csrf" value=csrfToken) - input(type="text" hidden name="email" autocomplete="username" value=email) + input(name='_csrf' type='hidden' value=csrfToken) + input(name='email' type='text' hidden autocomplete='username' value=email) .form-group.mb-3 - label.form-label(for='passwordField', data-ol-hide-on-error-message="token-expired") #{translate("new_password")} - input.form-control.auth-aux-new-password#passwordField( - type='password' + label.form-label( + for='passwordField' + data-ol-hide-on-error-message='token-expired' + ) #{translate("new_password")} + input#passwordField.form-control.auth-aux-new-password( name='password' - autocomplete="new-password" + type='password' + autocomplete='new-password' autofocus required minlength=settings.passwordStrengthOptions.length.min @@ -68,12 +68,8 @@ block content | !{translate('password_was_detected_on_a_public_list_of_known_compromised_passwords', {}, [{name: 'a', attrs: {href: 'https://haveibeenpwned.com/passwords', rel: 'noopener noreferrer', target: '_blank'}}])}. | #{translate('use_a_different_password')}. - input( - type="hidden" - name="passwordResetToken" - value=passwordResetToken - ) - div(data-ol-hide-on-error-message="token-expired") + input(name='passwordResetToken' type='hidden' value=passwordResetToken) + div(data-ol-hide-on-error-message='token-expired') div #{translate('in_order_to_have_a_secure_account_make_sure_your_password')} ul.mb-3.ps-4 li #{translate('is_longer_than_n_characters', {n: settings.passwordStrengthOptions.length.min})} @@ -85,7 +81,7 @@ block content data-ol-disabled-inflight aria-label=translate('set_new_password') ) - span(data-ol-inflight="idle") + span(data-ol-inflight='idle') | #{translate('set_new_password')} - span(hidden data-ol-inflight="pending") + span(hidden data-ol-inflight='pending') | #{translate('set_new_password')}… diff --git a/services/web/app/views/user/setPassword.pug b/services/web/app/views/user/setPassword.pug index 5da2b6b59a..653bc52045 100644 --- a/services/web/app/views/user/setPassword.pug +++ b/services/web/app/views/user/setPassword.pug @@ -4,28 +4,25 @@ block vars - bootstrap5PageStatus = 'disabled' block content - main.content.content-alt#main-content + main#main-content.content.content-alt .container-custom-sm.mx-auto .card form( - data-ol-async-form, - name="passwordResetForm", - action="/user/password/set", - method="POST", - data-ol-hide-on-error="token-expired" + name='passwordResetForm' + data-ol-async-form + action='/user/password/set' + method='POST' + data-ol-hide-on-error='token-expired' ) - div( - hidden - data-ol-sent - ) + div(hidden data-ol-sent) h3.mt-0.mb-2 #{translate("password_updated")} p.mb-4 #{translate("your_password_has_been_successfully_changed")}. a(href='/login') #{translate("log_in_now")} div(data-ol-not-sent) h3.mt-0.mb-2 #{translate("reset_your_password")} - p(data-ol-hide-on-error-message="token-expired") #{translate("create_a_new_password_for_your_account")}. - +formMessages() + p(data-ol-hide-on-error-message='token-expired') #{translate("create_a_new_password_for_your_account")}. + +formMessages +customFormMessage('password-contains-email', 'danger') | #{translate('invalid_password_contains_email')}. @@ -38,21 +35,30 @@ block content +customFormMessage('token-expired', 'danger') | #{translate('password_reset_token_expired')} br - a(href="/user/password/reset") + a(href='/user/password/reset') | #{translate('request_new_password_reset_email')} - input(type="hidden", name="_csrf", value=csrfToken) - input(type="text" hidden name="email" autocomplete="username" value=email) + input(name='_csrf' type='hidden' value=csrfToken) + input( + name='email' + type='text' + hidden + autocomplete='username' + value=email + ) .form-group - label(for='passwordField', data-ol-hide-on-error-message="token-expired") #{translate("new_password")} - input.form-control#passwordField( - type='password', - name='password', - placeholder=translate("enter_your_new_password"), - autocomplete="new-password", - autofocus, - required, + label( + for='passwordField' + data-ol-hide-on-error-message='token-expired' + ) #{translate("new_password")} + input#passwordField.form-control( + name='password' + type='password' + placeholder=translate('enter_your_new_password') + autocomplete='new-password' + autofocus + required minlength=settings.passwordStrengthOptions.length.min ) @@ -66,12 +72,8 @@ block content | !{translate('password_was_detected_on_a_public_list_of_known_compromised_passwords', {}, [{name: 'a', attrs: {href: 'https://haveibeenpwned.com/passwords', rel: 'noopener noreferrer', target: '_blank'}}])}. | #{translate('use_a_different_password')}. - input( - type="hidden", - name="passwordResetToken", - value=passwordResetToken - ) - div(data-ol-hide-on-error-message="token-expired") + input(name='passwordResetToken' type='hidden' value=passwordResetToken) + div(data-ol-hide-on-error-message='token-expired') div #{translate('in_order_to_have_a_secure_account_make_sure_your_password')} ul.mb-4.ps-4 li #{translate('is_longer_than_n_characters', {n: settings.passwordStrengthOptions.length.min})} @@ -79,11 +81,11 @@ block content li #{translate('is_not_used_on_any_other_website')} .actions button.btn.btn-primary.w-100( - type='submit', + type='submit' data-ol-disabled-inflight aria-label=translate('set_new_password') ) - span(data-ol-inflight="idle") + span(data-ol-inflight='idle') | #{translate('set_new_password')} - span(hidden data-ol-inflight="pending") + span(hidden data-ol-inflight='pending') | #{translate('set_new_password')}… diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 4ac35bef71..dc63e27abe 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -2,38 +2,65 @@ extends ../layout-react block entrypointVar - entrypoint = 'pages/user/settings' - + block vars - isWebsiteRedesign = true block append meta - meta(name="ol-hasPassword" data-type="boolean" content=hasPassword) - meta(name="ol-shouldAllowEditingDetails" data-type="boolean" content=shouldAllowEditingDetails) - meta(name="ol-oauthProviders", data-type="json", content=oauthProviders) - meta(name="ol-institutionLinked", data-type="json", content=institutionLinked) - meta(name="ol-samlError", data-type="json", content=samlError) - meta(name="ol-institutionEmailNonCanonical", content=institutionEmailNonCanonical) + meta(name='ol-hasPassword' data-type='boolean' content=hasPassword) + meta( + name='ol-shouldAllowEditingDetails' + data-type='boolean' + content=shouldAllowEditingDetails + ) + meta(name='ol-oauthProviders' data-type='json' content=oauthProviders) + meta(name='ol-institutionLinked' data-type='json' content=institutionLinked) + meta(name='ol-samlError' data-type='json' content=samlError) + meta( + name='ol-institutionEmailNonCanonical' + content=institutionEmailNonCanonical + ) - meta(name="ol-reconfirmedViaSAML", content=reconfirmedViaSAML) - meta(name="ol-reconfirmationRemoveEmail", content=reconfirmationRemoveEmail) - meta(name="ol-samlBeta", content=samlBeta) - meta(name="ol-ssoErrorMessage", content=ssoErrorMessage) - meta(name="ol-thirdPartyIds", data-type="json", content=thirdPartyIds || {}) - meta(name="ol-passwordStrengthOptions", data-type="json", content=settings.passwordStrengthOptions || {}) - meta(name="ol-isExternalAuthenticationSystemUsed" data-type="boolean" content=externalAuthenticationSystemUsed()) - meta(name="ol-user" data-type="json" content=user) - meta(name="ol-labsExperiments" data-type="json" content=labsExperiments) - meta(name="ol-dropbox" data-type="json" content=dropbox) - meta(name="ol-github" data-type="json" content=github) - meta(name="ol-projectSyncSuccessMessage", content=projectSyncSuccessMessage) - meta(name="ol-personalAccessTokens", data-type="json" content=personalAccessTokens) - meta(name="ol-emailAddressLimit", data-type="json", content=emailAddressLimit) - meta(name="ol-currentManagedUserAdminEmail" data-type="string" content=currentManagedUserAdminEmail) - meta(name="ol-gitBridgeEnabled" data-type="boolean" content=gitBridgeEnabled) - meta(name="ol-isSaas" data-type="boolean" content=isSaas) - meta(name="ol-memberOfSSOEnabledGroups" data-type="json" content=memberOfSSOEnabledGroups) - meta(name="ol-capabilities" data-type="json" content=capabilities) + meta(name='ol-reconfirmedViaSAML' content=reconfirmedViaSAML) + meta(name='ol-reconfirmationRemoveEmail' content=reconfirmationRemoveEmail) + meta(name='ol-samlBeta' content=samlBeta) + meta(name='ol-ssoErrorMessage' content=ssoErrorMessage) + meta(name='ol-thirdPartyIds' data-type='json' content=thirdPartyIds || {}) + meta( + name='ol-passwordStrengthOptions' + data-type='json' + content=settings.passwordStrengthOptions || {} + ) + meta( + name='ol-isExternalAuthenticationSystemUsed' + data-type='boolean' + content=externalAuthenticationSystemUsed() + ) + meta(name='ol-user' data-type='json' content=user) + meta(name='ol-labsExperiments' data-type='json' content=labsExperiments) + meta(name='ol-dropbox' data-type='json' content=dropbox) + meta(name='ol-github' data-type='json' content=github) + meta(name='ol-projectSyncSuccessMessage' content=projectSyncSuccessMessage) + meta( + name='ol-personalAccessTokens' + data-type='json' + content=personalAccessTokens + ) + meta(name='ol-emailAddressLimit' data-type='json' content=emailAddressLimit) + meta( + name='ol-currentManagedUserAdminEmail' + data-type='string' + content=currentManagedUserAdminEmail + ) + meta(name='ol-gitBridgeEnabled' data-type='boolean' content=gitBridgeEnabled) + meta(name='ol-isSaas' data-type='boolean' content=isSaas) + meta( + name='ol-memberOfSSOEnabledGroups' + data-type='json' + content=memberOfSSOEnabledGroups + ) + meta(name='ol-capabilities' data-type='json' content=capabilities) block content - main.content.content-alt#main-content + main#main-content.content.content-alt #settings-page-root diff --git a/services/web/app/views/user_membership/group-managers-react.pug b/services/web/app/views/user_membership/group-managers-react.pug index d227a7a511..34414ddbf2 100644 --- a/services/web/app/views/user_membership/group-managers-react.pug +++ b/services/web/app/views/user_membership/group-managers-react.pug @@ -4,10 +4,10 @@ block entrypointVar - entrypoint = 'pages/user/subscription/group-management/group-managers' block append meta - meta(name="ol-user", data-type="json", content=user) - meta(name="ol-users", data-type="json", content=users) - meta(name="ol-groupId", data-type="string", content=groupId) - meta(name="ol-groupName", data-type="string", content=name) + meta(name='ol-user' data-type='json' content=user) + meta(name='ol-users' data-type='json' content=users) + meta(name='ol-groupId' data-type='string' content=groupId) + meta(name='ol-groupName' data-type='string' content=name) block content - main.content.content-alt#subscription-manage-group-root + main#subscription-manage-group-root.content.content-alt diff --git a/services/web/app/views/user_membership/group-members-react.pug b/services/web/app/views/user_membership/group-members-react.pug index 05327c4b6d..4020ebdf58 100644 --- a/services/web/app/views/user_membership/group-members-react.pug +++ b/services/web/app/views/user_membership/group-members-react.pug @@ -2,18 +2,34 @@ extends ../layout-react block entrypointVar - entrypoint = 'pages/user/subscription/group-management/group-members' - + block append meta - meta(name="ol-user", data-type="json", content=user) - meta(name="ol-users", data-type="json", content=users) - meta(name="ol-groupId", data-type="string", content=groupId) - meta(name="ol-groupName", data-type="string", content=name) - meta(name="ol-groupSize", data-type="json", content=groupSize) - meta(name="ol-managedUsersActive", data-type="boolean", content=managedUsersActive) - meta(name="ol-isUserGroupManager", data-type="boolean", content=isUserGroupManager) - meta(name="ol-groupSSOActive", data-type="boolean", content=groupSSOActive) - meta(name="ol-canUseFlexibleLicensing", data-type="boolean", content=canUseFlexibleLicensing) - meta(name="ol-canUseAddSeatsFeature", data-type="boolean", content=canUseAddSeatsFeature) + meta(name='ol-user' data-type='json' content=user) + meta(name='ol-users' data-type='json' content=users) + meta(name='ol-groupId' data-type='string' content=groupId) + meta(name='ol-groupName' data-type='string' content=name) + meta(name='ol-groupSize' data-type='json' content=groupSize) + meta( + name='ol-managedUsersActive' + data-type='boolean' + content=managedUsersActive + ) + meta( + name='ol-isUserGroupManager' + data-type='boolean' + content=isUserGroupManager + ) + meta(name='ol-groupSSOActive' data-type='boolean' content=groupSSOActive) + meta( + name='ol-canUseFlexibleLicensing' + data-type='boolean' + content=canUseFlexibleLicensing + ) + meta( + name='ol-canUseAddSeatsFeature' + data-type='boolean' + content=canUseAddSeatsFeature + ) block content - main.content.content-alt#subscription-manage-group-root + main#subscription-manage-group-root.content.content-alt diff --git a/services/web/app/views/user_membership/institution-managers-react.pug b/services/web/app/views/user_membership/institution-managers-react.pug index ee62fcd430..9793058f6f 100644 --- a/services/web/app/views/user_membership/institution-managers-react.pug +++ b/services/web/app/views/user_membership/institution-managers-react.pug @@ -4,10 +4,10 @@ block entrypointVar - entrypoint = 'pages/user/subscription/group-management/institution-managers' block append meta - meta(name="ol-user" data-type="json" content=user) - meta(name="ol-users", data-type="json", content=users) - meta(name="ol-groupId", data-type="string", content=groupId) - meta(name="ol-groupName", data-type="string", content=name) + meta(name='ol-user' data-type='json' content=user) + meta(name='ol-users' data-type='json' content=users) + meta(name='ol-groupId' data-type='string' content=groupId) + meta(name='ol-groupName' data-type='string' content=name) block content - main.content.content-alt#subscription-manage-group-root + main#subscription-manage-group-root.content.content-alt diff --git a/services/web/app/views/user_membership/new.pug b/services/web/app/views/user_membership/new.pug index c59837b107..4e52ea160a 100644 --- a/services/web/app/views/user_membership/new.pug +++ b/services/web/app/views/user_membership/new.pug @@ -4,20 +4,13 @@ block vars - bootstrap5PageStatus = 'disabled' block content - main.content.content-alt#main-content + main#main-content.content.content-alt .container .row .col-md-10.col-md-offset-1 h3 #{entityName} "#{entityId}" does not exists in v2 - form( - data-ol-regular-form - method='post', - action='' - ) - input(name="_csrf", type="hidden", value=csrfToken) - button.btn.btn-primary( - type="submit", - data-ol-disabled-inflight - ) - span(data-ol-inflight="idle") Create #{entityName} in v2 - span(hidden data-ol-inflight="pending") #{translate("creating")}… + form(data-ol-regular-form method='post' action='') + input(name='_csrf' type='hidden' value=csrfToken) + button.btn.btn-primary(type='submit' data-ol-disabled-inflight) + span(data-ol-inflight='idle') Create #{entityName} in v2 + span(hidden data-ol-inflight='pending') #{translate("creating")}… diff --git a/services/web/app/views/user_membership/publisher-managers-react.pug b/services/web/app/views/user_membership/publisher-managers-react.pug index a956e30c35..2f805079a7 100644 --- a/services/web/app/views/user_membership/publisher-managers-react.pug +++ b/services/web/app/views/user_membership/publisher-managers-react.pug @@ -4,10 +4,10 @@ block entrypointVar - entrypoint = 'pages/user/subscription/group-management/publisher-managers' block append meta - meta(name="ol-user" data-type="json" content=user) - meta(name="ol-users", data-type="json", content=users) - meta(name="ol-groupId", data-type="string", content=groupId) - meta(name="ol-groupName", data-type="string", content=name) + meta(name='ol-user' data-type='json' content=user) + meta(name='ol-users' data-type='json' content=users) + meta(name='ol-groupId' data-type='string' content=groupId) + meta(name='ol-groupName' data-type='string' content=name) block content - main.content.content-alt#subscription-manage-group-root + main#subscription-manage-group-root.content.content-alt diff --git a/services/web/modules/launchpad/app/views/launchpad.pug b/services/web/modules/launchpad/app/views/launchpad.pug index fdf0576c4a..ff917eeb74 100644 --- a/services/web/modules/launchpad/app/views/launchpad.pug +++ b/services/web/modules/launchpad/app/views/launchpad.pug @@ -2,39 +2,43 @@ extends ../../../../app/views/layout-marketing mixin launchpad-check(section) div(data-ol-launchpad-check=section) - span(data-ol-inflight="pending") + span(data-ol-inflight='pending') i.fa.fa-fw.fa-spinner.fa-spin span  #{translate('checking')} - - span(hidden data-ol-inflight="idle") - div(data-ol-result="success") + + span(hidden data-ol-inflight='idle') + div(data-ol-result='success') i.fa.fa-check span  #{translate('ok')} button.btn.btn-inline-link span.text-danger  #{translate('retry')} - div(hidden data-ol-result="error") + div(hidden data-ol-result='error') i.fa.fa-exclamation span  #{translate('error')} button.btn.btn-inline-link span.text-danger  #{translate('retry')} - div.alert.alert-danger + .alert.alert-danger span(data-ol-error) block entrypointVar - entrypoint = 'modules/launchpad/pages/launchpad' - + block vars - metadata = metadata || {} - bootstrap5PageStatus = 'disabled' block append meta - meta(name="ol-adminUserExists" data-type="boolean" content=adminUserExists) - meta(name="ol-ideJsPath" content=buildJsPath('ide.js')) + meta(name='ol-adminUserExists' data-type='boolean' content=adminUserExists) + meta(name='ol-ideJsPath' content=buildJsPath('ide.js')) block content - script(type="text/javascript", nonce=scriptNonce, src=(wsUrl || '/socket.io') + '/socket.io.js') + script( + type='text/javascript' + nonce=scriptNonce + src=(wsUrl || '/socket.io') + '/socket.io.js' + ) - .content.content-alt#main-content + #main-content.content.content-alt .container .row .col-md-8.col-md-offset-2 @@ -49,8 +53,6 @@ block content .row .col-md-8.col-md-offset-2 - - if !adminUserExists .row(data-ol-not-sent) @@ -62,37 +64,34 @@ block content form( data-ol-async-form data-ol-register-admin - action="/launchpad/register_admin" - method="POST" + action='/launchpad/register_admin' + method='POST' ) - input(name='_csrf', type='hidden', value=csrfToken) - +formMessages() + input(name='_csrf' type='hidden' value=csrfToken) + +formMessages .form-group label(for='email') #{translate("email")} input.form-control( - type='email', - name='email', - placeholder="email@example.com" - autocomplete="username" - required, - autofocus="true" + name='email' + type='email' + placeholder='email@example.com' + autocomplete='username' + required + autofocus='true' ) .form-group label(for='password') #{translate("password")} - input.form-control#passwordField( - type='password', - name='password', - placeholder="********", - autocomplete="new-password" - required, + input#passwordField.form-control( + name='password' + type='password' + placeholder='********' + autocomplete='new-password' + required ) .actions - button.btn-primary.btn( - type='submit' - data-ol-disabled-inflight - ) - span(data-ol-inflight="idle") #{translate("register")} - span(hidden data-ol-inflight="pending") #{translate("registering")}… + button.btn-primary.btn(type='submit' data-ol-disabled-inflight) + span(data-ol-inflight='idle') #{translate("register")} + span(hidden data-ol-inflight='pending') #{translate("registering")}… // Ldap Form if authMethod === 'ldap' @@ -103,28 +102,25 @@ block content form( data-ol-async-form data-ol-register-admin - action="/launchpad/register_ldap_admin" - method="POST" + action='/launchpad/register_ldap_admin' + method='POST' ) - input(name='_csrf', type='hidden', value=csrfToken) - +formMessages() + input(name='_csrf' type='hidden' value=csrfToken) + +formMessages .form-group label(for='email') #{translate("email")} input.form-control( - type='email', - name='email', - placeholder="email@example.com" - autocomplete="username" - required, - autofocus="true" + name='email' + type='email' + placeholder='email@example.com' + autocomplete='username' + required + autofocus='true' ) .actions - button.btn-primary.btn( - type='submit' - data-ol-disabled-inflight - ) - span(data-ol-inflight="idle") #{translate("register")} - span(hidden data-ol-inflight="pending") #{translate("registering")}… + button.btn-primary.btn(type='submit' data-ol-disabled-inflight) + span(data-ol-inflight='idle') #{translate("register")} + span(hidden data-ol-inflight='pending') #{translate("registering")}… // Saml Form if authMethod === 'saml' @@ -135,28 +131,25 @@ block content form( data-ol-async-form data-ol-register-admin - action="/launchpad/register_saml_admin" - method="POST" + action='/launchpad/register_saml_admin' + method='POST' ) - input(name='_csrf', type='hidden', value=csrfToken) - +formMessages() + input(name='_csrf' type='hidden' value=csrfToken) + +formMessages .form-group label(for='email') #{translate("email")} input.form-control( - type='email', - name='email', - placeholder="email@example.com" - autocomplete="username" - required, - autofocus="true" + name='email' + type='email' + placeholder='email@example.com' + autocomplete='username' + required + autofocus='true' ) .actions - button.btn-primary.btn( - type='submit' - data-ol-disabled-inflight - ) - span(data-ol-inflight="idle") #{translate("register")} - span(hidden data-ol-inflight="pending") #{translate("registering")}… + button.btn-primary.btn(type='submit' data-ol-disabled-inflight) + span(data-ol-inflight='idle') #{translate("register")} + span(hidden data-ol-inflight='pending') #{translate("registering")}… br @@ -164,7 +157,6 @@ block content if adminUserExists .row .col-md-12.status-indicators - h2 #{translate('status_checks')} @@ -185,42 +177,31 @@ block content h3 #{translate('send_test_email')} form.form( data-ol-async-form - action="/launchpad/send_test_email" - method="POST" + action='/launchpad/send_test_email' + method='POST' ) .form-group - label(for="email") Email - input.form-control( - type="text" - id="email" - name="email" - required - ) - button.btn-primary.btn( - type='submit' - data-ol-disabled-inflight - ) - span(data-ol-inflight="idle") #{translate("send")} - span(hidden data-ol-inflight="pending") #{translate("sending")}… + label(for='email') Email + input.form-control(name='email' type='text' id='email' required) + button.btn-primary.btn(type='submit' data-ol-disabled-inflight) + span(data-ol-inflight='idle') #{translate("send")} + span(hidden data-ol-inflight='pending') #{translate("sending")}… p - +formMessages() - - + +formMessages hr.thin - .row .col-md-12 .text-center br p - a(href="/admin").btn.btn-info + a.btn.btn-info(href='/admin') | Go To Admin Panel |   - a(href="/project").btn.btn-primary + a.btn.btn-primary(href='/project') | Start Using #{settings.appName} br diff --git a/services/web/modules/user-activate/app/views/user/activate.pug b/services/web/modules/user-activate/app/views/user/activate.pug index deebe0b08a..82671f90a7 100644 --- a/services/web/modules/user-activate/app/views/user/activate.pug +++ b/services/web/modules/user-activate/app/views/user/activate.pug @@ -6,14 +6,14 @@ block vars include ../../../../../app/views/_mixins/material_symbol block content - main.content.content-alt#main-content + main#main-content.content.content-alt .container - div.col-lg-6.col-xl-4.m-auto + .col-lg-6.col-xl-4.m-auto .notification-list - .notification.notification-type-success(aria-live="off" role="alert") + .notification.notification-type-success(aria-live='off' role='alert') .notification-content-and-cta .notification-icon - +material-symbol("check_circle") + +material-symbol('check_circle') .notification-content p | #{translate("nearly_activated")} @@ -21,12 +21,12 @@ block content h1.h3 #{translate("please_set_a_password")} form( + name='activationForm' data-ol-async-form - name="activationForm", - action="/user/password/set", - method="POST", + action='/user/password/set' + method='POST' ) - +formMessages() + +formMessages +customFormMessage('token-expired', 'danger') | #{translate("activation_token_expired")} @@ -34,43 +34,39 @@ block content +customFormMessage('invalid-password', 'danger') | #{translate('invalid_password')} - input(name='_csrf', type='hidden', value=csrfToken) - input( - type="hidden", - name="passwordResetToken", - value=token - ) + input(name='_csrf' type='hidden' value=csrfToken) + input(name='passwordResetToken' type='hidden' value=token) .form-group label(for='emailField') #{translate("email")} - input.form-control#emailField( - aria-label="email", - type='email', - name='email', - placeholder="email@example.com", - autocomplete="username" + input#emailField.form-control( + name='email' + aria-label='email' + type='email' + placeholder='email@example.com' + autocomplete='username' value=email - required, + required disabled ) .form-group label(for='passwordField') #{translate("password")} - input.form-control#passwordField( - type='password', - name='password', - placeholder="********", - autocomplete="new-password", - autofocus, - required, + input#passwordField.form-control( + name='password' + type='password' + placeholder='********' + autocomplete='new-password' + autofocus + required minlength=settings.passwordStrengthOptions.length.min ) .actions button.btn.btn-primary( - type='submit', + type='submit' data-ol-disabled-inflight aria-label=translate('activate') ) - span(data-ol-inflight="idle") + span(data-ol-inflight='idle') | #{translate('activate')} - span(hidden data-ol-inflight="pending") + span(hidden data-ol-inflight='pending') | #{translate('activating')}… diff --git a/services/web/modules/user-activate/app/views/user/register.pug b/services/web/modules/user-activate/app/views/user/register.pug index 0f3e5f2f91..27e6f8215c 100644 --- a/services/web/modules/user-activate/app/views/user/register.pug +++ b/services/web/modules/user-activate/app/views/user/register.pug @@ -4,9 +4,9 @@ block entrypointVar - entrypoint = 'modules/user-activate/pages/user-activate-page' block append meta - meta(name="ol-user" data-type="json" content=user) + meta(name='ol-user' data-type='json' content=user) block content - .content.content-alt#main-content + #main-content.content.content-alt .container #user-activate-register-container diff --git a/services/web/package.json b/services/web/package.json index 59825e0e68..bdc42673bb 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -28,6 +28,8 @@ "format:fix": "prettier --write $PWD/'**/*.{js,jsx,mjs,ts,tsx,json}'", "format:styles": "prettier --list-different $PWD/'**/*.{css,less,scss}'", "format:styles:fix": "prettier --write $PWD/'**/*.{css,less,scss}'", + "format:pug": "prettier --list-different $PWD/'**/*.pug'", + "format:pug:fix": "prettier --write $PWD/'**/*.pug'", "lint": "eslint --max-warnings 0 --format unix --ext .js,.jsx,.mjs,.ts,.tsx .", "lint:fix": "eslint --fix --ext .js,.jsx,.mjs,.ts,.tsx .", "lint:styles": "stylelint '**/*.scss'", @@ -209,6 +211,7 @@ "@pollyjs/adapter-node-http": "^6.0.6", "@pollyjs/core": "^6.0.6", "@pollyjs/persister-fs": "^6.0.6", + "@prettier/plugin-pug": "^3.4.0", "@replit/codemirror-emacs": "overleaf/codemirror-emacs#4394c03858f27053f8768258e9493866e06e938e", "@replit/codemirror-indentation-markers": "overleaf/codemirror-indentation-markers#78264032eb286bc47871569ae87bff5ca1c6c161", "@replit/codemirror-vim": "overleaf/codemirror-vim#1bef138382d948018f3f9b8a4d7a70ab61774e4b", From 46555d27b07a9985fe6c569a2f3a644d400568a0 Mon Sep 17 00:00:00 2001 From: Antoine Clausse Date: Mon, 23 Jun 2025 11:09:08 +0200 Subject: [PATCH 003/250] [web] Add `window.` prefix to globals and add `no-restricted-globals` rule (#26422) * Add `no-restricted-globals` eslint rule Co-authored-by: Rebeka * Change `self` to `window.self` * Change `innerWidth` to `window.innerWidth` * Change `confirm` to `window.confirm` * Change `location` to `window.location` * Use `location` from `useLocation` hook * Use location from useLocation hook Co-authored-by: Antoine * Disable no-restricted-globals eslint rule for use of 'self' * Use `confusing-browser-globals` from npm * Prevent unexpected globals in workers, using `no-undef` * Use `self` as a global in workers * Use unexpected globals in workers, using `no-restricted-globals` in workers --------- Co-authored-by: Rebeka Co-authored-by: Rebeka GitOrigin-RevId: 526986799f5f2edf53c7d978fa85c1e98189565f --- package-lock.json | 22 +++++++++++++++++++ services/web/.eslintrc.js | 17 ++++++++++++++ .../emails/add-secondary-email-prompt.tsx | 2 ++ .../components/emails/confirm-email-form.tsx | 2 ++ .../components/sharing-updates-root.tsx | 8 ++++--- .../js/infrastructure/error-reporter.ts | 2 +- services/web/package.json | 2 ++ 7 files changed, 51 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 53388a5732..c38221dd63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18024,6 +18024,13 @@ "proto-list": "~1.2.1" } }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true, + "license": "MIT" + }, "node_modules/connect-flash": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/connect-flash/-/connect-flash-0.1.1.tgz", @@ -45366,6 +45373,7 @@ "chartjs-plugin-datalabels": "^2.2.0", "cheerio": "^1.0.0-rc.3", "classnames": "^2.2.6", + "confusing-browser-globals": "^1.0.11", "cookie-signature": "^1.2.1", "copy-webpack-plugin": "^11.0.0", "core-js": "^3.41.0", @@ -45392,6 +45400,7 @@ "formik": "^2.2.9", "fuse.js": "^3.0.0", "glob": "^7.1.6", + "globals": "^16.2.0", "handlebars": "^4.7.8", "handlebars-loader": "^1.7.3", "html-webpack-plugin": "^5.5.3", @@ -46342,6 +46351,19 @@ "node": ">=18.11.0" } }, + "services/web/node_modules/globals": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "services/web/node_modules/google-auth-library": { "version": "8.7.0", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.7.0.tgz", diff --git a/services/web/.eslintrc.js b/services/web/.eslintrc.js index ef3cf11de5..7dd154c942 100644 --- a/services/web/.eslintrc.js +++ b/services/web/.eslintrc.js @@ -1,3 +1,7 @@ +const _ = require('lodash') +const confusingBrowserGlobals = require('confusing-browser-globals') +const globals = require('globals') + module.exports = { root: true, parser: '@typescript-eslint/parser', @@ -19,6 +23,7 @@ module.exports = { }, rules: { 'no-constant-binary-expression': 'error', + 'no-restricted-globals': ['error', ...confusingBrowserGlobals], // do not allow importing of implicit dependencies. 'import/no-extraneous-dependencies': 'error', @@ -531,5 +536,17 @@ module.exports = { 'no-console': 'error', }, }, + { + files: ['**/*.worker.{js,ts}'], + rules: { + 'no-restricted-globals': [ + 'error', + ..._.difference( + Object.keys({ ...globals.browser, ...globals.node }), + Object.keys(globals.worker) + ), + ], + }, + }, ], } diff --git a/services/web/frontend/js/features/settings/components/emails/add-secondary-email-prompt.tsx b/services/web/frontend/js/features/settings/components/emails/add-secondary-email-prompt.tsx index 04bc4edbd1..8355138a14 100644 --- a/services/web/frontend/js/features/settings/components/emails/add-secondary-email-prompt.tsx +++ b/services/web/frontend/js/features/settings/components/emails/add-secondary-email-prompt.tsx @@ -8,6 +8,7 @@ import MaterialIcon from '@/shared/components/material-icon' import { sendMB } from '@/infrastructure/event-tracking' import { ReCaptcha2 } from '../../../../shared/components/recaptcha-2' import { useRecaptcha } from '../../../../shared/hooks/use-recaptcha' +import { useLocation } from '@/shared/hooks/use-location' import { postJSON } from '../../../../infrastructure/fetch-json' import RecaptchaConditions from '@/shared/components/recaptcha-conditions' @@ -25,6 +26,7 @@ export function AddSecondaryEmailPrompt() { const [error, setError] = useState() const [isSubmitting, setIsSubmitting] = useState(false) const { ref: recaptchaRef, getReCaptchaToken } = useRecaptcha() + const location = useLocation() if (!isReady) { return null diff --git a/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx b/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx index d82a43315c..bf08b5dcfd 100644 --- a/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx +++ b/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx @@ -9,6 +9,7 @@ import MaterialIcon from '@/shared/components/material-icon' import { sendMB } from '@/infrastructure/event-tracking' import OLFormLabel from '@/features/ui/components/ol/ol-form-label' import OLButton from '@/features/ui/components/ol/ol-button' +import { useLocation } from '@/shared/hooks/use-location' type Feedback = { type: 'input' | 'alert' @@ -267,6 +268,7 @@ function ConfirmEmailSuccessfullForm({ successButtonText: string redirectTo: string }) { + const location = useLocation() const submitHandler = (e: FormEvent) => { e.preventDefault() location.assign(redirectTo) diff --git a/services/web/frontend/js/features/token-access/components/sharing-updates-root.tsx b/services/web/frontend/js/features/token-access/components/sharing-updates-root.tsx index e7b0d96c2a..ade94ba304 100644 --- a/services/web/frontend/js/features/token-access/components/sharing-updates-root.tsx +++ b/services/web/frontend/js/features/token-access/components/sharing-updates-root.tsx @@ -12,6 +12,7 @@ import { sendMB } from '@/infrastructure/event-tracking' import LeaveProjectModal from './leave-project-modal' import OLButton from '@/features/ui/components/ol/ol-button' +import { useLocation } from '@/shared/hooks/use-location' function SharingUpdatesRoot() { const [showModal, setShowModal] = useState(false) @@ -20,6 +21,7 @@ function SharingUpdatesRoot() { const { isLoading, isSuccess, isError, runAsync } = useAsync() const projectId = getMeta('ol-project_id') + const location = useLocation() const joinProject = useCallback(() => { sendMB('notification-click', { name: 'link-sharing-collaborator', @@ -30,7 +32,7 @@ function SharingUpdatesRoot() { location.assign(`/project/${projectId}`) }) .catch(debugConsole.error) - }, [runAsync, projectId]) + }, [runAsync, projectId, location]) const viewProject = useCallback(() => { sendMB('notification-click', { @@ -42,7 +44,7 @@ function SharingUpdatesRoot() { location.assign(`/project/${projectId}`) }) .catch(debugConsole.error) - }, [runAsync, projectId]) + }, [runAsync, projectId, location]) const leaveProject = useCallback(() => { sendMB('notification-click', { @@ -54,7 +56,7 @@ function SharingUpdatesRoot() { location.assign('/project') }) .catch(debugConsole.error) - }, [runAsync, projectId]) + }, [runAsync, projectId, location]) if (!isReady) { return null diff --git a/services/web/frontend/js/infrastructure/error-reporter.ts b/services/web/frontend/js/infrastructure/error-reporter.ts index 5d5734535a..e70833860c 100644 --- a/services/web/frontend/js/infrastructure/error-reporter.ts +++ b/services/web/frontend/js/infrastructure/error-reporter.ts @@ -78,7 +78,7 @@ function sentryReporter() { const refererUrl = new URL(event.request.headers.Referer) if ( - refererUrl.hostname === location.hostname && + refererUrl.hostname === window.location.hostname && refererUrl.pathname.startsWith('/read/') ) { refererUrl.pathname = '/read/' diff --git a/services/web/package.json b/services/web/package.json index bdc42673bb..d946c62151 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -278,6 +278,7 @@ "chartjs-plugin-datalabels": "^2.2.0", "cheerio": "^1.0.0-rc.3", "classnames": "^2.2.6", + "confusing-browser-globals": "^1.0.11", "cookie-signature": "^1.2.1", "copy-webpack-plugin": "^11.0.0", "core-js": "^3.41.0", @@ -304,6 +305,7 @@ "formik": "^2.2.9", "fuse.js": "^3.0.0", "glob": "^7.1.6", + "globals": "^16.2.0", "handlebars": "^4.7.8", "handlebars-loader": "^1.7.3", "html-webpack-plugin": "^5.5.3", From 2d5a3efc128b0222296dbe6474396d69bdd73b50 Mon Sep 17 00:00:00 2001 From: Antoine Clausse Date: Mon, 23 Jun 2025 11:09:26 +0200 Subject: [PATCH 004/250] [web] Add compilation indicator favicon (#25990) * Import changes from Hackathon https://github.com/overleaf/internal/pull/24501 * Update compile status: allow errors * Update favicons. Use the ones from Figma * Optimize and reuse path from favicon.svg * Clear status favicon after 5s on active tab * Rename hook from useCompileNotification to useStatusFavicon * Add tests * Revert changes to favicon.svg * Query favicon on document.head GitOrigin-RevId: 3972b1981abaf6c80273e0ed5b1bc05eb51bd689 --- .../ide-react/components/layout/ide-page.tsx | 2 + .../ide-react/hooks/use-status-favicon.ts | 75 ++++++++++ services/web/public/favicon-compiled.svg | 9 ++ services/web/public/favicon-compiling.svg | 9 ++ services/web/public/favicon-error.svg | 9 ++ .../ide-react/unit/use-status-favicon.test.ts | 131 ++++++++++++++++++ 6 files changed, 235 insertions(+) create mode 100644 services/web/frontend/js/features/ide-react/hooks/use-status-favicon.ts create mode 100644 services/web/public/favicon-compiled.svg create mode 100644 services/web/public/favicon-compiling.svg create mode 100644 services/web/public/favicon-error.svg create mode 100644 services/web/test/frontend/features/ide-react/unit/use-status-favicon.test.ts diff --git a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx index 6a03d5b205..488d5476a7 100644 --- a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx +++ b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx @@ -16,6 +16,7 @@ import { } from '@/features/ide-redesign/utils/new-editor-utils' import EditorSurvey from '../editor-survey' import { useFeatureFlag } from '@/shared/context/split-test-context' +import { useStatusFavicon } from '@/features/ide-react/hooks/use-status-favicon' const MainLayoutNew = lazy( () => import('@/features/ide-redesign/components/main-layout') @@ -30,6 +31,7 @@ export default function IdePage() { useEditingSessionHeartbeat() // send a batched event when user is active useRegisterUserActivity() // record activity and ensure connection when user is active useHasLintingError() // pass editor:lint hasLintingError to the compiler + useStatusFavicon() // update the favicon based on the compile status const newEditor = useIsNewEditorEnabled() const canAccessNewEditor = canUseNewEditor() diff --git a/services/web/frontend/js/features/ide-react/hooks/use-status-favicon.ts b/services/web/frontend/js/features/ide-react/hooks/use-status-favicon.ts new file mode 100644 index 0000000000..c65d49e042 --- /dev/null +++ b/services/web/frontend/js/features/ide-react/hooks/use-status-favicon.ts @@ -0,0 +1,75 @@ +import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context' +import { useEffect, useState } from 'react' +import usePreviousValue from '@/shared/hooks/use-previous-value' + +const RESET_AFTER_MS = 5_000 + +const COMPILE_ICONS = { + ERROR: '/favicon-error.svg', + COMPILING: '/favicon-compiling.svg', + COMPILED: '/favicon-compiled.svg', + UNCOMPILED: '/favicon.svg', +} + +type CompileStatus = keyof typeof COMPILE_ICONS + +const useCompileStatus = (): CompileStatus => { + const compileContext = useCompileContext() + if (compileContext.uncompiled) return 'UNCOMPILED' + if (compileContext.compiling) return 'COMPILING' + if (compileContext.error) return 'ERROR' + return 'COMPILED' +} + +const removeFavicon = () => { + const existingFavicons = document.head.querySelectorAll( + "link[rel='icon']" + ) as NodeListOf + existingFavicons.forEach(favicon => { + if (favicon.href.endsWith('.svg')) favicon.parentNode?.removeChild(favicon) + }) +} + +const updateFavicon = (status: CompileStatus = 'UNCOMPILED') => { + removeFavicon() + const linkElement = document.createElement('link') + linkElement.rel = 'icon' + linkElement.href = COMPILE_ICONS[status] + linkElement.type = 'image/svg+xml' + linkElement.setAttribute('data-compile-status', 'true') + document.head.appendChild(linkElement) +} + +const isActive = () => !document.hidden + +const useIsWindowActive = () => { + const [isWindowActive, setIsWindowActive] = useState(isActive()) + useEffect(() => { + const handleVisibilityChange = () => setIsWindowActive(isActive()) + document.addEventListener('visibilitychange', handleVisibilityChange) + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange) + } + }, []) + return isWindowActive +} + +export const useStatusFavicon = () => { + const compileStatus = useCompileStatus() + const previousCompileStatus = usePreviousValue(compileStatus) + const isWindowActive = useIsWindowActive() + + useEffect(() => { + if (previousCompileStatus !== compileStatus) { + return updateFavicon(compileStatus) + } + + if ( + isWindowActive && + (compileStatus === 'COMPILED' || compileStatus === 'ERROR') + ) { + const timeout = setTimeout(updateFavicon, RESET_AFTER_MS) + return () => clearTimeout(timeout) + } + }, [compileStatus, isWindowActive, previousCompileStatus]) +} diff --git a/services/web/public/favicon-compiled.svg b/services/web/public/favicon-compiled.svg new file mode 100644 index 0000000000..8bee787bb2 --- /dev/null +++ b/services/web/public/favicon-compiled.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/services/web/public/favicon-compiling.svg b/services/web/public/favicon-compiling.svg new file mode 100644 index 0000000000..fed675637c --- /dev/null +++ b/services/web/public/favicon-compiling.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/services/web/public/favicon-error.svg b/services/web/public/favicon-error.svg new file mode 100644 index 0000000000..5b88401356 --- /dev/null +++ b/services/web/public/favicon-error.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/services/web/test/frontend/features/ide-react/unit/use-status-favicon.test.ts b/services/web/test/frontend/features/ide-react/unit/use-status-favicon.test.ts new file mode 100644 index 0000000000..f3a9294eb1 --- /dev/null +++ b/services/web/test/frontend/features/ide-react/unit/use-status-favicon.test.ts @@ -0,0 +1,131 @@ +import { expect } from 'chai' +import sinon from 'sinon' +import { renderHook } from '@testing-library/react' +import * as CompileContext from '@/shared/context/detach-compile-context' + +import { useStatusFavicon } from '@/features/ide-react/hooks/use-status-favicon' + +type Compilation = { uncompiled: boolean; compiling: boolean; error: boolean } + +describe('useStatusFavicon', function () { + let mockUseDetachCompileContext: sinon.SinonStub + let clock: sinon.SinonFakeTimers + let originalHidden: PropertyDescriptor | undefined + + const setCompilation = (compileContext: Compilation) => { + mockUseDetachCompileContext.returns(compileContext) + } + const setHidden = (hidden: boolean) => { + Object.defineProperty(document, 'hidden', { + writable: true, + configurable: true, + value: hidden, + }) + document.dispatchEvent(new Event('visibilitychange')) + } + + const getFaviconElements = () => + document.querySelectorAll('link[data-compile-status="true"]') + + const getCurrentFaviconHref = () => { + const favicon = document.querySelector( + 'link[data-compile-status="true"]' + ) as HTMLLinkElement + return favicon?.href || null + } + + beforeEach(function () { + mockUseDetachCompileContext = sinon.stub( + CompileContext, + 'useDetachCompileContext' + ) + + // Mock timers for timeout testing + clock = sinon.useFakeTimers() + + // Store original document.hidden descriptor + originalHidden = Object.getOwnPropertyDescriptor( + Document.prototype, + 'hidden' + ) + + // Clean up any existing favicon elements + document + .querySelectorAll('link[data-compile-status="true"]') + .forEach(el => el.remove()) + }) + + afterEach(function () { + sinon.restore() + clock.restore() + + // Restore original document.hidden + if (originalHidden) { + Object.defineProperty(document, 'hidden', originalHidden) + } + + // Clean up favicon elements + document + .querySelectorAll('link[data-compile-status="true"]') + .forEach(el => el.remove()) + }) + + it('updates favicon to reflect status: UNCOMPILED', function () { + setCompilation({ uncompiled: true, compiling: false, error: false }) + renderHook(() => useStatusFavicon()) + expect(getCurrentFaviconHref()).to.include('/favicon.svg') + }) + + it('updates favicon to reflect status: COMPILING', function () { + setCompilation({ uncompiled: false, compiling: true, error: false }) + renderHook(() => useStatusFavicon()) + expect(getCurrentFaviconHref()).to.include('/favicon-compiling.svg') + }) + + it('updates favicon to reflect status: COMPILED', function () { + setCompilation({ uncompiled: false, compiling: false, error: false }) + renderHook(() => useStatusFavicon()) + expect(getCurrentFaviconHref()).to.include('/favicon-compiled.svg') + }) + + it('updates favicon to reflect status: ERROR', function () { + setCompilation({ uncompiled: false, compiling: false, error: true }) + renderHook(() => useStatusFavicon()) + expect(getCurrentFaviconHref()).to.include('/favicon-error.svg') + }) + + it('keeps the COMPILED favicon for 5 seconds when the window is active', function () { + setCompilation({ uncompiled: false, compiling: false, error: false }) + const { rerender } = renderHook(() => useStatusFavicon()) + setHidden(false) + rerender() + expect(getCurrentFaviconHref()).to.include('/favicon-compiled.svg') + clock.tick(4500) + expect(getCurrentFaviconHref()).to.include('/favicon-compiled.svg') + clock.tick(1000) + expect(getCurrentFaviconHref()).to.include('/favicon.svg') + }) + + it('keeps the COMPILED favicon forever when the window is hidden', function () { + setCompilation({ uncompiled: false, compiling: false, error: false }) + const { rerender } = renderHook(() => useStatusFavicon()) + setHidden(true) + rerender() + + expect(getCurrentFaviconHref()).to.include('/favicon-compiled.svg') + clock.tick(90000) + expect(getCurrentFaviconHref()).to.include('/favicon-compiled.svg') + }) + + it('should only have one favicon element at a time', function () { + setCompilation({ uncompiled: true, compiling: false, error: false }) + const { rerender } = renderHook(() => useStatusFavicon()) + expect(getFaviconElements()).to.have.length(1) + expect(getCurrentFaviconHref()).to.include('/favicon.svg') + + setCompilation({ uncompiled: false, compiling: true, error: false }) + rerender() + expect(getFaviconElements()).to.have.length(1) + expect(getCurrentFaviconHref()).to.include('/favicon-compiling.svg') + }) +}) From de1ab31bfd877a3874320938713ce0884a34d114 Mon Sep 17 00:00:00 2001 From: Tim Down <158919+timdown@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:25:35 +0100 Subject: [PATCH 005/250] Merge pull request #26469 from overleaf/td-bs5-sp-ce-register Migrate SP/CE registration page to Bootstrap 5 GitOrigin-RevId: 5e99a6a091d725ea3ab54e7cf6a4d1ea4f6bfab6 --- services/web/app/views/user/register.pug | 26 ++++++++-------- .../frontend/stylesheets/app/register.less | 31 ------------------- .../bootstrap-5/pages/register.scss | 8 +++-- .../web/frontend/stylesheets/main-style.less | 1 - 4 files changed, 18 insertions(+), 48 deletions(-) delete mode 100644 services/web/frontend/stylesheets/app/register.less diff --git a/services/web/app/views/user/register.pug b/services/web/app/views/user/register.pug index 8aa40e8b35..ff53002ccb 100644 --- a/services/web/app/views/user/register.pug +++ b/services/web/app/views/user/register.pug @@ -1,13 +1,10 @@ extends ../layout-marketing -block vars - - bootstrap5PageStatus = 'disabled' - block content main#main-content.content.content-alt .container .row - .registration_message + .registration-message if sharedProjectData.user_first_name !== undefined h1 #{translate("user_wants_you_to_see_project", {username:sharedProjectData.user_first_name, projectname:""})} em #{sharedProjectData.project_name} @@ -15,21 +12,24 @@ block content | #{translate("join_sl_to_view_project")}. div | #{translate("already_have_sl_account")} + | a(href='/login') #{translate("login_here")} else if newTemplateData.templateName !== undefined h1 #{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 + .col-lg-8.offset-lg-2.col-xl-6.offset-xl-3 .card - .page-header - h1 #{translate("register")} - p - | Please contact - | - strong #{settings.adminEmail} - | - | to create an account. + .card-body + .page-header + h1 #{translate("register")} + p + | Please contact + | + strong #{settings.adminEmail} + | + | to create an account. diff --git a/services/web/frontend/stylesheets/app/register.less b/services/web/frontend/stylesheets/app/register.less deleted file mode 100644 index 61c7b79102..0000000000 --- a/services/web/frontend/stylesheets/app/register.less +++ /dev/null @@ -1,31 +0,0 @@ -.registration_message { - text-align: center; - padding-bottom: 20px; -} -// for focus-registration and focus-login split test variant -.registration_logo { - width: 130px; - padding: 8px 0; -} - -.website-redesign { - .login-register-header-focus { - padding-top: unset; - } -} - -.login-register-header-heading-focus { - color: @neutral-90; - margin-bottom: 0; -} - -.website-redesign { - .login-register-form-focus { - padding: @line-height-computed 0 0 0; - border-bottom: unset; - border-bottom: solid 1px @hr-border; - &:last-child { - border-bottom-width: 0; - } - } -} diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/register.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/register.scss index 85711e1609..90a90f32ce 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/register.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/register.scss @@ -1,3 +1,8 @@ +.registration-message { + text-align: center; + padding-bottom: var(--spacing-07); +} + .register-container { h1 { @include heading-sm; @@ -33,9 +38,6 @@ } .registration-message { - text-align: center; - padding-bottom: var(--spacing-07); - .registration-message-heading { color: var(--neutral-70); font-size: var(--font-size-05); diff --git a/services/web/frontend/stylesheets/main-style.less b/services/web/frontend/stylesheets/main-style.less index 040c6ac695..85368ea36e 100644 --- a/services/web/frontend/stylesheets/main-style.less +++ b/services/web/frontend/stylesheets/main-style.less @@ -104,7 +104,6 @@ @import 'app/plans.less'; @import 'app/recurly.less'; @import 'app/bonus.less'; -@import 'app/register.less'; @import 'app/blog.less'; @import 'app/features.less'; @import 'app/templates.less'; From ab140f578d5d41bdd761ef6c5d996dcacb4ede4d Mon Sep 17 00:00:00 2001 From: Tim Down <158919+timdown@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:25:47 +0100 Subject: [PATCH 006/250] Merge pull request #26244 from overleaf/td-limit-browser-translate-ide Prevent browser translation of stuff that shouldn't be translated in IDE page GitOrigin-RevId: 96a75b51c3c8efc4cbcec7eb17d9e331a03e2c96 --- services/web/app/views/_metadata.pug | 6 +++--- .../chat/components/message-content.tsx | 2 +- .../js/features/chat/components/message.tsx | 2 +- .../components/dictionary-modal-content.tsx | 4 +++- .../components/left-menu-button.tsx | 8 ++++--- .../components/settings/settings-compiler.tsx | 1 + .../components/settings/settings-document.tsx | 1 + .../settings/settings-editor-theme.tsx | 1 + .../settings/settings-font-family.tsx | 1 + .../settings/settings-image-name.tsx | 1 + .../settings/settings-menu-select.tsx | 3 +++ .../settings/settings-pdf-viewer.tsx | 1 + .../components/online-users-widget.tsx | 1 + .../file-tree/components/file-tree-doc.tsx | 1 + .../file-tree/components/file-tree-folder.tsx | 1 + .../components/change-list/changes.tsx | 2 ++ .../components/change-list/tag-tooltip.tsx | 21 +++++++++++++------ .../user-name-with-colored-badge.tsx | 10 ++++++++- .../file-tree/history-file-tree-doc.tsx | 1 + .../file-tree/history-file-tree-folder.tsx | 1 + .../ide-redesign/components/breadcrumbs.tsx | 2 +- .../editor-theme-setting.tsx | 1 + .../font-family-setting.tsx | 1 + .../compiler-settings/compiler-setting.tsx | 1 + .../compiler-settings/image-name-setting.tsx | 1 + .../root-document-setting.tsx | 1 + .../components/settings/dropdown-setting.tsx | 3 +++ .../editor-settings/pdf-viewer-setting.tsx | 1 + .../components/toolbar/project-title.tsx | 4 +++- .../outline/components/outline-item.tsx | 1 + .../components/pdf-log-entry-raw-content.tsx | 2 +- .../components/preview-log-entry-header.tsx | 8 +++++-- .../components/review-panel-entry-user.tsx | 2 +- .../review-panel-expandable-content.tsx | 3 +++ .../components/review-panel-message.tsx | 1 + .../review-panel-resolved-thread.tsx | 6 +++++- .../extensions/toolbar/toolbar-panel.ts | 2 +- .../components/collapsible-file-header.tsx | 1 + .../acceptance/src/ProjectInviteTests.mjs | 8 ++++--- .../src/helpers/expectErrorResponse.mjs | 2 +- .../src/steps/100_loadProjectDashboard.js | 3 ++- 41 files changed, 94 insertions(+), 29 deletions(-) diff --git a/services/web/app/views/_metadata.pug b/services/web/app/views/_metadata.pug index 6d7c599546..b28ca6abd2 100644 --- a/services/web/app/views/_metadata.pug +++ b/services/web/app/views/_metadata.pug @@ -1,10 +1,10 @@ //- Title if metadata && metadata.title - title= metadata.title + ' - ' + settings.appName + ', ' + translate('online_latex_editor') + title(translate='no')= 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') + title(translate='no')= settings.appName + ', ' + translate('online_latex_editor') meta( name='twitter:title' content=settings.appName + ', ' + translate('online_latex_editor') @@ -14,7 +14,7 @@ else if typeof title == 'undefined' content=settings.appName + ', ' + translate('online_latex_editor') ) else - title= translate(title) + ' - ' + settings.appName + ', ' + translate('online_latex_editor') + title(translate='no')= 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)) diff --git a/services/web/frontend/js/features/chat/components/message-content.tsx b/services/web/frontend/js/features/chat/components/message-content.tsx index ad2e6a623f..3d86b4be68 100644 --- a/services/web/frontend/js/features/chat/components/message-content.tsx +++ b/services/web/frontend/js/features/chat/components/message-content.tsx @@ -34,7 +34,7 @@ const MessageContent: FC<{ content: string }> = ({ content }) => { }, [content, mounted]) return ( -

+

{content}

) diff --git a/services/web/frontend/js/features/chat/components/message.tsx b/services/web/frontend/js/features/chat/components/message.tsx index aa2779f1e8..8a410c0d9d 100644 --- a/services/web/frontend/js/features/chat/components/message.tsx +++ b/services/web/frontend/js/features/chat/components/message.tsx @@ -29,7 +29,7 @@ function Message({ message, fromSelf }: MessageProps) { return (
{!fromSelf && ( -
+
{message.user.first_name || message.user.email}
)} diff --git a/services/web/frontend/js/features/dictionary/components/dictionary-modal-content.tsx b/services/web/frontend/js/features/dictionary/components/dictionary-modal-content.tsx index 38795e145c..52664de105 100644 --- a/services/web/frontend/js/features/dictionary/components/dictionary-modal-content.tsx +++ b/services/web/frontend/js/features/dictionary/components/dictionary-modal-content.tsx @@ -68,7 +68,9 @@ export default function DictionaryModalContent({
    {[...learnedWords].sort(wordsSortFunction).map(learnedWord => (
  • - {learnedWord} + + {learnedWord} + ['translate'] } function LeftMenuButtonIcon({ @@ -34,12 +35,13 @@ export default function LeftMenuButton({ disabledAccesibilityText, type = 'button', href, + translate, }: PropsWithChildren) { if (disabled) { return (
    - {children} + {children} {disabledAccesibilityText ? ( {disabledAccesibilityText} ) : null} @@ -51,7 +53,7 @@ export default function LeftMenuButton({ return ( ) } else { @@ -63,7 +65,7 @@ export default function LeftMenuButton({ className="left-menu-button" > - {children} + {children} ) } diff --git a/services/web/frontend/js/features/editor-left-menu/components/settings/settings-compiler.tsx b/services/web/frontend/js/features/editor-left-menu/components/settings/settings-compiler.tsx index 8d7076ebd4..2eab7f25b5 100644 --- a/services/web/frontend/js/features/editor-left-menu/components/settings/settings-compiler.tsx +++ b/services/web/frontend/js/features/editor-left-menu/components/settings/settings-compiler.tsx @@ -34,6 +34,7 @@ export default function SettingsCompiler() { ]} label={t('compiler')} name="compiler" + translateOptions="no" /> ) } diff --git a/services/web/frontend/js/features/editor-left-menu/components/settings/settings-document.tsx b/services/web/frontend/js/features/editor-left-menu/components/settings/settings-document.tsx index 839bd499eb..8655a63cfc 100644 --- a/services/web/frontend/js/features/editor-left-menu/components/settings/settings-document.tsx +++ b/services/web/frontend/js/features/editor-left-menu/components/settings/settings-document.tsx @@ -43,6 +43,7 @@ export default function SettingsDocument() { options={validDocsOptions} label={t('main_document')} name="rootDocId" + translateOptions="no" /> ) } diff --git a/services/web/frontend/js/features/editor-left-menu/components/settings/settings-editor-theme.tsx b/services/web/frontend/js/features/editor-left-menu/components/settings/settings-editor-theme.tsx index 5f9ad51869..870ce48ca1 100644 --- a/services/web/frontend/js/features/editor-left-menu/components/settings/settings-editor-theme.tsx +++ b/services/web/frontend/js/features/editor-left-menu/components/settings/settings-editor-theme.tsx @@ -40,6 +40,7 @@ export default function SettingsEditorTheme() { options={options} label={t('editor_theme')} name="editorTheme" + translateOptions="no" /> ) } diff --git a/services/web/frontend/js/features/editor-left-menu/components/settings/settings-font-family.tsx b/services/web/frontend/js/features/editor-left-menu/components/settings/settings-font-family.tsx index 5a327093a4..a0c2ec49dc 100644 --- a/services/web/frontend/js/features/editor-left-menu/components/settings/settings-font-family.tsx +++ b/services/web/frontend/js/features/editor-left-menu/components/settings/settings-font-family.tsx @@ -29,6 +29,7 @@ export default function SettingsFontFamily() { ]} label={t('font_family')} name="fontFamily" + translateOptions="no" /> ) } diff --git a/services/web/frontend/js/features/editor-left-menu/components/settings/settings-menu-select.tsx b/services/web/frontend/js/features/editor-left-menu/components/settings/settings-menu-select.tsx index 6b1f06ec36..c48486ec1c 100644 --- a/services/web/frontend/js/features/editor-left-menu/components/settings/settings-menu-select.tsx +++ b/services/web/frontend/js/features/editor-left-menu/components/settings/settings-menu-select.tsx @@ -28,6 +28,7 @@ type SettingsMenuSelectProps = { onChange: (val: T) => void value?: T disabled?: boolean + translateOptions?: 'yes' | 'no' } export default function SettingsMenuSelect({ @@ -39,6 +40,7 @@ export default function SettingsMenuSelect({ onChange, value, disabled = false, + translateOptions, }: SettingsMenuSelectProps) { const handleChange: ChangeEventHandler = useCallback( event => { @@ -95,6 +97,7 @@ export default function SettingsMenuSelect({ value={value?.toString()} disabled={disabled} ref={selectRef} + translate={translateOptions} > {options.map(option => (
    @@ -41,6 +42,7 @@ function Changes({ pathnames, projectOps }: ChangesProps) {
    {getProjectOpDoc(op)}
    diff --git a/services/web/frontend/js/features/history/components/change-list/tag-tooltip.tsx b/services/web/frontend/js/features/history/components/change-list/tag-tooltip.tsx index 18736c4f37..c766c4e25b 100644 --- a/services/web/frontend/js/features/history/components/change-list/tag-tooltip.tsx +++ b/services/web/frontend/js/features/history/components/change-list/tag-tooltip.tsx @@ -87,6 +87,7 @@ const ChangeTag = forwardRef( className="history-version-badge" data-testid="history-version-badge" {...props} + translate={isPseudoCurrentStateLabel ? 'yes' : 'no'} > {isPseudoCurrentStateLabel ? t('history_label_project_current_state') @@ -147,24 +148,32 @@ function TagTooltip({ label, currentUserId, showTooltip }: LabelBadgesProps) { const isPseudoCurrentStateLabel = isPseudoLabel(label) const currentLabelData = allLabels?.find(({ id }) => id === label.id) - const labelOwnerName = - currentLabelData && !isPseudoLabel(currentLabelData) - ? currentLabelData.user_display_name - : t('anonymous') + const isAnonymous = !currentLabelData || isPseudoLabel(currentLabelData) + const labelOwnerName = isAnonymous + ? t('anonymous') + : currentLabelData.user_display_name + const labelOwnerNameComponent = isAnonymous ? ( + labelOwnerName + ) : ( + {labelOwnerName} + ) return !isPseudoCurrentStateLabel ? (
    - +   {label.comment}
    - {t('history_label_created_by')} {labelOwnerName} + {t('history_label_created_by')} {labelOwnerNameComponent}
  • diff --git a/services/web/frontend/js/features/ide-redesign/components/breadcrumbs.tsx b/services/web/frontend/js/features/ide-redesign/components/breadcrumbs.tsx index 9949b98c7f..1c21df0f82 100644 --- a/services/web/frontend/js/features/ide-redesign/components/breadcrumbs.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/breadcrumbs.tsx @@ -83,7 +83,7 @@ export default function Breadcrumbs() { const numOutlineItems = outlineHierarchy.length return ( -
    +
    {folderHierarchy.map(folder => (
    {folder.name}
    diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/appearance-settings/editor-theme-setting.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/appearance-settings/editor-theme-setting.tsx index eba0fc5b6c..1a1a3f1c1e 100644 --- a/services/web/frontend/js/features/ide-redesign/components/settings/appearance-settings/editor-theme-setting.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/settings/appearance-settings/editor-theme-setting.tsx @@ -41,6 +41,7 @@ export default function EditorThemeSetting() { options={options} onChange={setEditorTheme} value={editorTheme} + translateOptions="no" /> ) } diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/appearance-settings/font-family-setting.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/appearance-settings/font-family-setting.tsx index d0310e2b1b..f1c279cf23 100644 --- a/services/web/frontend/js/features/ide-redesign/components/settings/appearance-settings/font-family-setting.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/settings/appearance-settings/font-family-setting.tsx @@ -27,6 +27,7 @@ export default function FontFamilySetting() { onChange={setFontFamily} value={fontFamily} width="wide" + translateOptions="no" /> ) } diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/compiler-setting.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/compiler-setting.tsx index c007a0608e..1da6a167c6 100644 --- a/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/compiler-setting.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/compiler-setting.tsx @@ -38,6 +38,7 @@ export default function CompilerSetting() { options={OPTIONS} onChange={setCompiler} value={compiler} + translateOptions="no" /> ) } diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/image-name-setting.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/image-name-setting.tsx index 0574657971..0a7166ae68 100644 --- a/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/image-name-setting.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/image-name-setting.tsx @@ -38,6 +38,7 @@ export default function ImageNameSetting() { options={options} onChange={setImageName} value={imageName} + translateOptions="no" /> ) } diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/root-document-setting.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/root-document-setting.tsx index bd493a018a..719ea4da5a 100644 --- a/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/root-document-setting.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/settings/compiler-settings/root-document-setting.tsx @@ -44,6 +44,7 @@ export default function RootDocumentSetting() { options={validDocsOptions} onChange={setRootDocId} value={rootDocId} + translateOptions="no" /> ) } diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/dropdown-setting.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/dropdown-setting.tsx index 47c5c54ef0..3d9db79c3d 100644 --- a/services/web/frontend/js/features/ide-redesign/components/settings/dropdown-setting.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/settings/dropdown-setting.tsx @@ -31,6 +31,7 @@ type SettingsMenuSelectProps = { disabled?: boolean width?: 'default' | 'wide' loading?: boolean + translateOptions?: 'yes' | 'no' } export default function DropdownSetting({ @@ -44,6 +45,7 @@ export default function DropdownSetting({ disabled = false, width = 'default', loading = false, + translateOptions, }: SettingsMenuSelectProps) { const handleChange: ChangeEventHandler = useCallback( event => { @@ -78,6 +80,7 @@ export default function DropdownSetting({ onChange={handleChange} value={value?.toString()} disabled={disabled} + translate={translateOptions} > {options.map(option => (