706 Commits

Author SHA1 Message Date
claude 4f5dad383b fix(typst-preview): use persistent session to avoid Rc ownership panics
Build and Deploy Verso / deploy (push) Successful in 11m4s
Every call to renderToSvg({ artifactContent }) internally routes through
runWithSession, which calls session.free() after fn resolves. Because the JS
GC may still hold a reference to the first RenderSession wrapper, the next
render's Rc::try_unwrap() panics with 'attempted to take ownership of Rust
value while it was borrowed'.

Fix: use renderer.createModule(data) once to create a persistent RenderSession
that is never freed during the component's lifetime. Subsequent renders call
session.manipulateData({ action: 'reset', data }) (synchronous, no ownership
transfer) + session.renderToSvg({ container }) which routes through
withinOptionSession's renderSession fast-path — bypassing runWithSession and
its session.free() entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 15:49:50 +00:00
claude eedf4b50f6 fix(typst-preview): use RenderByContentOptions to avoid Rust aliasing
Build and Deploy Verso / deploy (push) Successful in 10m35s
Replace runWithSession + manipulateData + session.renderToSvg with the
direct RenderByContentOptions form: renderer.renderToSvg({ format: 'vector',
artifactContent, container }).

The session-based API kept hitting 'recursive use of an object detected
which would lead to unsafe aliasing in rust' because runWithSession holds
a mutable borrow of the session while renderToSvg also takes one —
regardless of whether you call renderer.renderToSvg({ renderSession }) or
session.renderToSvg(). The content-based form creates and disposes the
session internally without any caller-visible borrow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 15:22:56 +00:00
claude c6d71e58b7 fix(typst-preview): fix recursive Rust aliasing error and stale renders
Build and Deploy Verso / deploy (push) Successful in 12m44s
Two bugs fixed:

1. 'recursive use of an object' Rust error: inside runWithSession(), calling
   renderer.renderToSvg({ renderSession: session }) passes the session to the
   renderer while runWithSession already holds it — double-aliasing the same
   Rust object. Fixed by using session.renderToSvg({ container }) directly.

2. Stale preview after edits: concurrent doRender calls (compile finishes
   while previous render is still in progress) would both enter runWithSession
   simultaneously, causing the Rust error and leaving the view frozen. Fixed
   with a render guard (isRenderingRef) that queues the latest vectorData and
   flushes it once the current render completes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 14:52:49 +00:00
claude 06085cda21 fix(csp): allow WebAssembly instantiation via wasm-unsafe-eval
Build and Deploy Verso / deploy (push) Successful in 11m54s
WebAssembly.instantiateStreaming() requires 'wasm-unsafe-eval' in the
script-src CSP directive. Unlike 'unsafe-eval', this only permits WASM
compilation and does not allow arbitrary eval() calls.

Needed for the typst.ts WASM preview (both compiler and renderer).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 14:35:26 +00:00
claude 8515a899ac fix(typst-preview): fix race condition and error-masking in WASM preview
Build and Deploy Verso / deploy (push) Successful in 12m35s
Two bugs were causing a brief red error then blank screen:

1. triggerCompile closed over `view` in useCallback deps, so every time
   the CodeMirror view reference changed, useEffect terminated and
   recreated the entire worker. Fixed by reading view via a ref, making
   triggerCompile stable (empty dep array).

2. When 'compiled' arrived before the renderer WASM finished loading,
   the old code called setStatus('ready') to silently skip rendering —
   this cleared any existing error and left a blank screen. Fixed by
   buffering the vectorData in pendingVectorRef and flushing it once
   the renderer is ready.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 14:05:41 +00:00
claude 200bff4ecb feat(typst): browser-side live preview via typst.ts WASM
Build and Deploy Verso / deploy (push) Successful in 12m51s
Adds a dual-mode Typst preview: a new "Live (browser)" mode compiles and
renders Typst documents entirely in-browser using typst.ts WASM (28 MB
compiler + 1 MB renderer). The existing server-side PDF mode is preserved
and selectable via a new "Preview mode" section in the recompile dropdown,
visible only for Typst projects.

Architecture:
- Web Worker (typst-preview-worker.ts) runs the WASM compiler; queues
  compile requests so only the latest compile runs after each keypress
- TypstWasmPreview component initialises the renderer on the main thread,
  listens to changedAt from the compile context, debounces at 400 ms, and
  renders SVG into a container div via renderToSvg
- typstPreviewMode ('wasm'|'pdf') is persisted per-project in localStorage
- isTypstProject, changedAt, typstPreviewMode, setTypstPreviewMode are
  exposed through both LocalCompileContext and DetachCompileContext
- Fonts loaded from jsDelivr CDN (text subset only) on first use
- Phase 1: single-file Typst only (no #include, no images)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 13:42:39 +00:00
claude b5cf5f9e7b docs: add AI writing assistant to alpha-4 TODO
Build and Deploy Verso / deploy (push) Successful in 59s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 12:29:15 +00:00
claude 60d48ae532 docs: release alpha 3 — remove in-progress marker, fix export description
Build and Deploy Verso / deploy (push) Has been cancelled
Build and Deploy Verso (prod) / deploy (push) Successful in 1m19s
Mark Alpha 3 as released (drop the "in progress" qualifier) and correct
the bidirectional format export entry: only LaTeX ↔ Typst conversion is
available; DOCX, Markdown and HTML exports are not yet implemented.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 11:32:41 +00:00
claude a796577199 fix(security): restrict publish-presentation routes to project owners
Build and Deploy Verso / deploy (push) Successful in 10m54s
Read-only collaborators and token-link users could publish, unpublish,
and rotate presentation share tokens. Change all three write endpoints
from ensureUserCanReadProject to ensureUserCanAdminProject so only the
project owner can perform these actions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 10:17:24 +00:00
claude 1814cba458 fix(mobile): extend CSS breakpoints to cover Tor Browser spoofed viewport
Build and Deploy Verso / deploy (push) Successful in 10m11s
Tor Browser sets viewport width to ~980px to resist fingerprinting, so
@media (max-width: 767px) never fires on a real phone using it. Add the
secondary condition (pointer: coarse) and (max-width: 1024px) — same
dual-check already used in JS — to all mobile CSS overrides in
ide-lumiere.scss and project-list-lumiere.scss. This activates the font
scaling, toolbar height increase, auto-zoom prevention, and project page
layout fixes on Tor Browser.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 08:24:29 +00:00
claude 4c5db24963 fix(mobile): prevent editor auto-zoom on iOS/Android
Build and Deploy Verso / deploy (push) Successful in 10m28s
Browsers zoom any focused editable element (including contenteditable)
whose font-size is below 16px. Apply font-size: max(1rem, 1em) on
.cm-content inside the mobile media query so the 16px floor is enforced
while still respecting user-set sizes larger than that.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 07:01:20 +00:00
claude 2d44c920fd fix(mobile): scale up editor UI chrome fonts and toolbar on mobile
Build and Deploy Verso / deploy (push) Successful in 12m33s
Adds a @media (max-width: 767px) block scoped to .ide-redesign-main
that bumps the CSS font-size tokens one step each and increases the
toolbar height, making buttons, labels, and panel headers readable
on a phone without touching the CodeMirror editor font size (which
is controlled by user settings independently).

Also reverts the unintended rail auto-collapse from the previous
commit — collapsing the sidebar was not the requested change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 22:23:23 +00:00
claude 7bc60a4e10 feat(mobile): collapse rail by default; tighten project page header layout
Build and Deploy Verso / deploy (push) Has been cancelled
Editor:
- Auto-collapse the file-tree rail on mobile so the editor+PDF panels
  occupy the full screen width instead of sharing it with a 15% sidebar.
  The user can still open the rail from the toolbar; the collapse only
  fires on first load or when switching to a mobile viewport.

Project page (Lumiere):
- Move the XS/M/L zoom buttons from the mobile filter-pill toolbar into
  a row with the New Project button, so neither is stranded alone.
- The search bar gets its own full-width row above the actions row.
- Desktop layout is unchanged (search + new-project stay side-by-side;
  zoom stays in the title row). `display:contents` makes the wrapper
  div transparent to the desktop flex container.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 22:18:28 +00:00
claude 51d0314c1c fix(mobile): detect touch devices with spoofed viewport width
Build and Deploy Verso / deploy (push) Successful in 10m17s
Tor Browser and Firefox with privacy.resistFingerprinting report a
desktop-sized viewport (~980px) even on a real Android phone. This made
window.matchMedia('(max-width: 767px)') return false, so isMobile was
always false on Tor, leaving the editor in side-by-side (horizontal)
layout instead of the expected vertical stack.

Fix: add a secondary check using `(pointer: coarse) and (max-width:
1024px)`. Touch hardware is not spoofed by fingerprinting resistance,
so this reliably catches phones and tablets regardless of the reported
viewport width. Applied to both getInitialLayout() and the live
isMobile state in main-layout.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 14:31:56 +00:00
claude 8fc71d677c Fix thumbnail quality and mobile vertical split layout
Build and Deploy Verso / deploy (push) Successful in 10m23s
Thumbnails: update the actual thumbnail endpoint (ConversionController.js
thumbnailFromBuild) to quality=90 and width=794. The previous fix targeted
ConversionManager.js which handles preview mode, not the thumbnail route
called by ThumbnailManager.mjs.

Mobile layout: move the isMobile guard before the stored-preference check
in getInitialLayout(). The autoSave race fix (build 274) stopped future
bad writes, but a stale 'flat' in localStorage was still being read on
every load, blocking the mobile check. Mobile now always starts in
verticalSplit regardless of any stored value.

CI: add node --check on all server-side .mjs files in the Dockerfile,
after source copy and before webpack compile, so syntax errors like the
escaped-backtick incident fail the build immediately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 13:48:44 +00:00
claude 8a821bc91d Fix escaped backtick syntax error in EmailBuilder.mjs
Build and Deploy Verso / deploy (push) Successful in 12m49s
sed substitution literal-escaped the backtick and \${ in the
groupSSOReauthenticate subject line, producing invalid JS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 13:13:05 +00:00
claude 33fa5986c8 Email rebranding, mobile filter alignment fix, minor UI cleanup
Build and Deploy Verso / deploy (push) Has been cancelled
Email:
- Replace Overleaf green palette with Verso teal (#2a9d8f) for CTA
  buttons, links, and logo text
- Rename force-overleaf-style CSS class to force-verso-style in all
  email body templates and layout
- Replace all user-visible "Overleaf" strings with settings.appName
  across email subjects, bodies, and sign-offs (EmailBuilder.mjs)
- Remove Overleaf CEO signature from onboarding email; substitute
  "The Verso Team"
- Fix utm_source=overleaf → utm_source=verso in onboarding links
- Point git token docs URL to local siteUrl instead of docs.overleaf.com
- Fix hard-coded Overleaf green (#0F7A06) in SSO disabled email link

Mobile filter chips:
- Switch from overflow-x:auto (still leaks through ancestor flex chain)
  to flex-wrap:wrap with flex:1 1 auto on each chip so all chips in a
  row grow to equal width — no overflow, clean alignment
- Toolbar becomes flex-column: chips on top, zoom below-right

Misc:
- Remove import_typst_file option from new-project menu
- Add interface_language to extracted-translations.json whitelist

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 12:56:34 +00:00
claude f629f6a50c Mobile polish: max thumbnail quality, tab-bar filter chips, fix vertical split default
Build and Deploy Verso / deploy (push) Successful in 10m12s
- CLSI thumbnails: bump to 794px/q90 (matches preview quality) for
  crisp display on 2x/3x phone screens
- Lumière filter chips → underline tab bar: single scrollable row with
  teal active indicator, no more wrapping alignment issues; zoom buttons
  separated by a vertical divider on the right
- Fix editor vertical split default on mobile: disable react-resizable-panels
  autoSaveId on mobile to prevent a stale collapsed PDF pane from firing
  onCollapse → changeLayout('flat') and overriding the verticalSplit
  default set by getInitialLayout()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 12:19:10 +00:00
claude c79ac23a15 Improve thumbnail quality and fix mobile editor default layout
Build and Deploy Verso / deploy (push) Successful in 10m18s
Thumbnails: increase CLSI thumbnail from 190px/q50 to 400px/q80.
At 190px/50% JPEG quality, images are noticeably blurry on 2x phone
screens (source needs 380px device pixels but source is only 190px).

Editor mobile layout: getInitialLayout() was returning sideBySide for
any stored 'split' preference (set from a desktop session), even on
mobile. sideBySide on mobile renders vertically via the isMobile check
in main-layout, but the stated default was still wrong. Now on mobile,
any stored value other than 'flat' maps to verticalSplit so the
top-bottom split is always the default; flat is preserved so a user
who explicitly chose editor-only keeps that preference.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 11:44:34 +00:00
claude 5aab716245 lumiere mobile: wrap filter pills instead of horizontal scroll
Build and Deploy Verso / deploy (push) Successful in 12m8s
overflow-x: auto only clips content when every ancestor has a definite
width; that constraint wasn't met so the pills were still expanding the
page. Switch to flex-wrap: wrap so pills flow onto multiple rows and
never cause horizontal overflow. Also allow the toolbar itself to wrap
so the zoom control can drop to a new row if needed.

Remove redundant sidebar hide rule — SidebarDsNav already has d-none
d-md-flex so it was never contributing to the overflow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 11:07:16 +00:00
claude 90df7f5cc2 lumiere mobile: hide sidebar to fix 2x page width
Build and Deploy Verso / deploy (push) Successful in 11m15s
The project-list-wrapper flex row includes the sidebar (min-width:200px)
alongside the content area. On a 375px phone this totals ~575px, causing
the 2x horizontal overflow. Hide the sidebar on mobile — the filter pills
in the mobile toolbar already cover all its navigation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 10:50:37 +00:00
claude d12a39329b lumiere mobile: fix overflow, style ⋮ button, show tile actions on touch
Build and Deploy Verso / deploy (push) Successful in 16m58s
1. Page width overflow: stack lumiere-header as column on mobile so
   lumiere-header-actions (flex-shrink:0) no longer forces the container
   wider than the viewport. Search form and new-project button each take
   full width on their own line.

2. ⋮ button in compact row: override dropdown-table-button-toggle inside
   lumiere-compact-actions with Lumière-palette colours and tighter
   padding, replacing the table-context styling.

3. Tile view action strip: add @media (hover: none) rule so
   lumiere-card-actions is always opacity:1 on touch devices (phones have
   no cursor, so the hover-reveal pattern is unusable).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 10:00:50 +00:00
claude 6f419477df lumiere: fix mobile overflow + restore tile view with XS/M/L zoom
Build and Deploy Verso / deploy (push) Successful in 1m42s
Overflow fix: ActionsCell (up to 9 icon buttons) was assigned to the
`auto` column in the compact row, blowing out the row to ~300px.
Replace with ActionsDropdown (single ⋮ button) on mobile via
d-md-none / d-none d-md-flex, same pattern as the classic table.

Tile view: remove the isMobile force to compact (effectiveScale=0).
Add a mobile-only toolbar row (d-flex d-md-none) combining the filter
pills and a new XS/M/L zoom control:
  - XS (scale=0)  → compact rows
  - M  (scale=1)  → 2 tiles per row  (CSS: repeat(2, 1fr))
  - L  (scale≥1.35) → 1 tile per row (CSS: 1fr, class --mobile-1col)

The --lum-card-scale inline var is suppressed on mobile so CSS media
query controls thumbnail height (0.85 for 2-col, 1.3 for 1-col).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 08:54:30 +00:00
claude a004f21688 Fix mobile ergonomics on Lumière project list page
Build and Deploy Verso / deploy (push) Successful in 14m58s
Compact row (xs): wrap format/owner/date in a lumiere-compact-meta div
that uses display:contents on desktop (preserving the 6-column grid) and
switches to a 2-row grid layout on mobile — row 1: checkbox/name/actions,
row 2: format·owner·date. Actions are always visible on touch devices.

Filter access: add a horizontal scrollable filter-pill bar (d-md-none)
to the Lumière header so users can switch between All/Yours/Shared/Archived/
Trashed on phone without needing the desktop sidebar.

Also: hide zoom control on mobile (d-md-none), auto-force compact view on
mobile via useIsMobile hook, fix search form min-width overflow on xs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 08:22:27 +00:00
claude 065534819c Fix file tree refresh after convert and compiler sync on set-as-main
Build and Deploy Verso / deploy (push) Successful in 14m4s
- Convert: backend now returns parentFolderId+isNew; frontend calls
  dispatchCreateDoc directly so the new file appears without a page refresh
- Set as main: infer compiler from file extension and POST both rootDocId
  and compiler together; updateProject propagates the change to the
  editor settings dropdown and project list immediately

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 07:50:02 +00:00
claude b0b389dc4c feat: set-as-main for .typ/.qmd; fix compiler filter; fix ZIP import compiler
Build and Deploy Verso / deploy (push) Successful in 14m56s
Set as main document (context menu):
- canSetRootDocId used selectedEntityIds so .typ/.qmd files couldn't be
  set as main via right-click on an unselected file.
  Now computed locally from contextMenuEntityId (same pattern as convert)
  using isValidTeXFile which already covers .typ, .qmd, .tex etc.

Compiler filter (editor settings):
- docs?.find(id) could return undefined due to ID format mismatch,
  causing all engines to show as available for non-LaTeX projects.
  Added findInTree fallback so the root doc name is always resolved.

ZIP import compiler:
- Projects created from ZIP always got defaultLatexCompiler ('quarto')
  regardless of content.
- findRootDocFileFromDirectory now also searches for .typ and .qmd root
  files after finding no .tex file.
- ProjectUploadManager now infers the compiler from the root doc
  extension and sets it on the project after import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 21:56:39 +00:00
claude bc131f6440 fix: convert items show for unselected files; add success toast
Build and Deploy Verso / deploy (push) Successful in 15m23s
canRename required selectedEntityIds.size === 1, so right-clicking an
unselected file hid the convert items even though contextMenuEntityId
was correctly set. Replace canRename with !fileTreeReadOnly for the
convert-specific gate, which is the actual write-access check needed.

Also add showExportDocumentSuccess so the user sees the warning toast
on successful conversion instead of silent nothing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 21:18:24 +00:00
claude ff7de70a61 fix: per-file convert — DuplicateNameError + first-click no-op
Build and Deploy Verso / deploy (push) Successful in 21m44s
Two bugs:
1. Converting when output already exists threw DuplicateNameError (400).
   Now overwrites existing doc via setDocument instead of failing.

2. Right-clicking an unselected file left contextMenuEntityId null,
   so the first click on Convert silently did nothing. Added
   contextMenuEntityId to FileTreeMainContext, set it on right-click
   and on the … button click; FileTreeItemMenuItems now uses it for
   the convert hooks rather than relying on selectedEntityIds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 20:35:50 +00:00
claude dbb7899ca3 docs: clean up Alpha 3 section, add Known Issues
Build and Deploy Verso / deploy (push) Successful in 15m23s
Remove RevealJS thumbnails (redundant with Features section) and Upload
reliability (still unresolved). Add Known Issues section documenting
the large file upload timeout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 19:58:52 +00:00
claude 7eeabc93aa fix: send empty JSON body on convert to avoid body-parser 400
postJSON without options sends Content-Type: application/json with no
body; body-parser's BodyParserWrapper intercepts the resulting
SyntaxError and returns 400 before the route handler runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 19:58:48 +00:00
claude ac2315bc8e feat: per-file Convert in explorer menu + fix export success toast
Build and Deploy Verso / deploy (push) Successful in 9m50s
- Add "Convert to Typst (.typ)" in the file tree context menu for .tex
  docs, and "Convert to LaTeX (.tex)" for .typ docs. Clicking runs pandoc
  on the file content and creates the converted file in the same folder.
- New backend endpoint POST /project/:id/doc/:id/convert/:type that reads
  the doc from document-updater, runs pandoc directly, and creates the
  result via ProjectEntityUpdateHandler (file tree updates via socket).
- Rewrite the export success toast for typst and latex conversions: no
  more link to /contact, replaced with a plain warning that errors are
  expected (pandoc does not support all constructs).
- Add i18n keys: convert_to_typst, convert_to_latex,
  typst_export_feedback_message, latex_export_feedback_message (EN + FR)
  and all four to extracted-translations.json.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 15:18:31 +00:00
claude de22dcf87f docs: add mobile layout to Alpha 3 release notes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 14:58:28 +00:00
claude e82f5fbead docs: update Alpha 3 release notes and fix Overleaf description
Add top/bottom split view and bidirectional format export (LaTeX↔Typst,
LaTeX→DOCX/Markdown/HTML) to the Alpha 3 feature list. Fix the security
section which incorrectly referred to Overleaf as a LaTeX/Typst editor —
Overleaf only supports LaTeX.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 14:56:45 +00:00
claude d64365aa56 Add missing i18n keys to extracted-translations.json
Build and Deploy Verso / deploy (push) Successful in 14m28s
top_bottom_split_view, export_as_typst, and export_as_latex were absent
from the whitelist, so translations-loader.js stripped them from the
locale bundles at build time — causing raw keys to show in the UI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 14:46:31 +00:00
claude 2d2a85f06f Fix typst export 500, add export-as-latex for typst projects
Build and Deploy Verso / deploy (push) Successful in 10m4s
- clsi-nginx: allow hyphens in project-id regex — conversion IDs are UUIDs
  which nginx was rejecting, causing 500 on file download after conversion
- CLSI ConversionController/Manager: add 'latex' export type (typst→latex via pandoc)
- Web: add 'latex' to SUPPORTED_CONVERSION_TYPES
- Frontend: add Export as LaTeX button (visible only for typst projects)
- Fix visibility logic: export-as-latex shows for typst, export-as-typst shows for latex
- Add export_as_latex translation key (en + fr)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 11:55:41 +00:00
claude b545d08939 Fix four reported bugs: typst export 500, export shown for all project types, Lumiere tile button layout, i18n
Build and Deploy Verso / deploy (push) Successful in 15m7s
- ConversionController.js: add typst to CONVERSION_CONFIGS (missing entry caused 400→500 chain)
- export-project-with-conversion-button: hide button for non-LaTeX projects (typst/quarto) via compiler check
- project-list-lumiere.tsx + scss: revert lumiere-card-actions back inside .lumiere-card (put them back like they were)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 11:12:50 +00:00
claude 9d11683920 feat: Typst → LaTeX import, and fix export button visibility
Build and Deploy Verso / deploy (push) Successful in 1m14s
Typst → LaTeX import:
- CLSI ConversionManager: add 'typst' to CONVERSION_CONFIGS
  (pandoc input.typ --from typst --to latex --standalone → zip archive)
- Web controller: allow 'typst' as a valid importDocument conversion type
- Frontend modal: add .typ file config to ImportDocumentModal
- New project button modal: add 'import_typst' variant + switch case
- New project button: show "Import Typst file" when enablePandocConversions
  is true (no split test gate — Verso has no SaaS split test infra)
- Locales: add choose_typst_file and import_typst_file keys (18 locales)

Export button fix:
- Remove featureFlag="export-typst" from ExportProjectWithConversionButton
  so the button shows whenever enablePandocConversions is true, without
  needing an unconfigured split test to return 'enabled'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 08:54:54 +00:00
claude 15ffaefb87 fix: install pandoc and enable pandoc conversions
Build and Deploy Verso / deploy (push) Failing after 1h2m54s
Pandoc was not installed in the base image, so the export buttons
(docx, markdown, html, typst) were hidden because ENABLE_PANDOC_CONVERSIONS
defaulted to false.

- Dockerfile-base: add pandoc via apt (Ubuntu Noble ships 3.1.3, which
  supports --to typst added in pandoc 3.0)
- env.sh: set ENABLE_PANDOC_CONVERSIONS=true so both the web and CLSI
  services expose and serve the export endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 08:46:09 +00:00
claude 32aec14c41 feat: export LaTeX project as Typst (.typ) via pandoc
Build and Deploy Verso / deploy (push) Successful in 9m58s
Adds a new "Export as Typst" option in the project title dropdown and
File menu, mirroring the existing docx/markdown/html export pipeline.

Changes:
- CLSI ConversionManager: add 'typst' to LATEX_EXPORT_CONFIGS
  (compressOutput: false, pandoc --from latex --to typst)
- Web controller: register 'typst' → 'typ' in SUPPORTED_CONVERSION_TYPES
- Frontend: extend conversionType union and add ExportProjectWithConversionButton
- File menu: add 'export-as-typst' to the download group command structure
- Locales: add export_as_typst key to all 18 locale files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 08:24:54 +00:00
claude f7d0369213 fix: translate hardcoded 'Editor settings' header in View menu
Build and Deploy Verso / deploy (push) Successful in 9m45s
The DropdownHeader in the View > Layout menu was using a raw English
string instead of t('editor_settings'). Added the key to all 18 locale
files with appropriate translations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 08:04:56 +00:00
claude 57e34aa104 fix: dropdown positioned at top-left on first click
Build and Deploy Verso / deploy (push) Successful in 15m17s
Popper lazy-initializes on first open, causing it to place the menu at
[0,0] before it has computed the toggle's position. renderOnMount forces
Popper to initialize while the component is first mounted, so the
position is ready before the user's first click.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 22:05:01 +00:00
claude 48d5e15f7f fix: Lumiere card dropdown clipped by card's backdrop-filter and transform
Build and Deploy Verso / deploy (push) Successful in 14m35s
.lumiere-card has both backdrop-filter and a hover transform, both of
which create a new containing block for position:fixed descendants,
trapping Popper dropdowns inside the card's overflow:hidden bounds.

Fix: move .lumiere-card-actions outside .lumiere-card (sibling inside
.lumiere-card-wrapper, which has position:relative but no filter or
transform). The actions strip is now absolutely positioned at the card
bottom with its own solid background and bottom border-radius.
No backdrop-filter on the strip to avoid re-introducing the same trap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 21:35:52 +00:00
claude b78845a926 fix: show presentation export dropdown on Lumiere tiles for Quarto projects
Build and Deploy Verso / deploy (push) Successful in 14m25s
ProjectCard in the Lumiere tile view always showed CompileAndDownloadProjectPDFButtonTooltip,
ignoring the project compiler. Quarto presentation projects need the
DownloadPresentationButtonTooltip (HTML/PDF dropdown) instead, matching
the logic already in actions-cell.tsx and actions-dropdown.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 21:08:43 +00:00
claude c10a13f605 fix: PC layout regression and silent PDF download failure
Build and Deploy Verso / deploy (push) Successful in 13m21s
layout-context: getInitialLayout() was returning verticalSplit for
any stored 'vertical' preference, including on desktop. Now checks
isMobile first so stored mobile preference doesn't bleed into PC.

compile-and-download-pdf: when compile succeeds but output.pdf is
absent from outputFiles, the code crashed silently at outputFile.build
leaving the user with no feedback. Now shows the error modal instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 20:42:22 +00:00
claude 762d3e75cf fix: footer translation, mobile search bar and table row layout
Build and Deploy Verso / deploy (push) Successful in 13m32s
- Footer: use translate('built_on') key instead of hardcoded 'Built on';
  update fr.json 'built_on' → 'Basé sur Overleaf'
- Mobile project list: move search bar + button outside the bordered
  TableContainer so its width is viewport-constrained, not affected by
  the table's fixed layout
- Mobile table rows: use width:100% (not auto) on cells so they fill the
  full tr width regardless of the higher-specificity column percentage
  rules; add explicit width:100% on tr to anchor flex-column sizing;
  keep width:auto on absolutely-positioned actions cell

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 15:31:09 +00:00
claude db5b2b1f82 fix: show presentation download dropdown for all Quarto projects
quartoFlavor is set only after a project is compiled with the current
build. Existing Quarto projects that haven't been recompiled have
quartoFlavor=undefined, so the old check (quartoFlavor === 'revealjs')
fell through to the regular PDF compile button with no dropdown.

Drop the quartoFlavor guard — compiler === 'quarto' is sufficient since
all Quarto projects in Verso are RevealJS presentations. Changes applied
to ActionsCell (desktop icon), DownloadPresentationButtonTooltip guard,
and ActionsDropdown (mobile three-dot menu).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 14:53:33 +00:00
claude 685a7ffca1 fix: language picker links, dropdown position, and editor layout defaults
Build and Deploy Verso / deploy (push) Successful in 14m34s
Language picker:
- Add fallback href in Pug so language links navigate even if JS fails
- Anchor dropdown to right edge (right:0) so it stays on-screen when
  the picker is near the right side of the footer on mobile

Editor layout:
- Read stored pdfLayout from localStorage on init so the last-used
  layout is remembered across sessions
- Default to verticalSplit (top/bottom) on mobile when no preference
  is stored, so the editor opens in a sensible layout on phones

Translations:
- Add top_bottom_split_view key to all 16 locales that were missing it

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 14:40:02 +00:00
claude 84d1efc271 fix: use <details>/<summary> for Pug language picker to get native toggle
Build and Deploy Verso / deploy (push) Successful in 14m10s
The manual JS click-handler approach (tried with stopPropagation,
containment check, and mousedown variants) never worked reliably on
login/password/settings pages. The browser's native <details>/<summary>
toggle behaviour requires no JavaScript and is immune to Bootstrap JS or
React event delegation interference.

The inline script now only builds the return_to hrefs and handles
outside-click-to-close (setting details.open=false). The CSS gains a
.language-picker-details rule that sets position:relative so the
absolutely-positioned dropdown-menu is positioned correctly, and
details[open] .dropdown-menu { display: block } to show the menu.

The #language-picker-toggle id remains on the <summary> so the existing
CSS (cursor, text-decoration, material-symbols alignment) continues to
apply. The React LanguagePicker is unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 12:55:39 +00:00
claude b461343b23 fix: project list phone layout and language picker
Build and Deploy Verso / deploy (push) Successful in 14m3s
- Search bar overflow: min-width:0 on form prevents input min-content
  from overflowing flex container on narrow screens
- Remove duplicate New Project button from header row (tableTopArea
  already provides it for mobile)
- Simplify tableTopArea: single button+search layout regardless of
  isLibraryEnabled flag
- 2-line project rows on mobile: merge dash-cell-date-owner +
  dash-cell-tag into a single dash-cell-meta so date/owner/tags flow
  inline on line 2 below the project name
- Footer mobile: hide language-picker-text on mobile (icon only) to
  prevent 3rd line in 2-row footer
- Language picker: use mousedown instead of click for outside-close
  handler — click bubbling is unreliable on iOS for non-interactive
  elements; mousedown fires for all taps

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 11:47:32 +00:00
claude be353d53bd Center footer items on mobile for harmonious two-line layout
Build and Deploy Verso / deploy (push) Successful in 9m44s
Both ul.site-footer-items rows are now justify-content: center on small
screens, so the copyright/language row and the licence/source row are
symmetrically aligned instead of left-justified.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 11:05:22 +00:00
claude 0a5bd4e47d Fix mobile layout key, language picker close handler, and presentation download
Build and Deploy Verso / deploy (push) Has been cancelled
- Mobile vertical layout: add key= based on direction so react-resizable-panels
  remounts cleanly when switching between horizontal and vertical; also use
  isVertical (not just pdfLayout) for the autoSaveId to avoid restoring a
  mismatched layout from localStorage
- Language picker: replace stopPropagation pattern with a containment check on
  the document click handler — more robust on React pages where Bootstrap JS or
  React's event delegation can interfere with stopPropagation
- Presentation download dropdown: use popperConfig strategy:'fixed' so the menu
  escapes overflow:hidden table cells; remove forced drop='up' and let Popper
  choose; defer URL.revokeObjectURL by 10 s to give the browser time to start
  the download before the blob URL is released

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 11:01:15 +00:00
claude 11227d59e3 Editor mobile ergonomics: vertical split layout on phones and as desktop option
Build and Deploy Verso / deploy (push) Successful in 14m3s
On mobile (< 768 px) the existing side-by-side layout automatically switches
to a vertical stack (editor on top, PDF/presentation on bottom) without
changing the stored layout preference.

A new "Top / bottom split" option is added to the layout menu so desktop
users can choose the same vertical split explicitly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 09:25:15 +00:00
claude 0f585ea5bb Fix: use picture_as_pdf icon for presentation download toggle
Using the generic 'download' icon duplicated the ZIP button icon,
giving two identical icons side by side. Switch to picture_as_pdf to
match the previous compile-and-download button's appearance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 08:51:29 +00:00
claude a6ea291ea8 Add PDF/HTML download dropdown for Quarto Slides in project list
Quarto Slides (RevealJS) projects compile to output.html, not output.pdf,
so the existing "Download PDF" button was meaningless for them. Replace it
with a two-option dropdown matching the editor's PdfHybridDownloadButton:

- Desktop (ActionsCell): icon button opens a dropup with
  "Download standalone HTML" and "Download PDF slides"
- Mobile (ActionsDropdown): two separate dropdown items with the same
  choices and per-format spinner while the export is in progress

Both use the same /project/:id/presentation-export/:format endpoint and
show a loading modal (with error reporting) during the server-side render,
exactly as the editor toolbar does.

Non-RevealJS projects continue to show the compile-and-download-PDF button
unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 08:48:03 +00:00
claude 703f4d6ee2 Replace native <select> language picker with styled dropdown
Build and Deploy Verso / deploy (push) Successful in 13m43s
The native <select> looked like a form control ("cheap"). Replace it with
the same HTML+CSS pattern as the React LanguagePicker: btn-inline-link
toggle with a translate icon, Bootstrap dropdown-menu for the list, active
item highlighted in green.

A tiny self-contained inline script handles the toggle since Bootstrap JS
is not loaded on React-layout pages. It builds the return_to URL from
window.location.pathname at click time (same as the old select approach).

Added top: auto / bottom: 100% to .language-picker .dropdown-menu so the
list always opens upward regardless of whether Popper.js is present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 08:29:10 +00:00
claude 86a902c197 Improve mobile ergonomics for footer and project list
Footer (both React and Pug):
- Below md: switch to flex-wrap so items wrap naturally instead of
  overflowing; reset line-height from the fixed 49px to normal; add
  row-gap so wrapped rows aren't crammed together
- Add footer-sep class to pipe separator <li>s so they can be hidden
  on small screens (wrapping mid-separator looks wrong)
- Change col-lg-3 text-end → text-lg-end so the right column (licence,
  source code) aligns left when it stacks full-width below lg

Project list:
- Show NewProjectButton on mobile in the header row (the sidebar that
  holds the button is already hidden below md via d-none d-md-flex,
  leaving users with no way to create projects on their phone)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 08:22:57 +00:00
claude 36f5e9fb06 Remove rounded top-left corner from project list content area
The content panel had border-top-left-radius: var(--border-radius-large)
at md+ breakpoint, left over from the old Overleaf theme. Everything else
is square now so this corner stood out. Removing it makes the edge flush.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 07:58:33 +00:00
claude 2b9ebe5522 Replace Overleaf O with Verso icon mark in editor state favicons
Build and Deploy Verso / deploy (push) Successful in 10m0s
favicon-compiled.svg, favicon-compiling.svg, favicon-error.svg all used
the Overleaf O logo as the base. Replace with the Verso icon mark (dark
background, four colored circles, white V polygon, clipped to a circle)
while keeping the existing status badge icons (✓ / ↻ / ✗) unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 07:54:33 +00:00
claude e4f1eead25 Branding polish: Verso favicons, OG image, and meta tag fixes
Build and Deploy Verso / deploy (push) Successful in 10m24s
Replace Overleaf favicons with Verso icon mark across all sizes (16x16,
32x32, 180x180 apple-touch, 192x192/512x512 android-chrome, ICO). Add
OG social preview image (1200x630) for Discord/Twitter link cards.

- New favicon.svg: Verso icon mark with four overlapping circles and V
- mask-favicon.svg: monochrome V polygon (was Overleaf chevron)
- og-image.png: 1200x630 social card with icon mark and "Verso" wordmark
- web.sitemanifest.json: rename "Overleaf" → "Verso", theme_color updated
- _metadata.pug: add og:url tag, fix og:type missing content= attribute,
  point CE default OG/twitter images to og-image.png instead of apple-touch-icon.png

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:19:01 +00:00
claude 323d74cc66 fix: replace Pug language picker dropdown with native select
Build and Deploy Verso / deploy (push) Successful in 14m31s
The old dropdown relied on data-bs-toggle and AngularJS directives,
neither of which are loaded on React-layout pages (layout-react.pug
intentionally excludes Bootstrap JS). The toggle button was inert on
pages like /user/settings.

Replace with a plain <select> that navigates via window.location.href
onchange — works without any framework on all page types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 18:46:03 +00:00
claude 81c1fc92d1 fix: remove data-bs-toggle from LanguagePicker dropdown toggle
Build and Deploy Verso / deploy (push) Successful in 10m8s
Bootstrap vanilla JS and react-bootstrap were both handling the dropdown,
causing the toggle to be unresponsive. react-bootstrap manages its own
state and does not need this attribute.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 14:15:36 +00:00
claude 319ccc32ee feat: add language picker to logged-in footer and editor settings
Build and Deploy Verso / deploy (push) Successful in 18m46s
- Rewrites LanguagePicker to use availableLanguages from ol-footer meta
  instead of subdomainLang (which is always empty in single-domain setup)
- Passes availableLanguages through layout-react.pug → ol-footer meta so
  React footer picks it up
- Adds InterfaceLanguageSetting component to the editor settings modal
  ("Spelling and language" tab) for use when no footer is present
- Adds interface_language key to all five locale files (en/fr/de/es/it)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 12:22:55 +00:00
claude 1a0197812d feat: cookie+DB language preference, eliminating subdomain requirement
Build and Deploy Verso / deploy (push) Successful in 14m53s
Users can now select their UI language directly without relying on
subdomain routing (fr.verso.alocoq.fr etc.).

Resolution order: (1) verso-lang cookie, (2) subdomain host header,
(3) OVERLEAF_SITE_LANGUAGE default — fully backward compatible.

Changes:
- Translations.mjs: read verso-lang cookie in middleware; include all
  bundled locale files in availableLanguageCodes regardless of subdomain
  config so every loaded locale appears in the picker
- User.mjs: add languageCode field to persist preference per user
- UserController.mjs: setLanguage handler — sets cookie (1 year) and
  writes languageCode to DB when called by a logged-in user
- AuthenticationController.mjs: on login, sync DB languageCode to cookie
  so preference follows the user to any new browser/device after login
- ExpressLocals.mjs: expose availableLanguages to all Pug templates
- router.mjs: GET /set-language?lng=<code> (anonymous + logged-in),
  POST /user/language (logged-in, REST-style)
- language-picker.pug: replace subdomain href links with /set-language
  redirect links; iterate availableLanguages instead of subdomainLang
- thin-footer.pug: show picker whenever availableLanguages.length > 1,
  not only when multiple subdomains are configured

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 11:44:56 +00:00
claude 589c60a325 i18n(de/es/it): correct machine-translation errors in German, Spanish, Italian
Build and Deploy Verso / deploy (push) Successful in 14m22s
German (43 fixes): table→Tabelle (was Tisch/furniture), figure→Abbildung (was Figur),
no_borders→Keine Rahmen (was Grenzen/national borders), run→Ausführen (was Lauf),
right→Rechts (was Richtig/correct), unpausing was "Ununterbrochen" (uninterrupted),
unlink_provider said "verknüpfen" (link) instead of "trennen" (unlink),
unpause_subscription said "pausieren" (pause) instead of "fortsetzen",
zotero_reference_loading_error mentioned Mendeley, light_themes→Helle Themen,
review_panel→Überprüfungsbereich (was Gremium/committee), x_price_per_month untranslated,
Writefull/Papers/booktabs/Labs product names preserved

Spanish (58 fixes): no_borders→Sin bordes (was fronteras/national borders),
invite_resent→Invitación reenviada (was "Invita a resentirse"),
trash→Papelera, trashed→En papelera (was "Destrozado"/destroyed),
plan→Plan (was Planificar/verb), premium→Premium (was prima/bonus),
disabled→Desactivado (was Discapacitado/handicapped), breadcrumbs navigation fixed,
cookie_banner→Banner de cookies, search_match_case fixed, Writefull/Papers/Dropbox/Git preserved

Italian (85 fixes): no_borders→Nessun bordo (was confini/national borders),
right→Destra (was Giusto/correct), run→Esegui (was Corri/run physically),
standard→Standard (was Norma), premium→Premium (was Premio/prize),
characters→Caratteri (was Personaggi), editor→Editor (was Editore/publisher) throughout,
light_themes→Temi chiari (was leggeri/lightweight), unpausing→Riattivazione,
back_to_x buttons fixed from "Torniamo" (we return) to imperative "Torna",
~20 infinitives corrected to imperatives for button labels,
booktabs/Writefull/Papers product names preserved, triple-r typo fixed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 11:30:04 +00:00
claude 38144b1033 i18n(fr): correct 41 machine-translation errors in French locale
Fix a batch of severe mistranslations produced by automated translation:
- typographic terms: bold→Gras (was "Audacieux"), figure→Figure (was "Chiffre"),
  characters→Caractères (was "Personnages"), borders→bordures (was "frontières")
- UI action labels: apply→Appliquer (was "Postuler"/job application),
  run→Exécuter (was "Courir"/to run physically), chat→Chat (was "Discuter"/verb),
  code→Code (was "Coder"/verb), more_actions→Plus d'actions (was "propositions")
- subscription plan terms: plan→Forfait (was "Planifier"/verb),
  premium→Premium (was "Prime"), standard→Standard (was "Norme")
- role/position: role→Rôle, position→Poste (both were "Grade"/military rank)
- light mode: mode lumière→mode clair throughout
- upload direction: Télécharger→Téléverser (download vs upload)
- invite_resent: catastrophic mis-parse of "resent" as resentment
- search_match_case: "Étui de correspondance" (phone case)→"Respecter la casse"
- product names preserved: booktabs, Writefull, GPT (were translated literally)
- typo: "toruvé"→"trouvé", missing spaces: "viaGit"→"via Git", "SSOconfiguration"→"Configuration SSO"
- untranslated: x_price_per_month was left in English
- accent: "Suedois"→"Suédois", "Koréen"→"Coréen", "Turque"→"Turc"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 11:17:32 +00:00
claude 00ccb9748e docs: red CAUTION block for security warning, fix hallucinated Overleaf/Typst claim
Use GitHub's > [!CAUTION] admonition (renders with red background) for the
trusted-environment security warning, matching the style used by Collabst.

Remove invented claim that Overleaf is working on Typst support — that was
a hallucination. Replace with a plain "Verso is built on Overleaf's infra"
statement. Add RevealJS as a separate ecosystem project worth supporting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 09:31:35 +00:00
claude a16ad0b977 docs: no contributions/donations, add security model + Collabst + ecosystem support
Build and Deploy Verso / deploy (push) Successful in 15m37s
- Remove Contributing section (not accepting PRs/issues)
- Add Security model section: Verso is for trusted environments only;
  point untrusted-user use cases at Overleaf non-Community offerings
- Mention Collabst as a promising open-source Typst-only alternative
  in the Verso vs Typst.app comparison
- Add Supporting the ecosystem section redirecting to Overleaf (Typst +
  RevealJS work) and the Typst project instead of Verso donations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 09:22:34 +00:00
claude c2a21da47c docs: rewrite README — positioning vs Overleaf/Quarto/Typst.app, add releases section
Build and Deploy Verso / deploy (push) Has been cancelled
Replace the "how to write a .qmd / .typ / .tex" tutorial content with a
clear positioning narrative: what Verso is vs Overleaf (same engine + more
languages), vs Quarto CLI (browser-based, collaborative, multi-language),
and vs Typst.app (self-hosted, AGPL, OT-backed, three languages).

Add a Releases section covering Alpha 1 (core multi-compiler foundation),
Alpha 2 (Typst grammar overhaul, format sub-types, Python for collaborators),
and Alpha 3 in-progress (Lumière, i18n, visual editors, upload fix).

Keep Quick start, Architecture, Env vars, and License sections intact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 09:11:02 +00:00
claude 94d3764c05 fix: stream HTTP 200 heartbeat before upload body to prevent proxy timeouts
Build and Deploy Verso / deploy (push) Has been cancelled
Uploads from slow connections consistently fail with 502 after ~60-120s
because an upstream proxy (Traefik or cloud load-balancer) has a
"first response byte" deadline that fires before the request body arrives.

Fix: add startStreamingResponse middleware (after auth, before multer)
that immediately writes HTTP 200 + Transfer-Encoding: chunked + '\n'.
With proxy_request_buffering off in Nginx, this reaches the proxy at T≈0,
so no timeout triggers. The upload body continues streaming; multer writes
to disk; the actual JSON result arrives as the final chunk. Periodic
heartbeat '\n' writes every 30s keep response-idle timeouts at bay too.

Client-side: override Uppy's getResponseData/validateStatus to trim
leading whitespace before JSON.parse so the extra '\n' bytes are ignored.
Server-side: sendUploadResponse() helper handles both streaming mode
(res.headersSent → res.end(json)) and normal mode (res.status(N).json()).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 08:59:02 +00:00
claude 8f372d13f8 fix: Lumière layout/navbar on settings+404, drop proxy_request_buffering
Build and Deploy Verso / deploy (push) Successful in 14m28s
- Move min-height:100vh from body to html:has(body[data-lumiere]) so the
  gradient fills the viewport on short pages without inflating document
  height or pushing the footer below the fold
- Remove min-height:60vh from .error-container (was causing scrollbar on
  404 when combined with thin footer)
- Replace Bootstrap 3 navbar selectors (.navbar-nav > li > a) with CSS
  custom property overrides (--navbar-link-color, --navbar-link-hover-*,
  --navbar-bg, etc.) consumed by navbar.scss — fixes header button colours
- Remove position:relative from .navbar-default override; base CSS already
  has position:absolute which provides the stacking context for ::before
- Drop proxy_request_buffering off from upload location: buffered mode +
  global client_body_timeout 15m (nginx.conf.template) is more compatible
  with multer's multipart stream handling on slow connections

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 20:38:00 +00:00
claude 211ca9c46d Fix Lumière theming + upload timeout via global middleware
Build and Deploy Verso / deploy (push) Successful in 14m26s
Theming: replace per-controller isLumiere lookups with a single
ExpressLocals middleware that sets res.locals.isLumiere for every
web request. Uses getOverallTheme() (now exported from
UserSettingsHelper) so the date-based default is handled correctly.
This covers 404, settings, setPassword, activate, and all future
server-rendered pages automatically.

Upload timeout: add client_body_timeout 15m to nginx.conf.template
at the http level (was defaulting to 60s globally). This is more
reliable than the location-specific override from build 229.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 19:33:53 +00:00
claude 2ec6ca827e Fix upload timeout + apply Lumière to settings/auth pages
Build and Deploy Verso / deploy (push) Successful in 15m0s
Nginx: add dedicated upload location with client_body_timeout 15m,
client_max_body_size 550m, and proxy_request_buffering off. Default
client_body_timeout of 60s was the actual culprit cutting slow uploads.
Node.js requestTimeout (build 228) remains as a backstop.

Lumière: pass isLumiere from UserPagesController (settings),
PasswordResetController (set-password), and UserActivateController
(first-time activation). auth.scss adds card styling for auth pages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 17:47:03 +00:00
claude 41d38a70ed Increase upload timeout to 15 min for slow connections
Build and Deploy Verso / deploy (push) Successful in 15m39s
Frontend fetch gets AbortSignal.timeout(15 min) so hung connections
fail cleanly. Server requestTimeout raised from Node default (5 min)
to match, preventing large-file uploads from being cut off server-side.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 16:58:10 +00:00
claude b8d5cb9816 fix: XS badge column, lumiere on welcome and 404 pages
Build and Deploy Verso / deploy (push) Successful in 14m39s
- XS compact row: format column 70px→96px so "QUARTO SLIDES" stays on one line;
  trim owner/date cols slightly to compensate
- Welcome page (0 projects): Lumière branch now renders before the 0-projects
  check; ProjectListLumiere renders WelcomePageContent when totalProjectsCount=0
  so new users get the full onboarding experience in the Lumière shell
- 404 page: notFound() now detects the user's overallTheme and passes isLumiere
  to the template; layout-base.pug sets data-lumiere on the body; error-pages.scss
  and project-list-lumiere.scss add [data-lumiere='true'] rules for the body
  background gradient, navbar white+stripe, and styled error box

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 16:33:23 +00:00
claude c5883e5954 feat: generate first-slide thumbnail for Quarto RevealJS presentations
Build and Deploy Verso / deploy (push) Successful in 14m10s
thumbnailFromBuild() now tries output.pdf → output-slides.pdf → decktape
on output.html (slide 1 only). The web service's ThumbnailManager already
calls this endpoint fire-and-forget on every successful compile, so RevealJS
project cards will show the first slide thumbnail automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 16:02:54 +00:00
claude 1ddd219449 fix: nowrap on format badges, widen search bar to 360px
Build and Deploy Verso / deploy (push) Successful in 14m25s
Prevents "Quarto Slides" from wrapping to two lines in XS view.
Widens search input from 300px to 360px so French placeholder text fits.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 15:44:57 +00:00
claude be6034afcf fix: remove block comments containing closing delimiter causing babel parse errors
Build and Deploy Verso / deploy (push) Successful in 15m19s
JSDoc blocks in typst-decorations.ts and quarto-decorations.ts contained
*/ sequences inside them, which Babel's parser treats as terminating the
block comment prematurely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 14:55:35 +00:00
claude 37ed70c7e9 build 223: Quarto visual editor — bold, italic, headings, inline code, strikethrough
Build and Deploy Verso / deploy (push) Has been cancelled
Add quarto-decorations.ts ViewPlugin for .qmd/.md files in visual mode:
- ATXHeading1-6: hide # prefix, apply font-size per level (2em → 1em)
- StrongEmphasis: hide ** markers, apply font-weight:700
- Emphasis: hide * or _ markers, apply font-style:italic
- Strikethrough: hide ~~ markers, apply text-decoration:line-through
- InlineCode: hide backtick markers, apply monospace + subtle bg
Markers reappear when cursor enters the node (selectionSet trigger).
Uses getChildren('EmphasisMark'/'StrikethroughMark'/'CodeMark') to locate
delimiters generically, handling both single- and double-backtick inline code.

CSS rules (.ol-cm-md-{strong,emph,strikethrough,inline-code,heading,h1-h6})
added to visual-theme.ts. Plugin wired into visual.ts after typstDecorations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 13:27:34 +00:00
claude e4dc5f3f5d build 222: Typst visual editor — bold, italic, headings, inline code
Build and Deploy Verso / deploy (push) Has been cancelled
Add typst-decorations.ts ViewPlugin that runs alongside the existing
LaTeX visual decorations. For Typst files in visual mode it:
- Hides *…* markers and applies font-weight:700 to the StrongBody
- Hides _…_ markers and applies font-style:italic to the EmphBody
- Hides = prefix marks and applies heading CSS (h1–h6 font sizes)
- Hides backtick delimiters and applies monospace to RawInlineContent
Markers reappear when the cursor enters the node (selectionSet trigger).

Register CSS rules for .ol-cm-typst-{strong,emph,heading,h1-h6,raw-inline}
in visual-theme.ts. Wire the plugin into visual.ts after markDecorations.

The visual editor toggle was already available for .typ files since 'typ'
is in validRootDocExtensions — no gating change needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 13:10:09 +00:00
claude 0058cc17b5 build 221: fix panel bg cascade, center toggle pill, Lumière XS compact rows
Build and Deploy Verso / deploy (push) Successful in 15m1s
- ide-lumiere.scss: declare --file-tree-bg/--outline-bg-color/--outline-container-color-bg:
  transparent at [data-lumiere] .ide-redesign-main scope to beat file-tree.scss +
  outline.scss which re-declare the same vars at .ide-redesign-main (closer ancestor)
- linked-file-highlight and disconnected-overlay get explicit fallback colors so they
  remain usable when --file-tree-bg is transparent
- custom-toggler: replace left:-5px with left:50%/transform:translateX(-50%) for
  reliable centering of the 16px pill over the 4px resize handle
- project-list-lumiere.scss: compact rows get teal-tinted bg + teal border (Lumière feel)
- project-format-badge-* overrides inside .project-list-lumiere so the XS compact view
  shows the soft Lumière palette (matching .lumiere-format-badge--* on cards)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 12:33:18 +00:00
claude 0754d986e9 build 220: XS compact view reuses classic table cells for full detail
Build and Deploy Verso / deploy (push) Successful in 20m14s
Replace the CSS-only compact card hack with a dedicated
ProjectCardCompact component that reuses FormatCell, OwnerCell,
LastUpdatedCell (with tooltip + updated-by), ActionsCell (full set),
and InlineTags — identical data to the classic table view. CSS Grid
aligns columns across rows: checkbox | name+tags | format | owner |
date | actions. Actions fade in on row hover.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 11:47:00 +00:00
claude 6e230e250e build 219: wider toggle pill, transparent panel bg, shorter panel names
Build and Deploy Verso / deploy (push) Has been cancelled
Resize handle toggle: 14px wide (centered over 4px handle) so the
arrow icon is clearly visible. File tree and outline backgrounds set
to transparent so the panel-group noise+gradient shows through.
FR: file_tree→"Fichiers", file_outline→"Plan" (+ hide variants).
IT: file_tree→"File", IT/ES hide keys shortened to match.
DE unchanged (Dateibaum/Gliederung already concise).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 11:35:12 +00:00
claude 4f299c6204 build 221: wider search bar, XS compact list view
Build and Deploy Verso / deploy (push) Successful in 14m55s
Search bar: set min-width: 300px so the placeholder text is fully
visible. XS zoom level (value 0): adds lumiere-card-grid--compact class
which switches cards to horizontal rows, hides the thumbnail, and shows
only name / badge / owner / date / actions in a compact strip. Stored
0.75 from old S already fell back to 1 (S); new 0 is cleanly additive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 11:06:49 +00:00
claude 261ca98103 build 220: remap tile zoom levels S/M/L
Old S (0.75×) removed — too small. Old M→S (1×), old L→M (1.35×),
new L added at 1.75×. Stored 0.75 in localStorage falls back to 1×.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 10:58:55 +00:00
claude 60f1e2c511 build 219: Lumière styling for file tree, outline, and panel dividers
Outline: override all CSS variables to use teal palette (bg, border,
hover, highlight, guide line) — was completely unstyled.
File tree: add teal inset left-accent bar on selected item.
Resize handles: narrow from 7→4px, remove dot-grip SVGs, teal hover
highlight. Collapse toggle: teal pill instead of grey.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 10:54:34 +00:00
claude f6bded1596 build 218: Verso logos on set-password and email-check pages, i18n zoom aria-label
Replace Overleaf icons with Verso branding on setPassword.pug
(square icon) and primaryEmailCheck.pug (wordmark). Replace hardcoded
French aria-label "Taille des cartes" on zoom control with t('card_size')
and add card_size key to all 5 locale files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 10:48:20 +00:00
claude 2f4b60574c build 217: square logo on password reset, align tile colours to classic badges
Build and Deploy Verso / deploy (push) Successful in 15m11s
Use verso-square.svg (small icon) instead of the wordmark on the
password reset page. Align card thumbnail gradients and format badge
colours to match the classic project list badge colours (LaTeX green
#098842, Typst teal #239dad, Quarto blue #447099, Quarto Slides
pink-red #e4637c). Split quarto-slides badge from quarto badge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 10:38:05 +00:00
claude 29880d1cf9 build 216: Verso logo on password reset page, fix SSO/ES translations
Build and Deploy Verso / deploy (push) Successful in 15m31s
Replace Overleaf icon with Verso wordmark on passwordReset.pug.
Fix ES password_reset_email_sent (meaning error: said the password
was sent, not a link). Translate <0>Log in with SSO</0> link text
in FR/DE/IT/ES (was left in English in all languages).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 10:15:37 +00:00
claude 9de127f879 i18n: fix login button and related strings in FR/DE/IT/ES
Build and Deploy Verso / deploy (push) Successful in 19m6s
- FR login button: 'Identifiant' (username field label) → 'Connexion'
- ES forgot_your_password: missing closing '?' added
- DE logging_in: 'Anmeldung' (noun) → 'Anmelden' (verb, fits spinner)
- IT logging_in: 'Entrata in corso' → 'Accesso in corso'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 09:51:16 +00:00
claude ae4e95312d i18n: translate hardcoded strings on project page and footer
Build and Deploy Verso / deploy (push) Successful in 17m39s
- Card owner: "You" now goes through t('you') so it renders as
  "Vous"/"Tu"/"Du" etc. instead of always "You"
- Relative dates (fromNow): moment.locale() is now set from the app
  language so "2 days ago" becomes "il y a 2 jours" etc.
- Footer: "Built on", "Source code", "AGPL licence" are now translated
  via t() with keys added to all locale files and extracted-translations
- New keys: built_on, source_code, agpl_licence (FR/DE/IT/ES translations
  added manually)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 09:33:22 +00:00
claude 093c25f3dd i18n: validate and fix 212 translation errors across FR/DE/IT/ES
Build and Deploy Verso / deploy (push) Successful in 13m46s
Automated validation pass found and corrected:
- Brand names translated literally (Overleaf→"au verso"/dorso/retro/umseitig,
  Verso→"verso", LaTeX→"látex", Quarto→"en cuarto", TeXGPT→"TestoGPT")
- React-Trans <N> tags eaten by Google Translate (5 strings)
- FR grammar: "va sera écrasé" → "sera écrasé"
- FR preposition: "en <b>__email__</b>" → "sur <b>__email__</b>" (×2)
- FR title: "Accepter l'erreur de modification" → "Erreur d'acceptation des modifications"
- FR redundancy: "la version Rolling TeX Live" → "le build Rolling TeX Live"
- ES: "mesa" (furniture) → "tabla" (document table) in 3 strings

Tooling committed: translate_missing.py, fix_translations.py,
validate_translations.py — reusable for future locale additions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 09:16:57 +00:00
claude 375c18873e i18n: complete FR/DE/IT/ES translations via Google Translate (free tier)
Build and Deploy Verso / deploy (push) Successful in 14m37s
All four locales are now at 100% key coverage (was FR 38%, DE 50%,
IT 12%, ES 21%). Translated ~7,700 missing keys using deep-translator
with placeholder preservation (__varName__, <N>…</N> React-Trans tags).
Manual corrections can be made on top of this baseline as needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 22:15:47 +00:00
claude 3ee564ef70 ui: fix header row layout, selected-tile contrast, toolbar gradient
Build and Deploy Verso / deploy (push) Successful in 14m19s
- Header: revert column layout; title+zoom stay on left, search+button
  on right — all on one row (flex-wrap handles narrow viewports)
- Selected cards: switch from teal tint (invisible on teal bg) to solid
  white + blue ring, clearly distinguishable from the page background
- Editor toolbar: replace flat teal wash with an exponential-like decay
  (20%→9%→3%→0% teal overlay) so only the very top has colour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 20:47:31 +00:00
claude 4ca6ec2b58 ui: move zoom picker next to section title, fix tooltip hover-stealing
Build and Deploy Verso / deploy (push) Successful in 14m18s
S/M/L buttons now appear inline with the page title rather than in the
action bar. Bootstrap tooltips get pointer-events:none so they can no
longer steal :hover from the card beneath them, preventing the lift
animation from flickering when hovering over tag dots.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 20:09:48 +00:00
claude be529e53f6 Fix translation keys and remove sidebar logo border
Build and Deploy Verso / deploy (push) Successful in 14m14s
- extracted-translations.json: add n_projects_selected, n_projects_selected_plural,
  deselect_all — the translations-loader.js only bundles keys present in this file,
  which is why the FR translations were silently stripped from the webpack chunk
- Revert lang.default workaround in i18n.ts (not needed)
- SCSS: remove border-top above .ds-nav-verso-logo in sidebar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 18:31:04 +00:00
claude 592b4d3dad Fix translations, center logo/footer, add tile zoom control
Build and Deploy Verso / deploy (push) Successful in 14m8s
- i18n: unwrap webpack module object on dynamic JSON import (lang.default ?? lang)
  so French bundle keys are correctly registered in the i18next store
- Login logo: use flex centering on wrapper instead of display:block + margin:auto
- Footer (project list + login): align-items:center on .row for vertical centering
- Tile zoom: S/M/L control in header with CSS custom property (--lum-card-scale)
  that scales grid column width and card thumbnail height; persisted in localStorage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 17:20:21 +00:00
claude d08e834f49 fix: footer default font size, tag dots inline in meta row, OLTooltip
Build and Deploy Verso / deploy (push) Successful in 14m57s
- Footer: remove font-size/letter-spacing overrides so the browser
  default applies; monospace right col keeps its uppercase + tracking
  but no longer shrinks the font
- Card tags: move coloured dots into the .lumiere-card-meta flex row
  instead of a separate div — zero added height, all cards uniform
- Card tags tooltip: replace native title attr with OLTooltip (React
  Bootstrap tooltip) for a consistent Verso look on hover

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 13:55:36 +00:00
claude 93c00d51a7 fix: lighter footer, dot-only card tags, deselect_all fr translation
Build and Deploy Verso / deploy (push) Successful in 14m46s
- Footer: pale teal (#edf7f4) with noise grain instead of dark navy;
  serif author credit darker (#1a2e3b), right col monospace muted;
  teal→blue 2px stripe preserved; same treatment on login page
- Card tags: render as 8px coloured dots only (title attr for tooltip)
  — no text means no wrapping, consistent card heights across the grid
- i18n fr.json: add deselect_all → "Tout désélectionner"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 13:20:33 +00:00
claude 7df583a3b2 feat: creative footer, card tags, selection bar theme, i18n fixes
Build and Deploy Verso / deploy (push) Successful in 15m23s
- Footer: dark navy (#0d1b24) with noise texture, teal→blue gradient
  top stripe; serif author credit on the left, monospace/uppercase
  meta links on the right; same design on login page
- Thumbnail: larger inset (14px top, 12px sides) so more background
  gradient shows around the preview; subtle drop shadow added
- Card tags: projects now show their label chips on the card (colored
  dot + name, read-only, max 3, no close button inside the link)
- Selection bar: btn-secondary recoloured to Lumière palette; dropdown
  menus rounded with teal accent on hover
- i18n fr.json: add n_projects_selected, n_projects_selected_plural,
  toolbar_selected_projects (×4 variants)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 12:54:22 +00:00
claude 194ffe54db fix: target footer.site-footer (ThinFooter) in Lumière theme
Build and Deploy Verso / deploy (push) Successful in 14m49s
Verso forces showThinFooter=true for non-SaaS instances, rendering
footer.site-footer everywhere — never .fat-footer. All previous footer
theme rules silently matched nothing. Fix both project and login page
footer selectors. Also increase login logo max-width to 520px, and
remove bottom inset/radius from thumbnail (folder-behind-card effect).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 12:23:36 +00:00
claude f9622820c7 i18n: fix Verso-branded keys and wire welcome message titles
Build and Deploy Verso / deploy (push) Successful in 21m59s
en.json: add browse_templates and learn_latex_with_a_tutorial.

fr.json:
- fix agree_with_the_terms (said "Overleaf", now says "Verso")
- add browse_templates + learn_latex_with_a_tutorial (FR)
- add 7 Verso-branded keys missing entirely from FR:
  add_manager_user_not_found, compile_timeout_explanation,
  download_metadata, institution_has_overleaf_subscription,
  one_step_away_from_professional_features,
  to_confirm_email_address_you_must_be_logged_in_with_the_requesting_account,
  welcome_to_overleaf_opening_workspace

welcome-message.tsx: replace two hardcoded English title strings with
t('learn_latex_with_a_tutorial') and t('browse_templates').

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 11:00:02 +00:00
claude 933efb1ff9 fix: thumbnail sizing, parallax hover effect, login logo size
Build and Deploy Verso / deploy (push) Successful in 14m45s
Thumbnail: explicit top/left/right/bottom + width/height on the img so
object-fit has a well-defined box in all browsers (inset shorthand alone
was underspecified for some).

Hover effect: the card already rises translateY(-3px) on hover. Add a
matching translateY(+3px) counter-transform on the thumbnail image so
its net viewport motion is zero — the document preview appears to float
in place while the gradient tile and card body lift up around it.

Login logo: raise max-width from 300px to 400px now that the competing
inline style has been removed from the pug template.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 10:41:14 +00:00
claude 0bbc07e0b9 fix: improve thumbnail quality and show gradient border around preview
Build and Deploy Verso / deploy (push) Successful in 15m20s
Increase pdftocairo output from 190px/q50 to 380px/q82 — 2× resolution
for crisp rendering on retina displays, higher quality to eliminate
visible compression artefacts.

Inset the thumbnail image 6px from the tile edges (inset: 6px) with a
4px border-radius so the card's colour gradient is visible as a frame
around the document preview.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 10:10:24 +00:00
claude 7da02d9e3a fix: use pdftocairo directly for thumbnails, no Docker image needed
Build and Deploy Verso / deploy (push) Successful in 15m11s
The previous implementation delegated to ConversionManager which uses
the Docker-based CommandRunner and is gated behind enablePdfConversions
(ENABLE_PDF_CONVERSIONS env var). Neither is configured in the Verso
deployment, so every thumbnail request 404'd before doing any work.

poppler-utils (which provides pdftocairo) is already installed directly
in the CLSI base image via install_deps.sh. Rewrite thumbnailFromBuild
to call pdftocairo via execFile instead — no feature flag, no Docker
image, no ConversionManager indirection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 09:42:35 +00:00
claude b70c8ddd0e feat: show compiled PDF thumbnail in Lumière project cards
Build and Deploy Verso / deploy (push) Successful in 14m47s
After a successful compile, web service calls a new CLSI endpoint
(GET /project/:id/user/:uid/build/:bid/thumbnail) which runs pdftocairo
page-1 to a 190px-wide JPEG using the existing thumbnail preset. The
JPEG is stored in Redis (90-day TTL, overwritten on next compile) by
the new ThumbnailManager.

GET /project/:Project_id/thumbnail serves the cached JPEG to authenticated
users, returning 404 when no thumbnail exists. Project cards in the
Lumière grid show the image overlaying the coloured gradient tile; if
the image 404s (project never compiled or cache expired) the onerror
handler hides it and the gradient + initial letter shows through.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 09:20:48 +00:00
claude 88ddbd2513 fix: center Verso logo on all pages and theme both footers
Build and Deploy Verso / deploy (push) Successful in 14m59s
Trim SVG viewBox from 760 to 590 (content ends ~x=570; the blank
right whitespace was making the wordmark look left-biased). Remove the
scale(1.2) transform from the sidebar logo — the negative-margin
container already fills the sidebar width. Change login logo max-width
to be CSS-controlled only (removed inline 480px override).

Footer: switch to `background` shorthand !important so the dark-theme
`var(--footer-background)` shorthand can't compete; deepen the teal to
#c8e4de so the Lumière colour is clearly visible. Add a
`body:has(.login-page) .fat-footer` rule so the login-page footer gets
the same treatment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 08:59:22 +00:00
claude 464aee7612 Add per-card action buttons to Lumière project grid
Build and Deploy Verso / deploy (push) Successful in 14m46s
Restructures ProjectCard so the card is a <div> container instead of <a>
(buttons cannot be nested inside anchor elements). A .lumiere-card-link
anchor wraps the thumb+body area; a .lumiere-card-actions strip sits below
it and fades in on hover.

Buttons added (reusing the same tooltip components as the classic table):
  - Copy project (opens CloneProjectModal)
  - Download project zip
  - Compile & download PDF
  - Archive project (skipped when already archived)
  - Trash project (skipped when already trashed)

Action icons are coloured $lum-text-muted at rest and shift to $lum-teal
on hover, matching the Lumière palette.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 08:17:45 +00:00
claude e3929572c3 Fix Lumière visual regressions: login, footer, logo, notifications
Build and Deploy Verso / deploy (push) Successful in 14m31s
- Login: centre logo (display:block + margin:auto, max-width 300px) and
  increase h1 from 1.4rem to 1.75rem
- Sidebar logo: switch from width:120%/margin-left:-10% to transform:scale(1.2)
  so the image scales symmetrically from centre and isn't cut on the left
- Footer: use !important on background-color/color to beat the dark-theme
  selector's higher specificity (:root [data-theme] = 0,4,0 vs our 0,3,0)
- Notifications: replace near-transparent rgba backgrounds with solid
  opaque colours so the teal page gradient can't bleed through; make the
  CTA button neutral slate-grey (not teal) with border-radius:8px

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 08:10:52 +00:00
claude 7ed1d02271 Lumière: replace pill toggle with rounded-square + sliding indicator
Build and Deploy Verso / deploy (push) Successful in 14m52s
The Code/Visual editor toggle now uses a teal sliding indicator (::before
pseudo-element) that glides between tabs via translateX instead of the
plain background-color crossfade. Container and labels get border-radius:
10px/7px to match the rest of the Lumière toolbar's rounded-square style.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 16:44:44 +00:00
claude aec466c6f3 Enlarge sidebar logo 20% and fix compile button corner rounding
- Logo (all themes): scale the Verso wordmark to 120% width, centered and
  clipped to the sidebar column — the word mark visually fills the full
  sidebar width. Uses overflow:hidden + width:120% !important + margin-left:-10%
  to override the existing inline width style.
- Compile button (Lumière): replace the all-corners border-radius:7px on the
  split button group with corner-specific rules — .compile-button gets 7px on
  the left side only, .compile-dropdown-toggle gets 7px on the right side only,
  so the shared inner edge stays flat as expected for a joined split button.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 16:35:11 +00:00
claude 8d3f550cef Fix Lumière notification colors, style CTA button, and theme footer
- Warning notification: switch from teal to amber (#b45309) so it reads as
  a genuine warning and doesn't blend into the teal UI chrome
- Notification CTA button (.btn-secondary): style with teal tint so the
  'Send confirmation code' button matches the Lumière theme
- Footer: override dark footer on .project-list-lumiere with a light teal
  background (#edf7f5), dark text, teal section headings — selector has same
  specificity as the default-theme dark rule but appears later in the cascade

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 16:16:28 +00:00
claude e19cd6e8ba Add multi-select to Lumière project card grid
Each project card now has a checkbox (top-left corner, semi-transparent by
default, fully opaque on hover). When any card is selected a selection bar
slides in above the grid showing: select-all checkbox, count, the existing
bulk-action toolbar (archive, trash, tags, delete), and a deselect-all button.
:has(input:checked) keeps all checkboxes visible once a selection is active.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 16:03:15 +00:00
claude 484a2e3aac Apply Lumière theme to editor settings modal and login page
- Settings modal: teal accent stripe on header, teal gradient nav background,
  teal active-tab highlight, teal section titles, teal focus rings on form
  controls — scoped via :has(.ide-settings-modal-body) so other modals are
  unaffected
- Login page: grainy teal gradient background, white rounded-square card with
  teal/blue accent stripe, teal labels, focus rings, primary button — always
  applied since users haven't set a theme yet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 15:53:10 +00:00
claude c70ec1c501 feat(lumiere): Lumière-styled notification banners
Build and Deploy Verso / deploy (push) Successful in 20m14s
Override Bootstrap orange/yellow warning and generic blue info colors
with the Lumière teal palette. Warning banners now use a soft teal
tint instead of orange; info banners use the Lumière blue. Both types
get 10px border-radius and a subtle shadow to match the card style.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 15:35:35 +00:00
claude 466f908f7c fix: admin button font size, extend button rounding to PDF toolbar
Build and Deploy Verso / deploy (push) Has been cancelled
- Remove font-size: 0.8rem from Admin navbar button (was shrinking text)
- Add border-radius: 7px to .toolbar-pdf .btn so the Recompile, Logs and
  Download buttons in the PDF panel get the Lumière rounded-square shape
- Add border-radius to compile-button-group .btn to cover the dropdown
  arrow toggle next to the Recompile button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 15:33:44 +00:00
claude 9e618280b1 fix: restore theme toggle in navbar account menu, Lumière button styling
Build and Deploy Verso / deploy (push) Successful in 21m51s
- logged-in-items: pass showThemeToggle to AccountMenuItems so the theme
  switcher is accessible from the top-right navbar (was lost when the
  sidebar account icons were removed); AccountMenuItems already gates on
  hasOverallThemes so it's a no-op on non-themed pages
- project-list-lumiere: restyle Account + Admin navbar buttons — rounded
  square (8px) instead of pill, teal resting tint on Account, subtle
  teal border on hover; matches Lumière design language
- ide-lumiere: extend rounded-square styling to all toolbar action buttons
  (Share, Present, History, Layout, File/Edit/Help menu buttons) via
  .ide-redesign-toolbar-actions and .ide-redesign-toolbar-menu selectors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 14:59:48 +00:00
claude 62847fbc15 Fix remaining Overleaf branding in email confirmation notifications
Replaces "Overleaf subscription"/"Overleaf Commons"/"Overleaf premium features"
with Verso equivalents in the institution subscription and commons upgrade
notification strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 14:37:33 +00:00
claude fc535590eb fix: restore account button, fix double border, logo, gradient, editor buttons
Build and Deploy Verso / deploy (push) Successful in 14m48s
- project-list-ds-nav.scss: remove display:none for .nav-item-account on
  desktop — it was hidden because the sidebar handled it, but now the sidebar
  no longer has the account icon so this made it invisible everywhere
- logged-in-items / nav-dropdown-menu: show User icon alongside 'Account'
  text in navbar dropdown so it's recognisable as an account button
- Lumière: remove border-top from .ds-nav-verso-logo (was doubling up with
  .ds-nav-sidebar-lower border)
- Logo hover: drop scale transform in both themes, use filter:brightness only
- Gradient: drop background-attachment:fixed (unreliable in scroll containers);
  switch to circle gradients at 0.60/0.45 opacity; base colour #e8f5f2
- Editor ide-lumiere: rounded square (7px) on .ol-cm-toolbar-button with teal
  hover/active states to match the Lumière design language

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 14:27:40 +00:00
claude 5fa73fa8e3 fix: rebrand Overleaf strings, move user menu, fix logo + button alignment
Build and Deploy Verso / deploy (push) Successful in 14m49s
- en.json: replace 'Overleaf' with 'Verso' in 6 user-visible strings
  (email_already_registered, add_manager_user_not_found, compile_timeout,
  download_metadata, to_confirm_email address, welcome_opening_workspace)
- groups-and-enterprise-banner: use dynamic appName instead of hard-coded
  'Overleaf'
- SidebarLowerSection: add showAccountIcons prop (default true); set false
  in project dashboard sidebar — account menu is already in the top-right
  navbar, so the bottom-left duplicate is removed for all themes
- ds-nav-verso-logo: replace opacity-fade hover with scale+brightness
  transform so logo is fully visible at rest
- Lumière: scope new-project-dropdown sidebar padding to avoid misaligning
  the button when it appears next to the search bar in the header

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 13:58:17 +00:00
claude 074ff2791d feat(lumiere): bold background + creative editor theme
Build and Deploy Verso / deploy (push) Successful in 14m43s
Dashboard:
- Grain opacity 0.12→0.28, teal orb 0.22→0.45, blue orb 0.16→0.32
- Added soft centre glow for depth; tinted base colour #f0f7f5

Editor:
- Toolbar: visible teal-wash gradient (dark→light) + 4px accent stripe
  + teal box-shadow
- Rail icon strip: teal-tinted gradient (#cceee8→#daf2ee)
- File tree / outline panel: full grainy gradient (matching dashboard)
  with teal/blue orbs + panel header with teal left-glow
- Compile button: teal gradient replacing the default blue

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 13:40:40 +00:00
claude a0d1829c1b fix(lumiere): fix toolbar dropdown clipping, boost grain/gradient visibility
Build and Deploy Verso / deploy (push) Successful in 14m29s
- Remove overflow:hidden from toolbar — it was clipping dropdown menus
- Increase SVG noise opacity 0.06→0.12 and gradient orb opacity for more
  visible texture on the dashboard background

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 12:21:34 +00:00
claude ac02fd707e feat(lumiere): grainy gradient background + editor chrome theme
Build and Deploy Verso / deploy (push) Successful in 15m0s
- Replace flat #f0f4f8 main content bg with layered grainy gradient:
  soft radial teal/blue orbs (background-attachment: fixed) plus
  SVG feTurbulence noise tile for organic depth
- Cards get backdrop-filter glass effect over the textured surface
- New ide-lumiere.scss: sets body[data-lumiere] via useThemedPage, then
  overrides toolbar (white + 3px teal→blue accent stripe), rail (teal
  active states), and file-tree (teal selected/hover) CSS variables

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 11:39:58 +00:00
claude 10a17c5443 feat: modernize Lumière sidebar and navbar styling
Build and Deploy Verso / deploy (push) Successful in 14m36s
- Navbar: teal-to-blue gradient accent stripe, soft shadow replacing
  hard border, pill-shaped hover on nav links
- New Project button: gradient teal fill, elevated shadow on hover
- Filter buttons: pill shape, solid teal active state, teal tint hover
- Tags section: uppercase teal section header, refined tag items
- Account/help icons: circular buttons with teal hover ring and glow
- Theme toggle: teal selected indicator
- Verso logo: subtle top separator with hover opacity transition

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 11:02:36 +00:00
claude 13577693f2 fix: restore sidebar CSS context in Lumière dashboard
Build and Deploy Verso / deploy (push) Successful in 15m50s
The Lumière container now carries project-ds-nav-page and
project-list-wrapper so the sidebar picks up all its existing styles.
The grey-rectangle button issue and broken sidebar layout were caused
by those expected parent classes being absent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 09:37:08 +00:00
claude 0b616436cf feat: toggle bold/italic and add underline, smallcaps, link for Typst editor
Build and Deploy Verso / deploy (push) Has been cancelled
Bold (Ctrl+B) and italic (Ctrl+I) now unwrap when the cursor is already
inside a Strong/Emphasis node. Added #underline[…] and #smallcaps[…]
wrap commands (toolbar only) and #link("")[…] with Ctrl+K shortcut that
places the cursor in the URL field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 09:31:28 +00:00
claude 926b6f7cbb feat: add Verso Lumière theme with card-based project dashboard
Build and Deploy Verso / deploy (push) Successful in 14m45s
New theme with gradient document cards, serif title typography and a
light airy palette. Set as the default for new users. Existing users
keep their current theme; all users can switch via the theme toggle
(new sparkle icon). Classic Dark / Classic Light are renamed accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 09:13:46 +00:00
claude 31db7b2b4e feat: add bold/italic shortcuts and toolbar buttons for Typst editor
Build and Deploy Verso / deploy (push) Successful in 14m39s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 08:28:47 +00:00
claude 4899afd45f fix: enable shell-escape in test deployment for svg package support
Build and Deploy Verso / deploy (push) Successful in 1m6s
OVERLEAF_LATEX_SHELL_ESCAPE=true was added to the prod workflow but
missed in the test workflow, so the svg package still failed on
test.alocoq.fr despite inkscape being installed in the image.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 15:10:06 +00:00
claude 8b6b76c2fa fix: install inkscape after pip to avoid apt/pip numpy conflict
Build and Deploy Verso / deploy (push) Successful in 1m12s
inkscape's apt dependencies include python3-numpy, which pip can't
uninstall (no RECORD file). Moving inkscape to its own RUN layer after
the pip installs avoids the conflict: pip numpy lands in /usr/local/lib
first, then apt installs its numpy into /usr/lib alongside it, and
Python resolves /usr/local/lib first at import time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 12:49:08 +00:00
claude 4285d8d74c fix: add --ignore-installed so pip can coexist with inkscape's apt numpy
Build and Deploy Verso / deploy (push) Successful in 1m8s
inkscape pulls in python3-numpy 1.26.4 via apt; pip can't uninstall apt
packages (no RECORD file). --ignore-installed makes pip install its own
copy into /usr/local/lib without touching the apt version; /usr/local/lib
takes import precedence so runtime code gets the pip-managed numpy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 11:12:11 +00:00
claude d8ce7f9dc1 feat: support svg package — install Inkscape and enable shell-escape
Build and Deploy Verso / deploy (push) Has been cancelled
The LaTeX svg package converts .svg files to PDF at compile time by
shelling out to Inkscape (requires --shell-escape). Without Inkscape in
the image and the flag enabled, compilation fails with "Did you run the
export with Inkscape?".

- Dockerfile-base: add inkscape to the apt install block
- settings.js: expose OVERLEAF_LATEX_SHELL_ESCAPE env var → clsi.latexShellEscape
- LatexRunner.js: pass -shell-escape to latexmk when the setting is on
- deploy-verso-prod.yml: set OVERLEAF_LATEX_SHELL_ESCAPE=true (trusted-user instance)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 10:57:56 +00:00
claude 8b7da8296c fix: pass session token so anonymous users can install python packages
Build and Deploy Verso / deploy (push) Successful in 14m23s
Build and Deploy Verso (prod) / deploy (push) Successful in 3m55s
userCanInstallPython passed null as the token, so anonymous users
accessing via a share link got privilege level NONE from the WithoutUser
path and allowPythonInstall was always false for them.

Read the token from req.session.anonTokenAccess via
TokenAccessHandler.getRequestToken and forward it through
userCanInstallPython to getPrivilegeLevelForProject.  For TOKEN_BASED
projects this resolves the anonymous user's access level via
getPrivilegeLevelForProjectWithToken, enabling package installation.

Also update Quarto Slides badge color to #e4637c.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 09:17:30 +00:00
claude e6773c6baf fix: read stored project compiler for quartoFlavor update
Build and Deploy Verso / deploy (push) Successful in 20m28s
options.compiler is set from req.body.compiler which the frontend never
sends, so the condition was never true and quartoFlavor was never written.
Use ProjectGetter to read the stored compiler instead. Fire-and-forget so
it does not delay the compile response.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 08:48:25 +00:00
claude 2c8dad08f6 project-list: show plain 'Quarto' badge when quartoFlavor is unset
Build and Deploy Verso / deploy (push) Successful in 13m55s
Existing projects have no quartoFlavor value in the database (new field),
so defaulting to 'Quarto PDF' incorrectly labelled all of them. Show the
plain 'Quarto' label until the first compile sets the flavor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 08:30:34 +00:00
claude 7fecaf491a compile: allow python package install for all users who can access the project
Build and Deploy Verso / deploy (push) Successful in 9m23s
Previously userCanInstallPython used ignorePublicAccess: true, which
blocked token-link users (not-yet-joined) and logged-in readers of public
projects from installing packages. This caused Quarto presentations with
Python cells to fail for shared read-only users even when the required
packages were already listed in requirements.vrf.

The security model is: what gets installed is fully controlled by
requirements.vrf, which is only writable by members with write access.
There is therefore no security reason to block other readers from
triggering installation of already-approved packages.

Drop ignorePublicAccess so all users with any privilege level (direct,
token-based, or public-project) can trigger the venv install.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 08:20:46 +00:00
claude 9c7a10aa39 fix: correct import path and minor issues in quartoFlavor update
Build and Deploy Verso / deploy (push) Successful in 13m44s
- Fix wrong import path '../models/Project.mjs' → '../../models/Project.mjs'
  (from Features/Compile/, '..' is Features/, not src/; the server would
  crash on startup with ERR_MODULE_NOT_FOUND in Node.js ESM)
- Log MongoDB errors instead of silently swallowing them
- Remove null from Mongoose String enum (not a valid enum value for strings)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 08:06:00 +00:00
claude bbf532d282 project-list: distinguish Quarto PDF vs Quarto Slides in format badge
Build and Deploy Verso / deploy (push) Successful in 9m30s
Add a quartoFlavor field ('revealjs' | 'pdf') to the Project model.
After each successful Quarto compile, CompileController detects the output
type (output.html → revealjs, otherwise pdf) and persists it.
ProjectListController includes it in the projection and serialization so
it reaches the frontend without an extra round-trip.

Badge variants:
  - quartoFlavor unset (new/uncompiled) → "Quarto PDF" #447099
  - quartoFlavor 'pdf'                  → "Quarto PDF" #447099 (Quarto blue)
  - quartoFlavor 'revealjs'             → "Quarto Slides" #7e56c2 (purple)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 07:55:45 +00:00
claude eada1e9979 file-tree: show python packages button for all quarto projects
Build and Deploy Verso / deploy (push) Successful in 13m42s
The previous approach (pdfFile?.path === 'output.html') caused a
chicken-and-egg problem: the button only appeared after a successful
RevealJS compile, but you need to add packages before the first compile.

Use compiler === 'quarto' from ProjectSettingsContext instead — this is
set from project metadata and available immediately, before any compile.
Quarto supports Jupyter Python cells in all output formats (RevealJS HTML,
PDF via LaTeX, PDF via Typst), so showing the button for any Quarto project
is the correct behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 07:41:21 +00:00
claude 2f88ad124d ui: correct LaTeX badge to Overleaf button green #098842
Build and Deploy Verso / deploy (push) Successful in 13m47s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 07:05:08 +00:00
claude a398127522 ui: brand badge colors and hide python packages for non-revealjs
Build and Deploy Verso / deploy (push) Successful in 13m8s
- LaTeX badge: #13c965 (Overleaf brand green, from upstream overleaf/overleaf)
- Typst badge: #239dad (Typst brand blue/teal, from typst.app)
- Python packages toolbar button: only shown when the compiled output is
  output.html, i.e. a Quarto RevealJS presentation.  Uses the same
  pdfFile?.path === 'output.html' check as PresentationPreviewButton.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 06:51:30 +00:00
claude 083b195462 typst: support '+' binary operator in arg values
Build and Deploy Verso / deploy (push) Successful in 9m27s
stroke: 0.8pt + brand broke arg-list parsing because '+' was not a grammar
terminal. The parser exited CodeArgs via error recovery, so subsequent
named args (radius:, inset:, fill:) were never seen as CodeArgKey.

Add codeArgValue { codeValue | codeArgValue !add "+" codeValue } — a
left-recursive inline rule used only inside CodeArgs.  The !add cut point
gives the shift strict dominance over the reduce (prec add > 0 vs 0), so
a '+' after a value greedily extends the expression.  Because codeArgValue
only appears inside CodeArgs, the codeStatement* LALR-merging that caused
trouble for the earlier callSuffix* approach does not apply here.

Also add PLUS to codeIdentTokenizer's valid-predecessor list so identifiers
after '+' (the right-hand operand) are correctly tokenized as CodeIdent.
Add "+" to @tokens @precedence so it beats MarkupContent in merged states.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 06:07:32 +00:00
claude 0656ddfe52 fix(typst): highlight keywords/idents inside #{} code blocks
Build and Deploy Verso / deploy (push) Successful in 9m32s
Replace the opaque CodeBlockBody external tokenizer with grammar-parsed
codeStatement* so that keywords (show, let, set, …) and identifiers
inside #{ } code blocks receive proper Lezer nodes and are highlighted.

Key grammar changes:
- CodeBlock { "{" codeStatement* "}" } — structured, not opaque
- codeStatement uses two explicit alternatives for keyword lines:
    CodeKeyword !kw callOrValueAndBody  (grabs the subject eagerly)
    CodeKeyword keywordBody?            (bare keyword or body-only form)
  The !kw cut-point gives shift prec kw > 0 over the unannotated reduce,
  resolving the LALR merge ambiguity without @left/@right on kw.
- callOrValue { FuncExpr | CodeIdent | CodeString } — replaces CallExpr
  { CodeIdent !call callSuffix* }.  The * quantifier annotated both
  shift and reduce with !call, making them a same-prec tie that @right
  could not reliably resolve in merged states.  Using FuncExpr (required
  callSuffixes) + bare CodeIdent makes the tie strict (call > 0 for
  FuncExpr shift vs 0 for bare-ident reduce), then @right handles only
  the extension-of-callSuffixes case (shift = call<<2, FuncExpr reduce
  = call<<2 - 1 via @right encoding).
- KeywordExpr gets the same two-alternative structure as codeStatement
  so nested show/set/let inside a code block (e.g. show sel: set text)
  also parse without LALR state-merge conflicts.
- CallExpr removed; its role is split between FuncExpr (has args/chain)
  and bare CodeIdent (no args).  Styling updated: CodeExpr/CodeIdent
  replaces CallExpr/CodeIdent for bare #ident function-style highlights.
- codeKeywordTokenizer and codeIdentTokenizer already accept keywords /
  identifiers after { and ; (added in previous commit) — consistent with
  the new grammar.

Parse results:
  #{ show strong: link.with(url); body }
  → CodeKeyword "show", CodeIdent "strong", FuncExpr "link.with(url)",
    CodeIdent "body" — all properly highlighted, no ⚠ errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 21:47:21 +00:00
claude 056d9a7f47 fix(typst): add CodeArray so tuple args don't break nested call parsing
Build and Deploy Verso / deploy (push) Successful in 9m44s
align: (left, center, left) is a Typst array literal.  Without a grammar
rule for it, the parser treated the ')' closing the tuple as the ')' closing
the enclosing function call, so everything after align: — all ContentBlock
args and any subsequent named keys like 'caption:' — fell outside the parsed
call tree and was highlighted as MarkupContent.

Add CodeArray { "(" codeArgList? ")" } as a codeValue alternative so
parenthesised arrays and dictionaries parse correctly.  Also regenerate
typst.mjs / typst.terms.mjs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 20:13:38 +00:00
claude 8ed44cc352 fix(typst): style CodeArgKey via HighlightStyle, not theme CSS
Build and Deploy Verso / deploy (push) Successful in 13m38s
The tok-attributeName CSS class relies on each theme defining it, but
26 of 41 themes never had it. Defining the colour directly in
typstHighlightStyle (like we do for heading/strong/emphasis) applies
it universally regardless of which theme is active.

Amber #c47900 is legible on both light and dark backgrounds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 19:56:20 +00:00
claude 52ebff6286 debug(typst): force arg keys bright red to confirm token pipeline
Build and Deploy Verso / deploy (push) Successful in 10m0s
Adds { tag: t.attributeName, color: '#cc0000', fontWeight: 'bold' } to
typstHighlightStyle so named arg keys are unmistakably red if the
CodeArgKey token is reaching the highlighter.  Will be removed once
the pipeline is confirmed working and replaced with per-theme colors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 19:41:57 +00:00
claude 7c0ec9dd39 fix(typst): commit compiled grammar so CI always uses current parser
Build and Deploy Verso / deploy (push) Successful in 13m45s
The webpack plugin that compiles typst.grammar may silently skip
recompilation when file mtimes are ambiguous in Docker BuildKit layers.
Committing typst.mjs and typst.terms.mjs guarantees the build always
ships the correct parser without depending on build-time generation.

To regenerate after grammar changes:
  node -e "const {buildParserFile}=require('/tmp/lezertest/...'); ..."
  (or: yarn run lezer-latex:generate from services/web)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 19:02:16 +00:00
claude e9a34a5bd8 fix(typst): exclude ] from MarkupContent so ContentBlock always closes
Build and Deploy Verso / deploy (push) Successful in 13m35s
The core bug: MarkupContent { ![...]+ } did not exclude ']', so inside
#figure(table([A],[B]), caption:[...]) the tokenizer consumed ']' as
MarkupContent, ContentBlocks never closed, and all remaining args like
'caption:' were swallowed as MarkupContent instead of CodeArgKey.

Fix mirrors the LaTeX grammar pattern (its Normal token excludes \] and
\[): add ']' to MarkupContent's exclusion set and provide ClosingSquare
{ "]" } as an item alternative for bare ']' in body text.  The grammar's
existing @precedence { "]" ClosingSquare } ensures "]" wins and closes
the ContentBlock; outside a ContentBlock only ClosingSquare is valid.

Also change URL style tag from t.url (tok-url, unstyled in all themes)
to t.string (tok-string, styled in every theme).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 18:33:20 +00:00
claude f9fc0d9905 typst: fix URL comment split and heading label highlighting
Build and Deploy Verso / deploy (push) Successful in 13m56s
- Add URL token (https://... / http://...) so '://' is never split into
  ':' + LineComment '//', preventing URLs from being styled as comments
- Stop headingTitleTokenizer before '<label>' patterns so labels at the
  end of headings get Label node styling instead of being consumed as
  heading title text
- Style URL nodes with t.url tag

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 17:38:32 +00:00
claude d7ca7b194d feat(typst): parse show-rule bodies, let-value bindings, and content-block call args
Build and Deploy Verso / deploy (push) Successful in 14m13s
Three grammar gaps caused large blocks of code to be unhighlighted:

1. KeywordExpr now accepts an exclusive keywordBody: '#show sel: body' is
   parsed via ':', and '#let name = value' via '='.  callOrValue extends
   the subject to include CodeString so '#import "pkg"' highlights the path.

2. ContentBlock added to callSuffix so '#func("arg")[content]' and
   '#next-step("url")[...]' parse their trailing content block as code
   rather than falling back to markup.

3. Tokenizer: COLON added as a valid predecessor so identifiers (e.g. 'blue'
   in 'fill: blue') and keywords (e.g. 'set' in '#show link: set text(...)')
   are recognised after ':'.  EQUALS already added in the previous commit.
   The ident-chain backward scan now also skips whitespace before testing for
   '#' or ':', enabling 'text' in 'set text' to trace back to '#' through the
   keyword gap.  @precedence updated with CodeString, '[', ':' to resolve
   overlapping-token conflicts with MarkupContent in merged states.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 14:57:21 +00:00
claude 47cf84f20b fix(typst): highlight named arg keys after complex nested calls
Build and Deploy Verso / deploy (push) Successful in 13m48s
canShift(CodeIdent) returns false in LALR-merged states that arise after
reducing a complex first argument (e.g. figure(table(...), caption: ...)).
The previous guard `!couldBeIdent && !canShift(CodeArgKey)` then caused
an early exit before the character-level scan ran, silently dropping the
CodeArgKey token for any named arg key that follows such a reduction.

Fix: run the backward character scan first and derive `couldBeArgKey`
from the raw predecessor char ('(' or ',') rather than from canShift.
The early-exit now reads `!couldBeIdent && !couldBeArgKey`, so arg-key
positions always proceed to the full scan regardless of parser state.
Also stop calling canShift(CodeArgKey) entirely — it is unreliable here.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 13:57:10 +00:00
claude 2d3e64da92 themes: add tok-attributeName color to 15 themes that were missing it
Build and Deploy Verso / deploy (push) Successful in 14m6s
The Typst grammar now emits CodeArgKey (mapped to tok-attributeName) for
named argument keys like 'columns:', 'align:', 'caption:'.  15 of 41
editor themes had no .tok-attributeName rule, so those keys appeared in
the default text color (black) despite the correct CSS class being set.

Chose colors that complement each theme's existing palette:
light themes → warm dark-orange family (#994409 / #7B3814 / #735C0F)
dark themes  → each theme's accent color (gold, warm red, lavender…)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 13:28:09 +00:00
claude 2db6e63162 typst: fix CodeArgKey detection using character-level context
Build and Deploy Verso / deploy (push) Successful in 9m31s
canShift(CodeArgKey) was consistently returning false because LALR
state merging folds the codeArgItem start state into others where
CodeArgKey is not in the valid set.  As a result, named arg keys like
'columns:', 'align:', 'caption:' were always falling through to
CodeIdent (black) instead of CodeArgKey (tok-attributeName).

Fix: detect named arg key position by inspecting the nearest
non-whitespace predecessor character instead of trusting canShift.
prev == '(' or ',' means we are inside a call argument list — the only
positions where a named arg key can appear.  prev == last char of a
keyword word (e.g. 'w' of 'show') correctly excludes '#show heading:'
from being treated as a named arg.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 12:38:05 +00:00
claude f2b7034b51 typst: distinguish function calls from value identifiers; fix math outline
Build and Deploy Verso / deploy (push) Successful in 9m49s
Named value identifiers like 'left', 'center', 'right' were being
highlighted as tok-function (blue) because codeValue used CallExpr
(callSuffix*), which styled any identifier in value position as a
function.  Fix: add FuncExpr { CodeIdent callSuffix+ } (requires at
least one argument list or method suffix) and use it in codeValue
instead of CallExpr.  Plain identifiers in value position now fall
through to CodeIdent → tok-variableName.  CallExpr (callSuffix*) is
kept for codeExprBody and KeywordExpr where zero-suffix idents are
valid.

Tokenizer safety: only acceptToken(CodeIdent) when canShift(CodeIdent)
is true, preventing emission in LALR-merged states where neither
CodeArgKey nor CodeIdent is expected.

Outline: track '$'-parity across lines so that lines inside a display
math block (e.g. '= b+c$') are not incorrectly reported as headings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 12:04:33 +00:00
claude 4c6032bce0 typst: fix named-arg key highlighting and multi-line math
Build and Deploy Verso / deploy (push) Successful in 13m41s
Named arg keys (columns:, align:, caption:) were appearing in black
because LALR state merging broke the CodeArgs/CodeIdent path for
multi-line expressions.  Fix: emit a dedicated CodeArgKey token from
codeIdentTokenizer (forward-peek for ':' to pre-disambiguate), declare
it in the grammar's codeArgItem rule, and map it to t.attributeName in
styleTags — bypassing LALR lookahead entirely.

Multi-line display math ($ ...\n... $) was consuming the rest of the
document as orange text when contextual:true caused a backward scan to
find a previous closing '$' and falsely set isDisplay=true.  Fix:
revert mathContentTokenizer to contextual:false with '\n' stop (each
MathContent token covers one line), and change InlineMath to
MathContent* so @skip consumes the newlines between lines.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 11:25:31 +00:00
claude 2fdb155547 fix(typst): support multi-line display math ($...$)
Build and Deploy Verso / deploy (push) Successful in 9m44s
mathContentTokenizer now detects inline vs display math by scanning back
to the opening '$': if @skip consumed whitespace between '$' and the
content the tokenizer removes the newline stop (display math), otherwise
it keeps it (inline math).  contextual: true prevents the tokenizer from
firing outside InlineMath entirely, avoiding the orange-body-text
regression seen when this was previously attempted without the guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 10:52:29 +00:00
claude 75bc3bcc73 fix(typst): highlight idents after #keyword and wire tok-attributeName
Build and Deploy Verso / deploy (push) Successful in 9m34s
- codeIdentTokenizer: extend guard to scan back through the keyword word
  and accept when '#' immediately precedes it, so 'text' in '#set text(...)'
  and 'heading' in '#show heading:' are highlighted as function names
- classHighlighter: add tags.attributeName → tok-attributeName mapping;
  all 26 themes already define .tok-attributeName colours but the tag was
  never mapped to the class, leaving named arg keys (columns:, caption:)
  completely unstyled

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 09:18:31 +00:00
claude 1b773fdda0 fix(typst): add newlines to @skip so multi-line code args parse cleanly
Build and Deploy Verso / deploy (push) Successful in 9m31s
'\n' inside CodeArgs was an invalid token, triggering Lezer error recovery
and resetting parser state before codeIdentTokenizer could fire.  Heading
detection is unaffected — headingTokenizer uses raw input.peek(-1) char
reads which see the '\n' byte regardless of what @skip consumes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 08:47:47 +00:00
claude 019b4041a8 fix(typst): highlight function names across newlines in code args
Build and Deploy Verso / deploy (push) Successful in 9m35s
codeIdentTokenizer's backward scan stopped at \n, so identifiers at the
start of a new indented line inside multi-line arg lists (e.g. image(),
table() inside #figure(...)) never matched the '(' guard and stayed black.
Extend the whitespace skip to also cross newlines.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 07:28:43 +00:00
claude 4aca4aaac6 fix: make CodeIdent external and replace strongItem*/emphItem* with flat body tokens
Build and Deploy Verso / deploy (push) Successful in 13m36s
Two LALR state-merging bugs prevented Strong/Emphasis nodes from ever being
produced (confirmed: tok-strong/tok-emphasis count = 0 in browser diagnostic).

Bug 1 — _italic_ consumed as CodeIdent:
  CodeIdent was a @tokens rule with identHead = [A-Za-z_], so '_italic_' (the
  entire string including both underscores) matched as one CodeIdent token.
  LALR merging caused CodeIdent to be in item*'s valid set, and CodeIdent >
  "_" in @precedence, so the parser never opened Emphasis.

  Fix: move CodeIdent to an external tokenizer (codeIdentTokenizer) with a
  character-level guard — only fires when the preceding non-whitespace char
  is one of '#', '.', '(', ',' (genuine code-context positions).  In body
  text where peek-back finds a newline, space, or markup delimiter, the
  tokenizer returns without emitting, letting '"_"' open Emphasis correctly.

Bug 2 — StrongText never produced inside Strong:
  The strongItem* / emphItem* loops merged with item* states via Lezer's
  aggressive LALR merging.  In the merged state MarkupContent was in the
  valid set (from the item* side) and MarkupContent > StrongText in
  @precedence, so MarkupContent was always produced — not a valid strongItem,
  leading to error recovery with no StrongText in the tree.

  Fix: replace the recursive strongItem* / emphItem* loops with flat external
  tokens StrongBody / EmphBody (contextual: true).  These fire only inside
  Strong → "*" . StrongBody? "*" and Emphasis → "_" . EmphBody? "_", states
  specific enough that canShift is reliable.  They read everything up to the
  closing delimiter or newline in one token, bypassing the LALR merging
  entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 22:15:22 +00:00
claude f9d46aabeb fix: revert mathContentTokenizer regression (contextual + no-newline-stop)
Build and Deploy Verso / deploy (push) Successful in 9m43s
The previous change switched mathContentTokenizer to contextual:true with no
newline stop, intending to support multi-line Typst block math.  However,
LALR state merging causes canShift(MathContent) to spuriously return true in
body-text positions (e.g. after a RawInline backtick close), so the tokenizer
consumed everything until the next '$' — turning a full paragraph orange.

Revert to contextual:false with newline stop.  This correctly handles both
inline ($x^2$) and single-line block ($ integral ... $) math.  Multi-line
block math ($ formula\n continuation $) remains a separate issue for later.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 21:50:15 +00:00
claude 54ab282efc Fix Typst: support multi-line math blocks ($ ... multi-line ... $)
Build and Deploy Verso / deploy (push) Successful in 14m1s
mathContentTokenizer was stopping at newlines, causing a parse error for
Typst block math that spans multiple lines.  The parser then entered a bad
state that cascaded: the stale error-recovery left the item* parser in a
degraded mode, causing body text below the math block to be highlighted as
t.string (orange).

Fix: switch to contextual: true (only fires inside InlineMath where
MathContent is actually expected) and remove the newline restriction so
the tokenizer reads until the closing '$' regardless of line boundaries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 21:03:17 +00:00
claude f976c5ba92 Fix Typst: bold/italic rendering and keyword false-highlights in body text
Build and Deploy Verso / deploy (push) Successful in 14m15s
Add .tok-strong and .tok-emphasis CSS to the static editor theme so
bold/italic markup actually renders visually.

Move CodeKeyword from @tokens to an external tokenizer (codeKeywordTokenizer)
with a peek(-1)==='#' guard. LALR state-merging causes code-mode states to be
reachable in markup positions, making common English words like "in", "for",
"while", "return" trigger CodeKeyword highlighting in body text. The '#' guard
ensures keywords only fire immediately after the '#' sigil, never in prose.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:20:02 +00:00
claude f5a94c0ced fix(typst): guard RawBlockBody against LALR-merged body-text states
Build and Deploy Verso / deploy (push) Successful in 10m0s
canShift(RawBlockBody) returns true in states LALR-merged with the
post-RawBlockOpen state, causing the tokenizer to consume all remaining
body text as one giant RawBlockBody. Add a backward character scan:
require newline immediately before input.pos, then walk back past any
lang tag (A-Za-z0-9) and verify three backticks precede it. Body-text
positions never have backtick-backtick-backtick there, so the guard
rejects them.

This was the root cause of everything after the first heading being
black: RawBlockBody swallowed the entire document from the user-name
line onward, making headings, bold, italic and math invisible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:44:05 +00:00
claude 11d852fe18 Revert "chore: add browser diagnostic script for Typst highlighting"
Build and Deploy Verso / deploy (push) Successful in 1m15s
This reverts commit 8c9088d054.
2026-06-08 19:29:46 +00:00
claude cc0d97903c Revert "chore: fix CodeMirror view accessor in diagnostic script"
This reverts commit 5850ffcad7.
2026-06-08 19:29:46 +00:00
claude 5850ffcad7 chore: fix CodeMirror view accessor in diagnostic script
Build and Deploy Verso / deploy (push) Successful in 1m4s
2026-06-08 19:27:06 +00:00
claude 8c9088d054 chore: add browser diagnostic script for Typst highlighting
Build and Deploy Verso / deploy (push) Successful in 1m12s
2026-06-08 19:21:30 +00:00
claude 974a9c4fb3 fix(typst): restore HeadingMark+HeadingTitle with character-level bleed guard
Build and Deploy Verso / deploy (push) Successful in 10m0s
The single-HeadingLine token approach caused everything after the first
heading to be unparsed. Reverting to the two-token structure but adding a
backward character scan in headingTitleTokenizer: after canShift(), walk
backward past whitespace and require '=' immediately before the current
position. Body-text positions in LALR-merged states will have a letter or
closing bracket there instead, so the tokenizer returns without accepting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 18:17:17 +00:00
claude 34025dc084 fix(typst): use HeadingLine single token to fix inline element highlighting
Build and Deploy Verso / deploy (push) Successful in 9m35s
The two-token approach (HeadingMark + HeadingTitle) caused LALR state
merging: the parser state waiting for HeadingTitle after HeadingMark was
merged into body-text item* states. In those merged states the
headingTitleTokenizer fired for every paragraph line, swallowing bold,
italic, math and inline function tokens — leaving body text black.

Fix: collapse the heading into a single HeadingLine external token that
covers the entire heading line (= prefix + title). A single-token Heading
rule leaves no post-token parser state waiting for a second token, so no
LALR merging can occur. The ViewPlugin and all HeadingMark/HeadingTitle
infrastructure are removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 16:55:09 +00:00
claude 0099672015 fix(typst): restore HeadingTitle token to fix broken syntax highlighting
Build and Deploy Verso / deploy (push) Successful in 10m12s
Removing HeadingTitle from the grammar left HeadingTitle? undeclared,
causing the Lezer grammar compiler to fail and producing no parser
output — hence everything rendered as unstyled black text.

Dual approach to prevent heading style bleed:
- HeadingTitle exists in grammar with contextual: true + canShift guard
  (prevents it from matching in body-text LALR states)
- HeadingTitle is intentionally absent from styleTags so even spurious
  matches cannot apply heading colour to body text
- ViewPlugin styles heading titles by finding HeadingMark nodes and
  extending tok-heading decoration to end-of-line

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 15:38:38 +00:00
claude a5ca432396 Fix heading bleeding and smooth_pdf_transition translation
Build and Deploy Verso / deploy (push) Successful in 13m35s
Two fixes:

1. Heading style bleeding (Typst): the HeadingTitle external token approach
   was unreliable — even with contextual:true and canShift(), body text was
   being styled as headings. Remove HeadingTitle from the grammar entirely.
   Instead, a ViewPlugin (headingLinePlugin in languages/typst/index.ts)
   walks the syntax tree, finds HeadingMark nodes, and decorates the rest of
   the line with tok-heading class + bold. This is unconditionally correct
   because it is based on the syntax tree rather than the LR tokenizer state.

2. smooth_pdf_transition raw key shown in all locales: the key was in the
   JSON locale files but missing from extracted-translations.json, which is
   the allowlist the webpack translation loader uses to decide what to bundle.
   Add it there so all locales (including fr, es, de already added) resolve
   to their translated strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 14:22:16 +00:00
claude f1abcaa4ce Hide LaTeX-only compile options for Typst/Quarto projects; add smooth_pdf_transition translations
Build and Deploy Verso / deploy (push) Successful in 9m35s
Draft compile mode and stop-on-first-error are LaTeX-only features not
supported by TypstRunner or QuartoRunner. Hide both sections from the
recompile dropdown for non-LaTeX projects. Also detect Quarto root files
(.qmd/.md/.Rmd) alongside Typst (.typ) to correctly set isLatexProject.

Add missing smooth_pdf_transition translations for French, Spanish, and
German (the English key already existed).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 13:45:02 +00:00
claude 1c323351a2 fix: parse Quarto schema YAML errors and stop heading style bleeding
Build and Deploy Verso / deploy (push) Successful in 9m46s
Two unrelated fixes:

1. quarto-log-parser: handle the two-line Quarto schema-validation
   error format:
     ERROR: In file main.qmd
     (line 6, columns 24--27) Field "section-numbering" has value …
   Previously neither the file name nor the line number were extracted,
   so the error appeared without a red highlight. Now the first line
   stores the filename in pendingLocation and the second line creates
   the log entry with the correct file and line so the editor can jump
   to and highlight it.

2. headingTitleTokenizer: change contextual: false → contextual: true
   and guard with stack.canShift(HeadingTitle). With contextual: false
   Lezer calls the tokenizer speculatively at positions beyond the strict
   post-HeadingMark state; in some LALR-merged states the resulting token
   was accepted for body-text lines, making them render as bold-blue
   heading text. The contextual guard ensures the tokenizer only fires
   in the one state where HeadingTitle is legitimately valid.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 13:31:26 +00:00
claude 55e8208892 chore: add TODO.md with next-alpha Typst experience ideas
Build and Deploy Verso / deploy (push) Successful in 13m41s
Two ideas borrowed from the Collabst project (a Typst-native
collaborative editor): typst.ts WASM in-browser preview and Tinymist
LSP integration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 12:26:07 +00:00
claude b8543c8bb9 fix: capture typst diagnostics emitted after the status line
Build and Deploy Verso / deploy (push) Has been cancelled
typst watch outputs the "[HH:MM:SS] compiled with errors" status line
FIRST, then the full diagnostic output (file:line:col, source snippets,
hints) AFTERWARDS. The previous code resolved the pending compile
promise as soon as COMPILE_DONE_RE fired, discarding all post-status
diagnostic lines. Those lines then got cleared by the next cycle's
COMPILE_START_RE, so output.log only ever contained the bare status
line — explaining the "zero verbosity" symptom.

Fix: introduce a two-phase buffering model. When COMPILE_DONE_RE fires,
enter "post-done" phase (storing doneResult) and keep accumulating into
currentLines. _finalizeCompile() is called either when the next
COMPILE_START_RE arrives (zero added latency) or after FLUSH_DELAY_MS
(150 ms fallback for the last compile). It concatenates pre-done and
post-done lines before resolving, so output.log now contains the full
diagnostic output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 12:25:43 +00:00
claude 7e6c8c30cc fix: cache typst compile result to eliminate race-condition failures
Build and Deploy Verso / deploy (push) Successful in 14m6s
When typst watch detects a file change and compiles before the CLSI
resolver is registered (ResourceWriter writes files → typst compiles →
runTypst is called), _resolveAllPending was discarding the result
because pendingResolvers was empty. This caused two symptoms:

1. output.log only contained "compiled with errors" (no diagnostics)
   because the result carrying the full stdout was thrown away.

2. Every other manual compile failed with "compilation already gone"
   because the missed result caused a timeout, which killed the watcher
   and triggered a watcher restart cycle (success → miss → timeout →
   kill → restart → success → miss → ...).

Fix: when _resolveAllPending fires with no pending resolvers, store the
result in entry.pendingResult. _waitForNextCompile checks this field
first and resolves immediately if a cached result is present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 11:48:46 +00:00
claude e0c717c131 fix: suppress interim compile errors while typing, show Typst error logs, dark-mode footer
Build and Deploy Verso / deploy (push) Successful in 1m43s
- local-compile-context: suppress failure/exited error state when
  changedAt > 0 (another compile is already queued), preventing the UI
  from flashing an error banner mid-typing that resolves moments later

- TypstRunner + CompileController: detect "compiled with errors" from
  typst watch and non-zero exit from typst compile, and signal
  status:'failure' to the frontend so the log panel opens automatically
  with the parsed error details (previously always returned 'success')

- footer.scss: add dark-mode overrides for footer.site-footer so the
  thin footer on project/marketing pages uses bg-dark-primary and
  content-primary-dark text in dark theme instead of hardcoded light bg

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 10:19:36 +00:00
claude 7a5218d472 typst: fix CodeIdent vs "_" token overlap after #keyword CallExpr?
Build and Deploy Verso / deploy (push) Failing after 8m41s
KeywordExpr { CodeKeyword CallExpr? } merges the post-keyword LR state
with document-level markup states, where "_" opens Emphasis.  CodeIdent
starts with identHead which includes "_", so the two tokens overlap.

Adding "_" after CodeIdent in @precedence resolves the conflict: CodeIdent
wins in the merged state (correct for '#set _name(...)'), and in pure markup
states CodeIdent is not in the valid set so "_" still opens Emphasis.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 09:16:35 +00:00
claude b16b096744 projection: visit overlay/mounted subtrees during tree iteration
Build and Deploy Verso / deploy (push) Successful in 22m39s
yamlFrontmatter() embeds the Markdown content as an overlay on the top-level
YAML-frontmatter tree.  The previous mode (IgnoreMounts | IgnoreOverlays)
skipped that overlay entirely, so ATXHeading nodes were never visited and the
Quarto (.qmd) file outline was always empty.

Dropping the mode flag lets the iterator descend into overlay and mounted
subtrees.  This is safe because every enterNode function already filters by
node name — visiting extra nodes from foreign-language mounts is a no-op.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 09:03:43 +00:00
claude e9cc63a261 typst: highlight function name after #set / #show keywords
Build and Deploy Verso / deploy (push) Has been cancelled
KeywordExpr now optionally includes a CallExpr, so '#set text(size: 12pt)'
parses 'text' as a CallExpr/CodeIdent and gets the function-name highlight
colour.  The optional CallExpr only shifts when the lookahead is CodeIdent,
so there is no shift/reduce conflict with other items.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 08:40:28 +00:00
claude 07c72cf7e5 typst: stop heading title at // or /* comment markers
Build and Deploy Verso / deploy (push) Successful in 9m24s
headingTitleTokenizer now stops reading when it encounters '//' or '/*',
so '= Heading // note' correctly produces a HeadingTitle token for 'Heading'
and a LineComment for the rest of the line.  Without this, the comment was
consumed into HeadingTitle, getting heading highlight and appearing verbatim
in the file outline.

Also strip trailing line comments from heading titles in the regex-based
document outline scanner, which reads raw text independently of the tree.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 08:27:02 +00:00
claude 4f98abbc5d classHighlighter: map function(variableName) to tok-function
Build and Deploy Verso / deploy (push) Has been cancelled
All cm6 themes define .tok-function but the classHighlighter had no entry
for tags.function(tags.variableName), so function-name tokens fell back to
tok-variableName (which themes leave unstyled).  This affected Typst function
calls (#func(...)) and would affect any future language that tags function
names with t.function(t.variableName).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 08:21:16 +00:00
claude 1dcd6e24f4 lezer-typst: convert LineCommentContent and MathContent to external tokenizers
Build and Deploy Verso / deploy (push) Successful in 10m10s
Both tokens are "read until delimiter" catchalls that match almost every
non-newline character, causing buildTokenGroups conflicts with every other
literal token in LALR-merged states.  Moving them to ExternalTokenizer (the
same pattern already used for HeadingTitle, RawBlockBody, etc.) makes them
context-isolated: the LR state machine only calls them when those tokens are
actually valid, so they never participate in the static token-group overlap
check.

Also exclude '<' from StrongText/EmphText so Label ('<' LabelName '>') is
recognised inside strong/emphasis spans rather than being consumed as plain
text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 07:56:20 +00:00
claude e21f7cc0d5 fix(typst): resolve all overlapping-token errors via @precedence
Build and Deploy Verso / deploy (push) Has been cancelled
Lezer's buildTokenGroups rejects grammars with ambiguous token sets.
Eight overlaps existed:

  EscapeChar vs spaces       — EscapeChar { _ } matches \t; after '\'
                               it must win over the @skip spaces token.
  "(" / "." vs text tokens   — in the LALR-merged state after #CodeIdent,
                               callSuffix delimiters must beat
                               MarkupContent / StrongText / EmphText.
  "]" vs LineCommentContent  — inside #[...], the ContentBlock closer
                               must win even if it follows "//".

One extended @precedence declaration resolves all eight.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 07:41:03 +00:00
claude e4f5385e35 fix(typst): fix zero-length token error for LineCommentContent
Build and Deploy Verso / deploy (push) Has been cancelled
LineCommentContent { ![\n]* } matches the empty string, which Lezer
rejects as a zero-length token (infinite-loop risk). Change to ![\n]+
and mark it optional in the LineComment rule so empty // comments parse.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 06:54:56 +00:00
claude 2f3e3e7363 fix(typst): make HeadingTitle an external token to end LALR conflicts
Build and Deploy Verso / deploy (push) Has been cancelled
Any item shared between headingTitleItem and document-level item causes
a shift/reduce conflict: the LALR automaton merges the two contexts and
makes the shared token ambiguous. The only structural fix is to make
HeadingTitle a terminal (external tokenizer) that reads greedily to EOL,
giving the LR state machine a context-isolated token that can never
collide with document-level item tokens.

Removes headingTitleItem sub-rule, HeadingText token, and updates
styleTags to match HeadingTitle directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 21:14:18 +00:00
claude 94e8ff3503 fix(typst): eliminate LALR(1) conflict on heading title items
Build and Deploy Verso / deploy (push) Failing after 3h10m10s
Removing Strong and Emphasis from headingTitleItem eliminates the
conflict: both appear in document-level item, causing the LR automaton
to merge heading-title states with document-item states and make "*"
ambiguous (Strong opener vs. end of heading).

HeadingText is widened to ![\n$#`<@\\]+ so "*" and "_" inside headings
are consumed as plain text rather than producing error nodes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:58:05 +00:00
claude 26da1f6205 fix(editor): resolve HeadingTitle shift/reduce conflict in Typst grammar
Build and Deploy Verso / deploy (push) Has been cancelled
headingTitleItem* allowed an empty HeadingTitle, causing a shift/reduce
conflict: after HeadingMark, seeing "*" the LR parser couldn't decide
whether to shift it as a Strong inside the heading or reduce HeadingTitle
to empty and treat "*" as a document-level item.

Changing to headingTitleItem+ forces HeadingTitle to be non-empty, so
"*" after HeadingMark must be inside the heading. Empty headings are
handled by Lezer's error recovery.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:35:42 +00:00
claude 5287ea6f00 fix(editor): break Strong/Emphasis mutual recursion in Typst grammar
Build and Deploy Verso / deploy (push) Has been cancelled
Strong{strongItem{Emphasis}} and Emphasis{emphItem{Strong}} created a
mutual-recursion cycle that caused Lezer's LR automaton builder to
produce exponentially many states and crash.

Remove each construct from the other's item list. StrongText already
includes '_' and EmphText already includes '*', so nested delimiters
render as plain text inside the opposite construct rather than errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:14:33 +00:00
claude 045d458875 feat(editor): native Lezer grammar for Typst syntax highlighting
Build and Deploy Verso / deploy (push) Has been cancelled
Replace the StreamLanguage tokenizer with a full LR grammar compiled by
@lezer/generator, giving Typst the same parse-tree infrastructure that
LaTeX and BibTeX already use.

Grammar features:
- Headings (=, ==, …) via SOL-detecting external tokenizer
- Code expressions (#keyword, #func(args), #ident.method, #{…}, #[…])
- Named argument highlighting (key: value in function calls)
- Inline and display math ($…$)
- Strong (*…*) and emphasis (_…_) with bold/italic formatting
- Raw blocks (```lang…```) and inline raw (`…`)
- Nested block comments (/* /* */ */) via depth-tracking external tokenizer
- Labels (<name>) and references (@name)
- Backslash escapes

Infrastructure changes:
- lezer-typst/typst.grammar — new Lezer grammar
- lezer-typst/tokens.mjs — external tokenizers for context-sensitive lexing
- scripts/lezer-latex/generate.mjs — Typst added to grammars array so the
  existing lezer-latex:generate script (and Dockerfile step) compile it
- .gitignore — generated typst.mjs / typst.terms.mjs excluded from git

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 19:49:17 +00:00
claude 2c0f387cef feat(editor): use @codemirror/lang-yaml for Quarto YAML frontmatter
Build and Deploy Verso / deploy (push) Successful in 12m20s
Replace the custom regex-based ViewPlugin with the official
@codemirror/lang-yaml package. yamlFrontmatter({ content: mdLS })
wraps the Markdown language with a mixed parser: the leading ---/---
block is handed to the full Lezer YAML parser (proper key/value/scalar/
anchor/alias highlighting), while the document body continues to use
the Markdown parser. The manual Frontmatter extension import is also
removed since yamlFrontmatter handles frontmatter recognition itself.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 18:30:35 +00:00
claude f9788a1c69 feat(editor): improve syntax highlighting for Typst and Quarto documents
Build and Deploy Verso / deploy (push) Successful in 10m50s
Typst: heading tokenizer now colors the entire heading line (not just the
'=' prefix), and bold/italic markers (*/_) map to strong/emphasis tags
rather than the generic operator tag. A typstHighlightStyle applies
bold/italic formatting even when the active theme lacks .tok-heading.

Quarto: enable @lezer/markdown's Frontmatter extension so the YAML header
is no longer mis-parsed as Setext headings. A new ViewPlugin decorates
frontmatter lines with type-appropriate CSS classes: keys (tok-typeName),
string/bool/number values, comments, and the --- delimiter markers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 17:42:51 +00:00
claude 489bdb01ec feat(pdf): dark mode for Quarto RevealJS HTML output
Build and Deploy Verso / deploy (push) Successful in 10m36s
Apply the same CSS inversion filter to the HTML iframe as is already
applied to pdfjs PDF pages, so Quarto RevealJS presentations respect
the dark mode toggle. Also show the theme button for HTML outputs and
relax the darkModePdf condition to activate for iframes regardless of
the pdfViewer setting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 17:18:29 +00:00
claude 0b8897540d Fix Quarto RevealJS media missing on second compile
Build and Deploy Verso / deploy (push) Successful in 11m49s
When output.html exists, findOutputFiles includes project media files
(images, videos) in outputFiles via the MEDIA_REGEX exception so they
get served from the cache.  _removeExtraneousFiles then treated them
as extraneous and deleted them.  On the next incremental compile,
unchanged binary files are not re-synced, so the files were gone when
Quarto ran and when _appendMissingResourceWarnings checked for them.

Fix: skip deletion for any file that is a project input resource.
Those files appear in outputFiles to be served, not cleaned up.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 15:30:08 +00:00
claude 2ead377ebc Fix stale error lines bleeding into next Typst compile log
Build and Deploy Verso / deploy (push) Failing after 36m24s
When typst watch doesn't emit "compiled with errors" after a failed
compile, currentLines accumulates indefinitely. The next successful
compile then flushes the buffer including the stale error from the
prior cycle. Reset currentLines at the start of each compile cycle
("[HH:MM:SS] compiling ...") so each log only contains output from
one compile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 15:10:59 +00:00
claude 5a85e1b9d8 Add smooth PDF transition toggle to compile settings
Build and Deploy Verso / deploy (push) Successful in 11m53s
Per-project-type setting: Typst defaults to on, LaTeX defaults to off.
Toggle appears in the compile dropdown under "Smooth PDF transition".
The enableTransition flag is read via a ref so toggling does not
reload the current PDF.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 14:19:37 +00:00
claude 165219dcb1 fix(autocompile): prevent compile chaining — wait for previous compile before starting next
Build and Deploy Verso / deploy (push) Successful in 11m38s
The auto-compile effect was calling debouncedAutoCompile() on every changedAt
update (every keystroke), including while a compile was already running.  With
a 1000ms maxWait the debounce fired every second even mid-compile, chaining
compiles back-to-back and making the user wait for all of them to drain.

Fix: add `compiling` to the effect's dependency array.
- While compiling: the effect cancels the debounce immediately, preventing
  any new compile from being queued.
- When compile finishes (compiling → false): the effect re-runs; if changedAt
  is still > 0 (changes were made during the compile), it re-arms the debounce
  exactly once.  One follow-up compile, then idle.

Also remove the debouncedAutoCompile() re-queue from compiler.ts's
wasCompiling guard — the effect now owns that responsibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 13:38:43 +00:00
claude 71755e5cee fix(pdf): replace document.startViewTransition with non-blocking canvas fade
Build and Deploy Verso / deploy (push) Successful in 14m22s
document.startViewTransition with an async callback places a ::view-transition
overlay on top of the entire page, intercepting pointer events for the duration
of the callback (up to the 1s safety timeout + 250ms animation).  With rapid
auto-compiles this created interface freezes and overlapping transitions that
could leave the visual lock in a broken state, causing 'stuck on compiling'.

Replace with a canvas snapshot overlay + CSS opacity fade-out:
- pointer-events:none so the overlay never blocks input
- snapshot covers the canvas-clear from setDocument() (no white flash)
- on pagerendered: opacity transitions to 0 over 250ms, then overlay removed
- gives the same smooth visual crossfade, reliably, in all browsers

Chrome 126+ retains the element-level startViewTransition path which is
scoped to the PDF container and does not affect the rest of the page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 13:09:18 +00:00
claude 453439e611 fix(pdf): suppress root view-transition animation to isolate fade to PDF pane only
Build and Deploy Verso / deploy (push) Successful in 11m47s
document.startViewTransition generates both a named pseudo-element for the PDF
container (ol-pdf-viewer) and a root-level pseudo-element that covers the entire
page, causing the editor to fade along with the PDF.

Inject a temporary <style> that sets animation:none on the root pseudo-elements
before starting the transition, then remove it in transition.finished.  Only the
named PDF container crossfades; the editor is unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 12:39:01 +00:00
claude d895e14e48 feat(pdf): restore smooth crossfade for Chrome 111+ using document.startViewTransition
Build and Deploy Verso / deploy (push) Successful in 11m24s
The original code used container.startViewTransition(setDocument) with a
synchronous callback, giving a 250ms CSS crossfade that looked smooth when
the PDF happened to re-render before the animation ended — but was a race.

Now there are three tiers:
- Chrome 126+: element-level startViewTransition, async, waits for pagerendered
- Chrome 111+ (Brave 138, Edge 111+): document-level startViewTransition with
  view-transition-name scoped to the PDF container, same async pattern
- Firefox / Safari / older Chromium: canvas snapshot overlay (no animation,
  but seamless — introduced in build #108)

The document-level path restores the smooth fade the user saw on Edge build
#93, now guaranteed to crossfade old→new rather than old→blank.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 12:22:46 +00:00
claude 4410a83146 fix: eliminate too-recently-compiled error and PDF flicker on fast Typst compiles
Build and Deploy Verso / deploy (push) Successful in 11m25s
Rate limit: auto-compile requests already have a client-side debounce; skip
the 1-second server-side recently-compiled gate for them to avoid spurious
'too-recently-compiled' rejections that were blocking ~1/3 of Typst compiles.

PDF flicker: add _snapshotCanvases() fallback for browsers without element-level
View Transitions (Chrome <126, Firefox, Safari).  Before setDocument() clears the
canvases it copies each rendered page to a positioned overlay; the overlay is
removed once the first page of the new document fires pagerendered, giving a
seamless old→new swap in all browsers.  Chrome 126+ continues to use the
startViewTransition async callback path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 12:00:01 +00:00
claude db162e54af fix(typst): correct auto-compile default and debounce detection for Typst projects
Build and Deploy Verso / deploy (push) Successful in 11m37s
The previous implementation used useState() to detect the project type, but the
file tree is loaded asynchronously after the WebSocket joinProject event, so
pathInFolder() always returns null on the initial render.

Use useEffect() instead — it re-runs when getRootDocInfo's reference changes
(i.e. when the file tree populates), correctly detecting .typ root docs.
Also adds updateAutoCompileDebounce() to DocumentCompiler so the tight
debounce can be applied at that point.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 11:37:28 +00:00
claude 228ad00075 feat(typst): auto-compile on by default with fast debounce + smoother PDF transitions
Build and Deploy Verso / deploy (push) Successful in 14m23s
- Typst projects default autocompile to enabled (300ms debounce / 1s max-wait
  instead of 2.5s/5s), so the PDF refreshes nearly as the user types.
- Make startViewTransition wait for the first page to render before completing
  the crossfade, eliminating the old-PDF→blank flash on Chrome 126+.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 10:40:10 +00:00
claude 7eaeaedcd8 Implement persistent typst watch for incremental compilation
Build and Deploy Verso / deploy (push) Successful in 59m27s
Instead of cold-starting 'typst compile' on every request, TypstRunner
now maintains a long-lived 'typst watch' process per project. Subsequent
compiles reuse the warm process, which caches fonts, packages, and the
compiled AST via Typst's comemo framework — dramatically faster.

Architecture:
- WatchTable: maps compileName → live watcher process + state
- _startWatcher: spawns 'typst watch input.typ output.pdf', registers
  stdout/close handlers, then immediately awaits the first compile result.
  The resolver is pushed to pendingResolvers synchronously inside the
  Promise constructor before any I/O event can fire — eliminating the
  race between file-write detection and resolver registration.
- _onWatcherData: parses stdout line-by-line, resolves pending callers
  on "compiled successfully/with warnings/with errors" (the three terminal
  lines typst watch emits at the end of each compile cycle).
- Graceful restart: watcher is restarted after MAX_COMPILES_BEFORE_RESTART
  (1000) cycles to stay clear of Typst's ~65k FileId limit, or immediately
  if the "ran out of file ids" message is detected in stdout.
- killTypst: tears down both the watcher and any cold-start fallback job;
  called by stopCompile (user-initiated) and clearProject/clearProjectWithListing
  (before compile-dir deletion).
- Docker fallback: Settings.clsi.dockerRunner=true falls back to the
  original cold-start 'typst compile' path unchanged.
- process.on('exit') kills all watcher process groups on CLSI shutdown.

CompileManager: call TypstRunner.promises.killTypst before deleting the
compile directory in both clearProject and clearProjectWithListing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 09:09:33 +00:00
claude 54c510c818 Revert Typst SyncTeX attempt; clean up diagnostic logging
Build and Deploy Verso / deploy (push) Has been cancelled
Typst has no --synctex CLI option (open feature request #289 since 2023).
Revert the frontend guard back to LaTeX-only and remove --synctex from
the Typst compile command. Also remove the temporary logger.warn calls
added for diagnosing the LaTeX synctex issue (now resolved).

The official Typst binary installation in Dockerfile-base is kept as it
is cleaner than using Quarto's modified fork for .typ compilation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 08:43:18 +00:00
claude 5796c0157c Install official Typst binary and use it for .typ compilation
Build and Deploy Verso / deploy (push) Has been cancelled
Quarto bundles a modified Typst fork that lacks --synctex, making
bidirectional sync impossible. Install the official Typst binary
(v0.13.1) from upstream and use it in TypstRunner instead.

This also means .typ projects now use the unmodified Typst compiler,
which is correct since TypstRunner handles plain .typ files (not .qmd).
QuartoRunner continues to use Quarto's bundled Typst internally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 08:40:48 +00:00
claude 3f68c147a4 Call Typst binary directly for compile and SyncTeX support
Build and Deploy Verso / deploy (push) Successful in 13m8s
Instead of going through 'quarto typst compile' (which intercepts
--synctex before it reaches Typst), call the Typst binary bundled in
the Quarto .deb directly at /opt/quarto/bin/tools/x86_64/typst.

This allows passing --synctex output.synctex.gz to generate the SyncTeX
file for bidirectional editor↔PDF sync.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 08:24:20 +00:00
claude 0780963bc7 Fix --synctex argument order for Typst compile
Build and Deploy Verso / deploy (push) Successful in 11m33s
Typst's CLI requires options before positional arguments (INPUT OUTPUT).
Placing --synctex after output.pdf caused it to be treated as an extra
positional arg and rejected with 'unexpected argument'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 08:09:52 +00:00
claude 43a622cd71 Add SyncTeX support for Typst projects
Build and Deploy Verso / deploy (push) Successful in 11m58s
- TypstRunner: add --synctex output.synctex.gz to quarto typst compile,
  generating a synctex file alongside the PDF (requires Typst 0.11+,
  bundled in Quarto 1.5+).
- use-synctex: extend the root-doc guard from LaTeX-only to also cover
  .typ files, enabling the Show in PDF / Show in code buttons for Typst.

The rest of the sync infrastructure (OutputCacheManager, synctex binary,
SynctexOutputParser, CLSI routes) is already format-agnostic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 21:52:05 +00:00
claude 9079b545f7 Switch TeX Live from scheme-basic to scheme-full
Build and Deploy Verso / deploy (push) Successful in 50m16s
Replaces the minimal scheme-basic install (plus explicit latexmk,
texcount, synctex additions) with scheme-full, giving users access
to the complete LaTeX package ecosystem without manual tlmgr installs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 20:55:06 +00:00
claude eb45ececf0 Install synctex binary via tlmgr for SyncTeX support
Build and Deploy Verso / deploy (push) Has been cancelled
The synctex binary was not included in scheme-basic and was not
explicitly installed, causing `spawn synctex ENOENT` on every
sync request. Add it alongside latexmk and texcount.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 20:53:28 +00:00
claude e6add1e6f0 Add diagnostic logging to synctex to identify failure cause
Build and Deploy Verso / deploy (push) Successful in 9m42s
Logs: request params, directory used, whether output.synctex.gz
is found, and the actual synctex binary output or error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 16:21:35 +00:00
claude 170818e6fc fix(synctex): gate sync buttons to LaTeX-only projects
Build and Deploy Verso / deploy (push) Successful in 11m0s
Verso added 'qmd' and 'typ' to validRootDocExtensions, which caused
isValidTeXFile() to return true for Typst/Quarto files — enabling
SyncTeX UI controls for projects that never produce output.synctex.gz.

Replace the open-doc extension check in canSyncToPdf with a
LaTeX-only regex on the project root document path (tex|ltx|Rtex|Rnw),
and add the same guard in _syncToCode so PDF-click sync never fires
an API request for non-LaTeX projects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 14:50:02 +00:00
claude 9ea904f78f Merge upstream Overleaf up to PR #34297 (68 commits)
Build and Deploy Verso / deploy (push) Successful in 11m30s
Conflicts resolved:
- fat-footer-website-redesign.pug: keep Verso footer (discard Overleaf marketing footer)
- MaterialSymbolsRoundedUnfilledPartialSlice.woff2: regenerated from merged
  unfilled-symbols.mjs (preserves Verso's deployed_code + adds upstream's spellcheck)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 13:39:32 +00:00
roo hutton 757735b075 Merge pull request #34297 from overleaf/rh-prev-plan-type-cancel
Update previous_plan_type on subscription expiry

GitOrigin-RevId: 19381e5516fdbfd2650a9a5b94b61791e0da909f
2026-06-05 08:07:27 +00:00
Mathias Jakobsen fa36cd508b Merge pull request #34310 from overleaf/mj-handle-lazy-errors-for-search-and-share
[web] Handle errors while loading full project search and share modal

GitOrigin-RevId: 29d863324a54fa872022002f612498335f88f377
2026-06-05 08:07:10 +00:00
Mathias Jakobsen b7735d402d Merge pull request #34195 from overleaf/mj-command-palette-menu-labels
[web] Add menu labels to commands, and add more commands to command palette

GitOrigin-RevId: 21e17142bb3112b5fdcda85a472122b011979f49
2026-06-05 08:07:06 +00:00
Mathias Jakobsen cabe0046c5 Merge pull request #34102 from overleaf/mj-document-import-errors
[web] Expose pandoc errors in import

GitOrigin-RevId: 55f89b91a52099a99a5d955bc05f3657b87b2cdc
2026-06-05 08:07:02 +00:00
Anna Claire Fields 97247b8ea5 [PnP migration] Remove mock-fs dependency (#33835)
GitOrigin-RevId: ff8df32d85b2ecd2837c9eee6d6d2b3b95285239
2026-06-05 08:06:54 +00:00
Anna Claire Fields 3fcd133198 [patch] update sandboxed-module for Yarn PnP compatibility and add mongodb-legacy type definitions (#33983)
GitOrigin-RevId: 8f1e9a4e4b4b5fbf3a770951a070b5a259abdcee
2026-06-05 08:06:50 +00:00
Anna Claire Fields 44dee7592a use require.resolve for mocha reporter paths (#34235)
GitOrigin-RevId: af607dfdeac8f91f63db294a964ade7622225932
2026-06-05 08:06:46 +00:00
Anna Claire Fields bfcf75855a [PnP migration] Convert .prettierrc to .prettierrc.cjs with require.resolve (#34237)
GitOrigin-RevId: ab57ca143bca8bfd2b44f03f9712a1aae70b2c1c
2026-06-05 08:06:42 +00:00
Antoine Clausse 3140e46e68 [web] Replace token-link email verification with 6-digit code on SSO registration (ORCID) (#33889)
* Replace token-link email with 6-digit code on SSO registration

Unverified SSO emails previously received a long-lived token link
(90-day TTL) via UserEmailsConfirmationHandler. This replaces that
flow with the same 6-digit code verification used for password
registration, redirecting through /registration/confirm-email.

- SSOManager.registerSSO now always confirms email (caller must
  verify first); removes sendConfirmationEmail / _finishRegistration
- SSOController._signUp sends confirmation code and stores
  pendingSSORegistration in session when IdP email_verified is false
- New SSOConfirmEmailHandler completes registration after code check
  via completeSSOEmailConfirmation module hook
- OnboardingController confirm-email handlers accept
  pendingSSORegistration alongside pendingUserRegistration

confirmEmailFromToken (POST /user/emails/confirm) removal is deferred
to a follow-up PR to avoid breaking in-flight 90-day tokens.

Closes #28607

* Fix unverified-email edge cases; Add ORCID e2e tests;

* Rename `confirmEmail` parameter to `emailVerifiedByIdP` in _signUp function

* Remove `sendConfirmationEmail`

* Mock getUserByAnyEmail in tests

* Extract _finishSSORegistration helper to deduplicate the register →
set session flags → allocate referral → finishSaasLogin → finishLogin
sequence shared by both the direct and deferred (code-confirmed) paths.

* Stop duplicating session data in pendingSSORegistration

analyticsId, splitTests, and referal_* are already in the session at
confirmation time — no need to copy them into pendingSSORegistration.
Re-fetch splitTests fresh on completion instead.

* Simplify the code

* Remove dead confirmEmail template

No callers remain after sendConfirmationEmail was deleted. The token-link
flow (confirmEmailFromToken) only validates tokens, never sends email.

* Remove dead reconfirmEmail template

* Address comments from Copilot

* Clear stale pending registration when starting a new flow

* Add unit tests for completeSSOEmailConfirmation

* Add `verificationMethod` param

* Fix camelcase issues

* Extract _createSSOUser and _registerAndFinish helpers to deduplicate registration logic

* Remove obscure "registration_error"

* Prevent FormTextIcon from shrinking

* Enable "email_already_registered_sso" error

* Misc. improvements to confirm-email-form.tsx

* Remove `UserEmailsConfirmationHandler` mock

Co-authored-by: Olzhas Askar <olzhas.askar@overleaf.com>

* Add info on sso_email.pug page

---------

Co-authored-by: Olzhas Askar <olzhas.askar@overleaf.com>
GitOrigin-RevId: d0196ebc6d81ff61bcd27726d0b899b743d08d64
2026-06-05 08:06:34 +00:00
Daniel Kontšek 2570b6559d Merge pull request #34112 from overleaf/dn0-migration-check-predeploy-hook
Add predeploy migration gate for Mongo-bundling services

GitOrigin-RevId: d9eb192ea32b5328fb24fd453ddb1370f373858e
2026-06-05 08:06:30 +00:00
Davinder Singh 6ce36a2606 adding web changes of Export HTML (#34117)
GitOrigin-RevId: 804c576faefebfc6683a0363b45372e66a43d8fc
2026-06-05 08:06:19 +00:00
Jakob Ackermann fc2abf5b24 [web] fix submit modal in Codespaces (#34137)
GitOrigin-RevId: dc057ed736e97265a901b1cf21995c1f391339a5
2026-06-05 08:06:15 +00:00
Malik Glossop b8fc478e1f Merge pull request #34185 from overleaf/worktree-mg-error-assist-paywall
Show paywall from gutter when user hits suggestion limit

GitOrigin-RevId: 36c09e3d93ac38e1e675aa8ffb419e928094d68e
2026-06-05 08:06:11 +00:00
Malik Glossop d25b032e16 Merge pull request #33450 from overleaf/worktree-mg-writefull-spelling-tab
Add writefull language suggestions section to Spelling and language tab

GitOrigin-RevId: 6195683ca175a4c3da25a7ab334a605c67db04b8
2026-06-05 08:06:07 +00:00
Mathias Jakobsen fc31a88767 Merge pull request #34145 from overleaf/ds-download-html-using-pandoc-clsi-1
[CLSI] Download as HTML feature

GitOrigin-RevId: 374101c1f957a00eda423a6be0363c08b5de7a95
2026-06-05 08:06:03 +00:00
Jakob Ackermann 0501586743 [latexqc] migrate to local s3, add codespaces support, add e2e tests (#34136)
GitOrigin-RevId: 167171103c14ed3c4ba2939d80231c343645e53a
2026-06-05 08:05:59 +00:00
Jakob Ackermann df61bfc788 [clsi] initial version of /convert/pdf-to-jpeg (#33752)
* [monorepo] consolidate clsi-lb host/ip env-vars

Target env-var is CLSI_LB_HOST. Keep CLSI_LB_IP populated for a week.

* [clsi] initial version of /convert/pdf-to-jpeg

* [rails] use fake-secrets in CI and Codespaces

* [rails] adapt tests for using clsi to convert PDFs to image

* [rails] add rake task for comparing clsi conversion with transloadit

* [clsi] double check that output.jpg is a regular file

Co-authored-by: Brian Gough <brian.gough@overleaf.com>

* [clsi] fix composing basename

* [monorepo] fix clsi-lb host env-var post merge

* [monorepo] sort dev-environment.env hosts

* [rails] use local pdf file rather than downloading it again

Download from the old renderer code path still. It's dead code.

* [terraform] clsi: enable pdf to jpg conversion

---------

Co-authored-by: Brian Gough <brian.gough@overleaf.com>
GitOrigin-RevId: 5ecaa8559d299486340bb3961f06b29f7c4dfcca
2026-06-05 08:05:55 +00:00
Brian Gough 9ec0ff065d add missing mongo dependencies (#34298)
* add missing mongo dependency for analytics

* update build scripts for analytics

* add missing mongo dependency for third-party-datastore

* update build scripts for third-party-datastore

* add missing mongo dependency for third-party-references

* update build scripts for third-party-references

* update yarn.lock for buildscript changes

GitOrigin-RevId: 1c42e49af5075529a334d50648da990e4cedb1b4
2026-06-05 08:05:50 +00:00
Olzhas Askar 8e36f20950 Merge pull request #34267 from overleaf/oa-move-upgrade
[web] Moving the upgrade button

GitOrigin-RevId: 33dcdcfa4e816e29177abe2c045e919edd7a4e08
2026-06-04 08:07:21 +00:00
roo hutton 06e99fe62a Merge pull request #34130 from overleaf/rh-enterprise-cio
Expose enterprise indicators and previous_plan_type for first subscriptions to customer.io

GitOrigin-RevId: 693db7f796609f00ecd31216a6d6be32c1f569c8
2026-06-04 08:07:09 +00:00
Maria Florencia Besteiro Gonzalez d112271b1c Merge pull request #34184 from overleaf/cs-icon-button-labs-library
feat(library): add Labs feedback badge to Library heading

GitOrigin-RevId: 6dacc588cc58300a09b8195ca800d042d40f4c89
2026-06-04 08:06:52 +00:00
Antoine Clausse 0658bd9a31 [web] Change plans order in Change Plan modal (#34096)
* [web] Order plans in Change Plan modal consistently

Reorder the plans returned by `buildPlansListForSubscriptionDash` so the
Subscription page "Change plan" modal lists them top-to-bottom as:

  1. Student annual
  2. Student monthly
  3. Standard monthly
  4. Standard annual
  5. Pro monthly
  6. Pro annual

Previously `buildPlansList` produced three per-period buckets which the
dash function concatenated, giving an order that flipped per family.
Replace that with an explicit `CHANGE_PLAN_MODAL_PLAN_CODES` list so the
order matches the Design QA spec at a glance. The now-unused
`studentAccounts`, `individualMonthlyPlans`, `individualAnnualPlans`,
`groupMonthlyPlans`, and `groupAnnualPlans` buckets are dropped from
`buildPlansList` (no other callers).

Closes #34024

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [web] Update personal-plan acceptance test for new buildPlansList shape

The previous test asserted `buildPlansList().individualMonthlyPlans`,
which no longer exists after the change-plan modal reorder dropped the
per-period buckets. Move the assertion to
`buildPlansListForSubscriptionDash()`, which is where the personal-plan
exclusion is now enforced (via `CHANGE_PLAN_MODAL_PLAN_CODES`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [web] Drop now-dead client-side plan filter

`IndividualPlansTable` used to filter out `paid-personal`,
`paid-personal-annual` and `institutional_commons` defensively because
the old `buildPlansListForSubscriptionDash` returned every non-group
plan that wasn't `hideFromUsers`. The previous commit pins the modal to
an explicit six-plan list (`CHANGE_PLAN_MODAL_PLAN_CODES`), so none of
those plan codes ever reach the frontend and the filter is dead. Remove
it and the now-unused `useMemo` import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "[web] Drop now-dead client-side plan filter"

This reverts commit 83e8448f2cfa2c68e44b749d5a2bc350a7443c6d.

We'll do that in a later cleanup

* Swap "Student monthly" and "Student annual" for consistency

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 046a235e14e7ad6622288f5a5a723f5a4f7f14da
2026-06-04 08:06:40 +00:00
Antoine Clausse b07d141397 [web] Fix /user/subscription/plans#ai-assist redirects (#34124)
* [web] Redirect missing AI add-on purchase to subscription dashboard

The two error paths in `previewAddonPurchase` redirected to
`/user/subscription/plans#ai-assist`, but the `#ai-assist` anchor was
removed when the AI Assist add-on was retired, so users land at the top
of the plans page with no context. Align both with the other error
branches in the same function and the `plans-2026-phase-1` enabled
branch, which already redirect to
`/user/subscription?redirect-reason=ai-assist-unavailable` — the
subscription dashboard shows the matching warning alert
(`redirect-alerts.tsx`).

Update the acceptance test to match the new redirect target.

Closes #34074

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [web] Update ai-assist-unavailable warning to reflect bundled AI features

The previous copy said "AI Assist isn't available to you due to your
current subscription type", which read as a hard block. Now that the AI
Assist add-on has been retired and AI features are included with every
paid plan, the warning should point users to the pricing page instead of
implying their plan can't access AI at all.

Keep the existing translation key for now — a follow-up can rename it
once #33624 (AI page CTA destination) is resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [web] Link the ai-assist-unavailable warning to the pricing page

* [web] Rename key `ai_assist_unavailable_due_to_subscription_type` -> `ai_assist_unavailable`

* [web] Update french and german translations

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: ae1319fa5b857d8f292de77c82ef0bda1c7ad144
2026-06-04 08:06:31 +00:00
Jakob Ackermann 6869ad5bdf [misc] remove HTTP method override capability (#34243)
GitOrigin-RevId: 2d88b9cdb63c7861e0604bb19d0e0c924701f3e4
2026-06-04 08:06:22 +00:00
Domagoj Kriskovic 9cf1085fbb [web] use updateProject for saving trackChangesState
GitOrigin-RevId: eecb2b78ff18547e8b3653fdff2d380d295c367f
2026-06-04 08:06:14 +00:00
Domagoj Kriskovic ea57ae9125 Rename sourceEditorVisualExtensions to sourceEditorMarkdownExtensions
GitOrigin-RevId: a242742c3844cccb355d4a98eb27b74123ad107e
2026-06-04 08:06:09 +00:00
Domagoj Kriskovic 5cf1b43ce7 Add Markdown visual editor support
GitOrigin-RevId: 4ec2ffb276c729a58f82ccb26ed571f4187a4178
2026-06-04 08:06:04 +00:00
Chris Dryden e38f4e18e4 Merge pull request #33868 from overleaf/dk-package-loading-tests
[web] Add tests for pyodide worker streams and output pane rendering

GitOrigin-RevId: 41ffc25230be23d68d50c61980cfaf1260a0247d
2026-06-04 08:06:00 +00:00
Liam O'Brien f1282ee5cd Helper script for changing expiry of git pat (#34234)
* Helper script for changing expiry of git pat

* Validation fail for invalid date

GitOrigin-RevId: 6786d4e808e0e4e87ef1293f4c22236257948128
2026-06-04 08:05:51 +00:00
Copilot a2f72adf67 Remove useSecondary from backup worker to fix false positive errors during MongoDB failover (#34186)
* Initial plan

* Remove useSecondary from configureBackup to fix false positive backup errors

Co-authored-by: briangough <7457354+briangough@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: briangough <7457354+briangough@users.noreply.github.com>
GitOrigin-RevId: f7c9bb88fb2f7526948fee196f0444bd33a96e56
2026-06-04 08:05:47 +00:00
Brian Gough 0da93aaab3 add script to finalise broken history-v1 chunks (#34005)
* add script to finalise broken history-v1 chunks

* use history-id instead of project-id

* update project-id to history-id in tests

* silence unwanted event emitter warnings

* fix up test for historyId

GitOrigin-RevId: 58d2a768f1eff296e921e2ed985f6faf3929f619
2026-06-04 08:05:42 +00:00
Liam O'Brien e53c6f2aea Notify users about expiring git PATs and expose PATs in admin panel (#33802)
* Allow admin access to user PATs

* Tests for new screen in admin panel

* Adding error for invalid token and way to parse error for OAuth 2

* Git bridge handles expired PAT

* Script for alerting on close to expiry and expired git tokens

* Refactoring and simplifying

* Updating email templates to match agreed docs

* tweak to email subject to include Overleaf

* Allowing dry run in scripts and general tidy up

* removing redundant tests and dry running script

* Fixing CI errors

* Adding new tab to admin test expectation

* Address PR feedback on oauth2-server changes

- Replace ad-hoc overleafErrorCode prop with a TokenExpiredError subclass
- Collapse listTokens/listTokensForAdmin into a single hook

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Adding cron definitions for alerting on expiring git pat

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
GitOrigin-RevId: 69b9fd901a201592a580c69abe7bd7d603e85d3a
2026-06-04 08:05:26 +00:00
claude c249d6a6e9 Prod: load SMTP env via envFrom secretRef (flat, paste-proof)
Build and Deploy Verso / deploy (push) Successful in 1m19s
Build and Deploy Verso (prod) / deploy (push) Successful in 1m9s
Replace the six nested secretKeyRef env entries with a single
'envFrom: - secretRef: { name: verso-smtp, optional: true }' in both the
standalone app manifest and the prod workflow. Avoids the deep nesting that
tripped strict server-side decoding, and is simpler to edit. The secret's keys
must now be named exactly like the env vars (OVERLEAF_EMAIL_*).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 12:40:07 +00:00
claude 2d8f23509a Prod: standalone Deployment/Service bootstrap files; drop namespace create
Build and Deploy Verso / deploy (push) Successful in 1m22s
- Add server-ce/k8s/verso-prod-data.yaml (Mongo + Redis) and
  verso-prod-app.yaml (Verso app), mirroring the workflow so the verso
  namespace can be bootstrapped/validated by hand.
- Drop 'kubectl create namespace verso' from the prod workflow (namespace is
  pre-created), so the runner only needs namespaced rights in verso, matching
  the test namespace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 12:03:02 +00:00
claude 0f640c74b2 Prod: provision PVCs out of band (storageClass under operator control)
Build and Deploy Verso / deploy (push) Successful in 1m18s
- Add server-ce/k8s/verso-prod-pvcs.yaml (mongo-data/redis-data/verso-data,
  ReadWriteOnce, storageClassName left for the operator to set — use a Ceph RBD
  block class).
- Drop the inline PVC definitions from deploy-verso-prod.yml so it won't fight
  the operator-provisioned PVCs; the deploy now assumes they already exist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:36:41 +00:00
claude 54ccb3d712 Add prod deploy workflow (verso namespace, persistent, friends-only)
Build and Deploy Verso / deploy (push) Successful in 1m20s
New .gitea/workflows/deploy-verso-prod.yml triggered by pushes to the 'prod'
branch — a real production target distinct from the ephemeral test rig:

- Runs in the 'verso' namespace; Mongo/Redis/app-data on PersistentVolumeClaims,
  applied idempotently and NEVER deleted (data survives deploys).
- Replica set initialised only once; admin created only if no users exist.
- Builds/pushes verso:stable (separate tag from test's verso:latest);
  imagePullPolicy Always so each rollout pulls the new build.
- SMTP via an optional 'verso-smtp' Secret (no credentials in the repo);
  anonymous read-write sharing left off and public registration off
  (friends-only).
- Example Ingress for verso.alocoq.fr at server-ce/k8s/verso-prod-ingress.example.yaml
  (apply by hand to match the existing TLS/annotation setup).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:21:33 +00:00
claude 35fa7cec05 README logo + dates; capitalize Alpha in instance title
Build and Deploy Verso / deploy (push) Successful in 1m21s
- README: show the Verso wordmark logo instead of a text title.
- README: original Overleaf copyright now 2014-2026; Verso modifications 2026.
- Instance/version title: 'alpha' -> 'Alpha'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:01:23 +00:00
claude 2385166213 Dashboard footer fix, larger version text, rewrite README
Build and Deploy Verso / deploy (push) Successful in 9m47s
- Fix the projects dashboard footer needing a scroll to reach: the main area
  used min-height: 100% which always pushed the footer a full screen down.
  Lay the content out as a flex column with main growing (flex: 1 0 auto), so
  the footer sticks to the bottom of the viewport when the list is short.
- Bump the instance-name/version text to ~33px ('7.5', between font-size-07
  and -08).
- Rewrite README to match the current triple-compiler product (Quarto + LaTeX
  + Typst), the editor language support, format badge, publishing flow and
  Python venv option; drop the stale 'Quarto-only / TeX Live removed' notes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:31:49 +00:00
claude fddb141d19 Polish: smaller version text, bigger loader logo, keep RevealJS slide on recompile
Build and Deploy Verso / deploy (push) Successful in 9m35s
- Reduce the dashboard instance-name/version font size (07 -> 06).
- Enlarge the Verso logo in the loading animation (160px -> 240px).
- Preserve the current RevealJS slide across recompiles: capture the deck's
  URL hash (same-origin) and re-append it to the iframe src so the new build
  reopens on the same slide instead of jumping to the start.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 09:10:33 +00:00
claude 8272d6de88 Editor/dashboard polish: PDF publish, Typst outline, bigger branding
Build and Deploy Verso / deploy (push) Successful in 9m29s
- Hide the Present button when the current output is a PDF (it only makes
  sense for HTML/RevealJS decks).
- Publish now supports PDF projects: snapshot output.pdf and serve it inline
  via a small index.html wrapper at /p/:token, so link holders can view the
  PDF straight from the published version.
- Add a Typst document outline (scans '=' headings) wired into the file
  outline panel.
- Dashboard branding: enlarge the instance-name/version text and let the
  sidebar Verso wordmark span the full column width.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 08:33:56 +00:00
jmescuderowritefull a553a8390d Clean 'writefull-keywords-generator' flag (#34200)
GitOrigin-RevId: 3014f02eba721b002eb35ec81750252993597748
2026-06-03 08:07:27 +00:00
Mathias Jakobsen 5ad548e7d7 Merge pull request #34199 from overleaf/mj-tabs-divider-tweaks
[web] Drop dividers next to active tab

GitOrigin-RevId: 9610e22b0aa7f036233108282687772c30f4c1b0
2026-06-03 08:07:06 +00:00
Maria Florencia Besteiro Gonzalez 021b2e305c Merge pull request #34108 from overleaf/mfb-show-warning-of-duplicate-citation-keys
Show duplicate citation keys as a warning beside the relevant entry

GitOrigin-RevId: e8506b2d77febec6d269a242f6d9b237171db66f
2026-06-03 08:06:44 +00:00
Mathias Jakobsen cc762bb7e6 Merge pull request #33994 from overleaf/mj-command-palette-synctex
[web] Add synctex to command palette

GitOrigin-RevId: 10e769dae6088d279d010fcfa3577b489c6ff89c
2026-06-03 08:06:40 +00:00
Brian Gough f8c7e092fa upgrade to eslint v10 (#34054)
* upgrade from eslint version 8 to eslint version 10

* remove unsupported eslint-env directive

* include jsx files in latexqc linting

* use basePath and extends to maintain paths in writefull eslint

* fix yarn.lock

with ./bin/yarn install

* preserve existing glob patterns in web eslint config

* restore original comments

* fix worker path

* corrected comment about eslint-plugin-mocha

* remove unused imports

* remove unused import of includeIgnoreFile

* switch to individual eslit.config.mjs files

* fix lint errors on eslint.config.mjs in web

* update build scripts for eslint.config.mjs

* update volumes for RUN_LINTING_CI_MONOREPO in web Makefile

updated manually as this makefile is not autogenerated
the RUN_LINTING_CI_MONOREPO command is only used for prettier, not eslint, but updating for consistency.

* migrate from mocha/no-skipped-tests to mocha/no-pending-tests

see https://github.com/lo1tuma/eslint-plugin-mocha/pull/365
"rule no-skipped-tests has been removed, its functionality has been merged into the existing no-pending-tests rule"

GitOrigin-RevId: 2c8f25c8049a0dba374a51df1214286bb5093a51
2026-06-03 08:06:29 +00:00
Mathias Jakobsen 98bd09c31d Merge pull request #34189 from overleaf/mj-fix-flaky-review-panel-tests
[web] Fix flaky <ReviewPanel /> Cypress tests

GitOrigin-RevId: b34dc9a0ca53da5a282513e8fb92297e4b2f702a
2026-06-03 08:06:17 +00:00
Alf Eaton 78bea8d574 Use Emulation.setFocusEmulationEnabled in Cypress (#33787)
GitOrigin-RevId: d3b9ba1b2362bdb23dbf8282514c972c52c83fec
2026-06-03 08:06:04 +00:00
Alf Eaton 979f065581 Upgrade to MathJax v4 (#15030)
GitOrigin-RevId: d1536bce67286da23e15aa18eb525dd83859978b
2026-06-03 08:05:55 +00:00
Andrew Rumble b6451d5bb0 Merge pull request #34179 from overleaf/ar-analytics-upgrade-development-postgres
[analytics] upgrade test postgres version to 14

GitOrigin-RevId: fac9d063c2572d4393d885554ef688876c300c29
2026-06-03 08:05:50 +00:00
Lucie Germain d52b5ae141 [Security upgrade] bump brace-expansion to 5.0.6 (#33915)
* Bump brace-expansion to 5.0.6 in linked-url-proxy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* drop unnecessary brace-expansion resolution; ^5.0.5 already permits 5.0.6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
GitOrigin-RevId: 837dcd88e5e0a6181d3ac2fe4f512a6ec1904002
2026-06-03 08:05:41 +00:00
claude 4fc86ebd3d Editor: qmd/typst autocomplete, format column, compiler gating, Verso loader
Build and Deploy Verso / deploy (push) Successful in 9m48s
- Add a Typst language (stream highlighting + completions) for .typ, and
  Quarto completions (code chunks, callouts, cross-refs) for .qmd/markdown.
- Project dashboard: new Format column (Quarto/Typst/LaTeX) from the cheap
  project compiler field, surfaced through the projects list API.
- Compiler dropdown: grey out engines that don't match the root file's
  extension (.qmd->Quarto, .typ->Typst, .tex->LaTeX engines).
- Replace the Overleaf fill loader with an animated Verso logo: the four
  quadrant circles drift on their own orbits while colour warms up with load
  progress; reused on the token-access screen too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 07:47:31 +00:00
claude 12cabd1d1b Branding: build-number version, EB Garamond title, blue filters, Present button
Build and Deploy Verso / deploy (push) Successful in 9m21s
- Instance name: stamp the nav title with the build number at deploy time
  ("Verso V0.<run> alpha") via a sed placeholder fed by GITHUB_RUN_NUMBER,
  instead of the static "Verso V1.0 Alpha".

- Title typeface: self-host the EB Garamond latin subset (same one embedded in
  the logo SVGs) and apply it to .navbar-title so the instance name matches the
  Verso wordmark.

- Sidebar wordmark: let the logo fill the full sidebar column width (drop the
  160px cap).

- Project filters: switch the ds-nav active state (filter selection + theme
  toggle) from the green tokens to the blue scale, matching the rail.

- Present button: rename the presentation toolbar action from "Preview" to
  "Present" / "Présenter" and add a tooltip explaining it publishes the
  presentation and opens it in a new tab. New keys present /
  present_publishes_and_opens_in_new_tab in en, fr and extracted-translations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 22:32:07 +00:00
claude 676663ffcc Branding polish: blue rail accent, drop fork link, bigger login logo, dashboard logo placement
Build and Deploy Verso / deploy (push) Successful in 9m41s
- Editor rail: the active item used the Overleaf green accent. Switch
  --ide-rail-link-active-color/background to the blue scale (--blue-10/70,
  --bg-info-03) to match the Verso palette.

- Footers: remove the default "Fork on GitHub!" right_footer item (redundant
  with the "Built on Overleaf" link); right_footer now defaults to [].

- Login: move the hero wordmark into a full-width centered block and bump it to
  max-width 480px so it's no longer constrained by the form column.

- Projects dashboard: restore the instance name in the top-left navbar (set
  OVERLEAF_NAV_TITLE="Verso V1.0 Alpha") instead of the wordmark logo, and move
  the full Verso wordmark to the sidebar's lower section (where the old
  "Digital Science" mark sat). Revert HeaderLogoOrTitle to its title-first
  behaviour now that the dashboard no longer passes a logo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 21:28:50 +00:00
claude 7d5deebfce Fix Python-packages toolbar icon: add deployed_code to unfilled font subset
Build and Deploy Verso / deploy (push) Successful in 9m35s
The file-tree "Python packages" button rendered the literal text
"deployed_code" in the icon font because that glyph was missing from the
outlined/unfilled Material Symbols subset (MaterialSymbolsRoundedUnfilledPartialSlice.woff2),
so the ligature never resolved. The toolbar buttons all use the unfilled
variant, so switching this one to the full filled font would look inconsistent.

Add 'deployed_code' to unfilled-symbols.mjs and regenerate the subset woff2
(same Google Fonts request build-unfilled.mjs makes) so the box/package icon
renders, matching the other outlined toolbar icons.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 21:06:55 +00:00
claude 7a50f42e02 Restore dashboard footer; show navbar logo over title; enlarge login logo
Build and Deploy Verso / deploy (push) Successful in 9m46s
Three follow-ups after the visual-identity deploy:

- Footer: restore the React <Footer> on the projects dashboard (both
  ProjectListDsNav and the legacy DefaultNavbarAndFooter). Removing it earlier
  was an overcorrection — it now renders the Verso/AGPL thin footer rather than
  the old "Powered by Overleaf" line. Other pages already kept the pug footer.

- Navbar brand: HeaderLogoOrTitle previously hid the logo whenever a nav title
  was set, so on the dashboard only the "Verso" instance-name text showed and
  the wired-up Verso logo never appeared. Make a configured logo (custom logo
  or the Verso brand logo) take precedence over the title text; fall back to the
  title only when no logo is provided (unchanged for other navbars).

- Login: enlarge the hero wordmark (max-width 260px -> 380px, full column width).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 20:41:18 +00:00
claude 38edd5269c CI: retry Yarn Classic dep packing on transient esbuild fetch corruption
Build and Deploy Verso / deploy (push) Successful in 11m5s
Build #78 failed in the compile step while Yarn Classic prepared the
@replit/codemirror-* git deps: fetching esbuild's per-platform binaries
returned truncated tarballs ("the file appears to be corrupt" / missing
.yarn-tarball.tgz). The tmpfs classic cache is fresh each build, so there is no
stale entry to blame and nothing to fall back to — it is a transient download
failure (builds #75-77 passed with an identical Dockerfile).

Wrap both the install and compile steps in a 3-attempt retry loop that wipes
the Yarn Classic cache (/usr/local/share/.cache/yarn) and re-fetches before
giving up, dumping pack.log on final failure. The persistent Berry cache and
YARN_NETWORK_CONCURRENCY=1 are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 20:03:51 +00:00
claude 2eccfe7f75 Add Verso visual identity (logos + favicon)
Build and Deploy Verso / deploy (push) Has been cancelled
Introduce the Verso brand marks as self-contained SVGs with the EB Garamond
latin subset embedded as a base64 @font-face, so they render identically in
every context (favicon, CSS background, <img>, inline) with no runtime Google
Fonts dependency — important for the self-hosted alpha. Falls back to Georgia
serif if a browser ignores SVG-embedded fonts.

Assets:
- verso-square.svg  — rounded "V" tile (200×200); used as favicon.svg and the
  editor top-left toolbar logo.
- verso-logo.svg / verso-logo-dark.svg — wide "verso · ONLINE EDITOR" wordmark
  (760×200), light + dark wordmark variants.

Wiring:
- favicon: public/favicon.svg replaced with the square mark.
- editor toolbar: --redesign-toolbar-logo-url (light + dark) -> verso-square.svg.
- projects dashboard navbar: ProjectListDsNav logo -> verso-logo(.dark), with
  --navbar-brand-width widened to 200px to fit the wide wordmark.
- login page: centered Verso wordmark above the form; suppress the top navbar
  so the hero logo stands alone (no competing Overleaf mark).

PNG favicons / apple-touch-icon are left as-is (no raster tooling available);
modern browsers use the SVG favicon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:43:36 +00:00
claude 7670982f60 Dashboard: drop footer + Digital Science branding; Verso-ify React thin footer
Build and Deploy Verso / deploy (push) Successful in 9m47s
The project dashboard ("main menu") rendered the React <Footer> (ThinFooter)
at the bottom of the page, which forced a page-level scrollbar just to reach a
stale "© 2025 Powered by Overleaf" line. Remove <Footer> from both dashboard
variants (ProjectListDsNav and the legacy DefaultNavbarAndFooter) so the main
menu has no footer and no useless scroll. The login/auth pages keep the Verso
attribution footer (thin-footer.pug), which already satisfies the AGPL source
link requirement.

Also remove the hardcoded "Digital Science" marking from the dashboard sidebar
lower section, and update the React ThinFooter (used by other React pages such
as settings/subscription) to match the pug footer: © <year> Aloïs Coquillard ·
Built on Overleaf, with AGPL licence + source-code links on the right, instead
of "Powered by Overleaf".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:13:19 +00:00
claude f50d6cb053 Footer: put Verso/AGPL attribution in the rendered thin-footer
Build and Deploy Verso / deploy (push) Successful in 9m41s
The login (and other CE) pages render layout/thin-footer, not
fat-footer-website-redesign, because showThinFooter = !hasFeature('saas')
is true in Community Edition. The earlier footer edit therefore never
showed. Replace the static "© 2025 Built on Overleaf" line with the
Verso attribution (© <year> Aloïs Coquillard · Built on Overleaf) and
add an AGPL licence + source-code link group on the right, keeping the
language picker and custom nav items intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 17:42:06 +00:00
claude e0a4938a78 CI: make Yarn Classic fallback cache a tmpfs (fresh per build)
Build and Deploy Verso / deploy (push) Successful in 11m9s
#74 corrupted the persistent fallback cache again despite serialising the
fetch, so the cause isn't a write race: BuildKit evicts part of that persistent
cache mount between builds (the first build after each id bump always passed,
later ones failed). Mount /usr/local/share/.cache/yarn as tmpfs so it's clean
every build and nothing can be half-evicted; the Berry cache stays persistent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 15:15:38 +00:00
claude 2e657e51d6 Replace marketing footer with Verso attribution/AGPL footer
Swap the Overleaf marketing footer (careers, pricing, 'for universities', …)
shown on the login and other auth pages for a clean Verso footer: copyright
(Aloïs Coquillard -> alocoq.fr), 'Built on Overleaf' (-> Overleaf repo), and on
the right the AGPL licence (-> repo LICENSE) and Source code (-> the Gitea
repo). This also satisfies the AGPL source-offer on the public domain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 15:06:27 +00:00
claude 4c13d139f6 Share modal: 'Add collaborators' heading; fix French 404 title
Build and Deploy Verso / deploy (push) Has been cancelled
Add an 'Add collaborators' heading above the email-invite section in the share
modal so it's visually distinct from the presentation-sharing section.

Add the missing French 'not_found' key so the 404 page shows 'Introuvable'
instead of the raw 'not_found' (the 404 pug template translates server-side;
the key existed in en.json but not fr.json).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 14:36:33 +00:00
claude 405c1d27c9 Bundle Python requirements into a dedicated editor; hide requirements.vrf
Add a 'Python packages' button to the file-tree toolbar that opens a modal to
edit the project's requirements.vrf (one package per line, pip syntax), backed
by GET/POST /project/:id/python-requirements (read via ProjectEntityHandler,
write via EditorController.upsertDocWithPath, write-gated). The .vrf file is now
hidden from the file tree, so it is managed only through this editor rather than
appearing as a loose file. Adds python_packages / python_packages_help i18n.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 14:26:03 +00:00
claude c9727a26e4 Python deps: smart missing-package hint + switch to .vrf requirements file
Build and Deploy Verso / deploy (push) Successful in 9m46s
Option A: when a {python} cell fails with ModuleNotFoundError/ImportError, the
log now suggests the exact PyPI package to add (with a module->package map, e.g.
cv2 -> opencv-python, sklearn -> scikit-learn), names the Verso requirements
file, and notes it could instead be a local module — so the langmuirthermalstudy
case isn't mistaken for a PyPI package.

Switch the per-project requirements file from requirements.txt to a Verso-
specific requirements.vrf (so it won't be confused with arbitrary .txt files);
QuartoRunner now looks for requirements.vrf, and 'vrf' is registered as an
editable text extension. The dedicated in-UI editor (and hiding it from the
file tree) follows in a separate change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 14:19:01 +00:00
claude 8530c5ebe0 Run Quarto Python kernel inside the project venv, not base python
Build and Deploy Verso / deploy (push) Successful in 9m33s
The global python3 kernelspec hardcodes /usr/bin/python3, so even with
QUARTO_PYTHON pointing at the project venv, Quarto launched the kernel in the
base interpreter — packages installed into the venv (e.g. openpyxl) were not
importable. Register a python3 kernelspec inside the venv via
'ipykernel install --sys-prefix' (kernel.json argv -> the venv's python); since
Quarto runs kernel discovery through QUARTO_PYTHON, the venv's kernelspec is
found ahead of the global one and the kernel runs in the venv.

Bump the completion marker (.verso-complete -> .verso-ready) so venvs built
before this change are rebuilt with the kernelspec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 13:50:01 +00:00
claude 83b6b323c3 Add cv2/tqdm to base; implement per-project Python venvs (Design B, Phase 1)
Build and Deploy Verso / deploy (push) Successful in 17m0s
Base image: add opencv-python-headless (cv2) and tqdm to the bundled
scientific stack, and python3-venv (needed to build per-project venvs).

Per-project dependencies: a project's requirements.txt is now installed into a
venv cached by its sha256 (python3 -m venv --system-site-packages, so the
bundled stack stays visible and only extra packages are installed); QuartoRunner
points Quarto at it via QUARTO_PYTHON. A per-hash flock serialises concurrent
builds; pip output is merged into output.log; on failure the render falls back
to the base interpreter. Venvs live under PYTHON_VENVS_DIR
(default /var/lib/overleaf/data/python-venvs).

Gating: PythonVenvGate.userCanInstallPython restricts installs to the project
owner + invited collaborators (ignorePublicAccess excludes anonymous/link
users), threaded to CLSI as allowPythonInstall on the editor compile,
presentation export, and publish paths. Behind OVERLEAF_ENABLE_PROJECT_PYTHON_VENV
(enabled in the deployment). Design doc updated; Phase 2 (egress policy) and
Phase 3 (venv eviction) remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 13:14:47 +00:00
claude 8b9fe4e760 CI: stop Yarn git-dep prepare from corrupting the shared fallback cache
Build and Deploy Verso / deploy (push) Successful in 12m50s
The web build's 'yarn install' re-prepares the git-sourced @replit/codemirror-*
deps whenever the Berry cache misses (BuildKit GCs it between builds). Each
prepare uses Yarn Classic, which pulls every esbuild platform binary into the
single shared /usr/local/share/.cache/yarn folder; running several prepares in
parallel races and corrupts it ('tar content corrupt', EEXIST, missing
.yarn-tarball.tgz). Bumping the cache id only cleared it until the next
cache-miss build (#69).

Serialise Yarn's fetch with YARN_NETWORK_CONCURRENCY=1 on the install and
compile steps so the prepares no longer write that cache concurrently, and bump
the fallback cache id (v2 -> v3) once more to discard the currently-corrupt
cache. Slightly slower fetch, but no more random cache corruption.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 12:20:35 +00:00
claude 654cd7db9f Fix Quarto Jupyter engine: install PyYAML
Build and Deploy Verso / deploy (push) Has been cancelled
Quarto's own jupyter wrapper (/opt/quarto/share/jupyter/jupyter.py ->
notebook.py) does 'from yaml import safe_load', so executing a {python} cell
failed with ModuleNotFoundError: No module named 'yaml'. The minimal jupyter
stack didn't pull PyYAML in (psutil/ipython already come via ipykernel), so
add pyyaml explicitly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 11:39:48 +00:00
claude 51620caf8b docs: design for per-project Python dependencies (cached venv)
Captures the proposed requirements.txt -> cached virtualenv approach (keyed by
hash, --system-site-packages, QUARTO_PYTHON), its guard rails (auth gating,
egress restriction, resource caps) given anonymous write is enabled, lifecycle
(eviction, failure UX), a phased rollout, and the open decisions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 11:35:38 +00:00
claude 96fc1a90a1 Surface missing-Python-package errors clearly in the Quarto log
Build and Deploy Verso / deploy (push) Successful in 14m45s
When a {python} cell fails with ModuleNotFoundError/ImportError, the Quarto
log parser now emits an actionable error ('Python package "X" is not
installed on the server') noting which scientific packages are pre-installed,
instead of leaking an opaque traceback line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 11:32:08 +00:00
claude f1d827202f Bundle the scientific-Python stack in the base image
Pre-install numpy, pandas, scipy, matplotlib, seaborn, scikit-learn, sympy,
plotly and tabulate so the common data-science libraries are available to
Quarto's Python code cells out of the box. matplotlib uses the headless Agg
backend automatically in the compile environment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 11:25:18 +00:00
claude 8691907210 Enable Python code execution in Quarto documents (install Jupyter)
Build and Deploy Verso / deploy (push) Successful in 14m1s
Quarto executes ```{python}``` cells via a Jupyter kernel, but the base image
had no Jupyter ('Jupyter: (None)') and the runtime user (www-data) couldn't
create Quarto's log dir or Jupyter's runtime dir ('Permission denied: mkdir
/var/www/.local/...').

Install the headless Jupyter execution stack (jupyter-client, nbclient,
nbformat, ipykernel) for the system python3 Quarto uses, and register a
system-wide python3 kernelspec under /usr/local/share/jupyter. Also make
/var/www/.local writable by www-data so Quarto/Jupyter can write their
runtime/log files (mirrors the existing /var/www/.cache setup).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 11:20:22 +00:00
claude e3fb781042 CI: bust corrupted Yarn fallback build cache
Build and Deploy Verso / deploy (push) Successful in 8m0s
The web compile step failed packing the git-sourced @replit/codemirror-*
deps with 'tar content corrupt' / EEXIST / missing .yarn-tarball.tgz errors,
all under /usr/local/share/.cache/yarn/v6 — i.e. a corrupted BuildKit
fallback-cache mount (likely left half-written by an interrupted build), not
a code or dependency change. Bump the fallback cache id so BuildKit
allocates a fresh empty cache; the berry and webpack caches are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 10:07:14 +00:00
claude f2abd42969 Presentation export: progress modal + inline failure log
Build and Deploy Verso / deploy (push) Has been cancelled
The HTML/PDF export links were plain downloads that left the browser
silently spinning during the server-side render and, on failure, saved an
error page as pdf.txt/pdf.htm. Replace them with fetch-based downloads that
show a modal: a spinner with a 'this can take up to a minute' message while
compiling, and the actual compile log inline if the export fails. The user
can dismiss at any time; a stale request that finishes after dismissal no
longer reopens the modal. Adds the three i18n keys (en/fr + extracted).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 09:57:25 +00:00
claude 7e1c2ce53a Fix standalone-HTML export: inject embed-resources into the deck frontmatter
Build and Deploy Verso / deploy (push) Has been cancelled
embed-resources cannot be enabled from the CLI: Quarto only honours it when
nested under the format, and a document's own format block fully overrides
project/CLI metadata (confirmed in Quarto docs). So --metadata embed-resources
was silently ignored and the 'standalone' HTML was the ordinary non-embedded
deck referencing a sibling _files/ dir — unstyled, no math, no images once
downloaded on its own.

For the html-standalone export, render a temporary copy of the root .qmd with
embed-resources/self-contained-math enabled and chalkboard disabled inside its
revealjs block (replacing an existing chalkboard key rather than duplicating
it), then clean the temp file up. Falls back to the original file if the deck
isn't an editable nested-revealjs document, so the export is never worse than
before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 09:51:52 +00:00
claude 67b27c2684 Fix PDF export Chromium launch + HTML export caching
Build and Deploy Verso / deploy (push) Successful in 7m54s
The slide-PDF export failed because the CLSI runtime user has no writable
HOME, so Chromium's crashpad couldn't create its database and the browser
died on launch ('chrome_crashpad_handler: --database is required'). Give
decktape's Chromium a fresh writable temp dir via HOME/XDG_*/--user-data-dir
(plus --disable-gpu).

The standalone-HTML export kept returning the old non-embedded file partly
because the GET response had no cache headers, so the browser served its
cached copy; add Cache-Control: no-store to both export responses. Also
switch the embed-resources flags to the long '--metadata KEY:VALUE' form
(the documented Quarto syntax) to remove any ambiguity vs the '-M' alias.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 09:27:44 +00:00
Jakob Ackermann ec84a88eb3 [server-ce] tests: build git-bridge latest locally in CI (#34177)
GitOrigin-RevId: 78bf654eee77a62185a53b1abdf2cd9ae0662802
2026-06-02 08:08:39 +00:00
Mathias Jakobsen 96e0830eef Merge pull request #34167 from overleaf/mj-conversion-error-update
[web] Point conversion errors to docs page

GitOrigin-RevId: 1a5208065252159b6a69bc6ae4cecae1dd0cd4d8
2026-06-02 08:08:31 +00:00
Copilot a9a9f6ee6b Migrate history-v1 recover_zip scripts from archiver to zip-stream (#32813)
* migrate recover_zip_from_backup from archiver to zip-stream

Replace the `archiver` package with `zip-stream` (the lower-level library
that `archiver` wraps) in the `recover_zip_from_backup.mjs` script and
`backupArchiver.mjs` library. The `archiver` package has known issues with
hanging when creating large zip files and is no longer actively maintained.

Changes:
- Add `zip-stream@^7.0.2` as a direct dependency
- Update `backupArchiver.mjs` to use promisified `ZipStream.entry()`
  instead of `Archiver.append()`
- Rewrite `recover_zip_from_backup.mjs` to use `ZipStream` with
  `stream/promises.pipeline` for cleaner async flow
- Keep `archiver` dependency for `project_archive.js` (separate code path)

Agent-Logs-Url: https://github.com/overleaf/internal/sessions/0df27a8b-97f1-43cc-ac26-f5247a84313f

Co-authored-by: briangough <7457354+briangough@users.noreply.github.com>

* extract finalize timeout to named constant

Agent-Logs-Url: https://github.com/overleaf/internal/sessions/0df27a8b-97f1-43cc-ac26-f5247a84313f

Co-authored-by: briangough <7457354+briangough@users.noreply.github.com>

* convert recover_zip.js to zip-stream, remove finalize timeout, add verbose logging

Agent-Logs-Url: https://github.com/overleaf/internal/sessions/9380d08a-d813-4e9f-a2ac-4891122c163b

Co-authored-by: briangough <7457354+briangough@users.noreply.github.com>

* add acceptance tests for recover_zip_from_backup in raw and latest modes

Agent-Logs-Url: https://github.com/overleaf/internal/sessions/9380d08a-d813-4e9f-a2ac-4891122c163b

Co-authored-by: briangough <7457354+briangough@users.noreply.github.com>

* fix comment formatting in recover_zip_from_backup.mjs

Agent-Logs-Url: https://github.com/overleaf/internal/sessions/9380d08a-d813-4e9f-a2ac-4891122c163b

Co-authored-by: briangough <7457354+briangough@users.noreply.github.com>

* restore EventEmitter.defaultMaxListeners in recover_zip.js, add acceptance test

Agent-Logs-Url: https://github.com/overleaf/internal/sessions/e7443126-22d5-4d0e-a176-a7a5dba49ffd

Co-authored-by: briangough <7457354+briangough@users.noreply.github.com>

* fix formatting

* refactor: simplify stream handling by using named imports for pipeline

* fix blob hash verification in backup acceptance tests

* fix recover_zip script and tests

* fix: exit with non-zero status on error in recover_zip.js

Agent-Logs-Url: https://github.com/overleaf/internal/sessions/ef3f109b-488f-47c9-84a5-b5269387166a

Co-authored-by: briangough <7457354+briangough@users.noreply.github.com>

* migrate from npm to yarn

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: briangough <7457354+briangough@users.noreply.github.com>
Co-authored-by: Brian Gough <briangough@users.noreply.github.com>
GitOrigin-RevId: 6255f9610f3c846790e2ed8b1979ac08b7effece
2026-06-02 08:08:18 +00:00
Mathias Jakobsen 7053434da6 Merge pull request #34160 from overleaf/mj-tabs-design-tweaks
[web] Apply design tweaks to editor tabs

GitOrigin-RevId: c0b064c4f5977bb13a961f03d2c5f2949d338cfe
2026-06-02 08:08:13 +00:00
Mathias Jakobsen 24dba36060 Merge pull request #34152 from overleaf/mj-select-all
[web] Add select all to context menu

GitOrigin-RevId: ff5fb828db8e1cd57d1361a2e572918339e5e18b
2026-06-02 08:08:08 +00:00
Brian Gough fda0283490 Merge pull request #33377 from overleaf/lucie/js-yaml-security-fix
[Security Upgrade]: js-yaml in yarn.lock

GitOrigin-RevId: 4f388ca74de0e33a4f8894b1aa7e7963d1de552d
2026-06-02 08:07:45 +00:00
Antoine Clausse 2a5f1be811 [web] Fix "For students" link, fix toggles and navigation (#34051)
* [web] Fix footer For Students link to activate student toggle

The footer link only set itm_referrer plus a #student-annual hash. The
plans page reads the active plan/period from `plan` and `period` query
params (PlansHelper.getPlansPageViewOptions), so the student tab never
activated from the footer link.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* syncStudentModeFromPlanType after in handleDeprecatedHash

* Change URL update to use replaceState in the pricing page

* Revert "Change URL update to use replaceState in the pricing page"

This reverts commit eac71f193029e3f1c75e0c97261d8a5982c0d35c.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 69d689d0fe89fc68cefab9233739fc61da8f2ced
2026-06-02 08:07:40 +00:00
Antoine Clausse 105a0ff35c [web] Add nonprofit discount FAQ to plans page (#34126)
Insert a new "Do you offer discounts for nonprofits?" accordion item
under the educational group discount question in the "Overleaf
multi-license plans" FAQ tab. Routes the "contact sales" link through
the existing `faqContactLink` mixin so click tracking stays consistent
with the other FAQ contact links.

Closes #33494

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 582517f1d1f1f7934610253c252cf0f8af2b68a2
2026-06-02 08:07:35 +00:00
Antoine Clausse 2c7129be3a [web] Stop bolding AI features on the interstitial plans page (#34125)
* [web] Stop bolding AI features unconditionally on the interstitial

The four `strong: true` flags on the AI features in `sectionMain2026`
caused those rows to render bold on every interstitial visit, regardless
of paywall context. The original intent (per Design QA #34022) was for
boldness to highlight the features relevant to the specific paywall the
user came from (e.g. AI paywall -> AI features bolded) — that
conditional logic was never wired up, and currently no `purchaseReferrer`
or paywall reason is plumbed through to the feature config.

Remove the unconditional `strong: true` so the cards render consistently
with the pricing page. Reintroduce conditional bolding in a follow-up
once the paywall→features mapping is scoped by design.

Closes #34022

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove `card-include-strong` and related code

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 2112214217f3b53d34518efbca546082ce559e26
2026-06-02 08:07:31 +00:00
Antoine Clausse bd4f73b836 [web] Render "Try for free instead" CTA as link, not button (#34098)
* [web] Render "Try for free instead" CTA as link, not button

Design QA wants the "Try for free instead" CTAs on the pricing and
interstitial pages styled as marketing links (`link-monospace link-lg`)
rather than the current `btn-ghost` button. Add a `link` button type to
the `plans-cta` mixin that drops the `btn` class and applies the link
classes, and set `buttonType: 'link'` on the six `try_for_free_instead`
CTAs (plans-individual, plans-student, interstitial-payment).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Make link smaller

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: f911698a9bfa19f8180e58edb3cebcea90468cbd
2026-06-02 08:07:26 +00:00
Antoine Clausse 0e4fe4090a [web] Migrate manual plurals to i18next _plural convention (#33989)
* Add tests on plurals

* Update `collabs_per_proj` and its pluralisations

* Update `n_user` and its pluralisations

* Update `showing_x_results` and its pluralisations

* Update `show_x_more_projects` and its pluralisations

* `bin/run web npm run extract-translations`

* Populate `_plural` keys in non-en locales

For 2-form languages (da, de, es, fi, fr, it, nl, no, pt, sv, tr), copy
the existing bare-key value into the new `_plural` sibling to prevent
i18next from falling back to English for count!=1.

Also remove orphan singular keys (`collabs_per_proj_single`,
`showing_1_result*`) left over from the previous commits.

Bare-key values remain in their original plural form pending translator
review — count=1 will still render the plural form in non-en until
translators flip those to singular. Multi-form (cs, pl, ru) and
single-form (ja, ko, zh-CN, zh-TW) locales are unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Flip non-en bare-key values to singular form

Per review, the i18next v3 plural convention uses the bare key for count=1
(singular) and `_plural` for count!=1. The non-en bare-key values were
left as the original plural form by the previous commit so the `_plural`
siblings could be copied from them; this commit flips the bare values to
the singular form per language.

Languages where singular and plural noun forms coincide (Finnish, Swedish,
Turkish) are unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Apply suggestions from code review

Co-authored-by: Olzhas Askar <olzhas.askar@gmail.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Olzhas Askar <olzhas.askar@gmail.com>
GitOrigin-RevId: 628513ca792c2dcce023247e52b7320e2741cc54
2026-06-02 08:07:17 +00:00
Antoine Clausse 58884231c1 [web] Redirect to plans page when previewing subscription change without an existing subscription (#33925)
GitOrigin-RevId: feb47fb519dd7872149d787a8543293cae66a908
2026-06-02 08:07:12 +00:00
Chris Dryden db1deb1617 Merge pull request #33895 from overleaf/cd-pyodide-unsupported-module-message
Improve error messaging for python modules unsupported by Pyodide

GitOrigin-RevId: 038c672ad9da46ea6d4640b8ed37426c92d22e72
2026-06-02 08:07:01 +00:00
Brian Gough b8067723b6 Merge pull request #33628 from overleaf/lg-otel-security-upgrade
Bump @opentelemetry/sdk-node and auto-instrumentations-node (GHSA-q7rr-3cgh-j5r3)

GitOrigin-RevId: 2d5bac25735e9ef8a462423505f142f49ef73d8b
2026-06-02 08:06:52 +00:00
Davinder Singh c09ada9ddb Revert "[WEB] Move Review Toggle into the toolbar (#34066)" (#34150)
This reverts commit 36847e0debdc4dce5f96492261d25e7cc46b2e96.

GitOrigin-RevId: 9bab306156006f683314fd59eea45854f62eae62
2026-06-02 08:06:47 +00:00
Davinder Singh 8b61e8cdca [WEB] removing the beta icon from import modal (#34025)
* removing the beta icon from import modal

* removing the unused classes

GitOrigin-RevId: 11dbe04f31ba831f96e30ab93f3f6c732166e08f
2026-06-02 08:06:38 +00:00
Davinder Singh e0f542a241 [WEB] Move Review Toggle into the toolbar (#34066)
* move Review Toggle into the toolbar

* cleaning up and adding a comment

* adding the cursor styling

* adding isolation on writefull toolbar to adjust z-index of writefull toolbar

* fixing the dark mode colours for review dropdown trigger

* Fix review mode switcher dark mode styles

GitOrigin-RevId: 36847e0debdc4dce5f96492261d25e7cc46b2e96
2026-06-02 08:06:34 +00:00
Davinder Singh ac83bc520c Merge pull request #34065 from overleaf/ds-move-toggle-to-right
[WEB] Move Code/Visual toggle to right-hand side and redesign

GitOrigin-RevId: efc1aa062fd44e20fdf719a6d4ecba9d8bb0e5e8
2026-06-02 08:06:28 +00:00
claude 4d9adb2723 Fix presentation export: Quarto -M uses colon syntax, harden decktape
Build and Deploy Verso / deploy (push) Successful in 8m13s
The standalone-HTML export produced a non-self-contained file (no slide
CSS/JS, math or images when opened away from the server) because Quarto's
--metadata/-M flag uses KEY:VALUE (colon), not KEY=VALUE. '-M
embed-resources=true' silently registered a bogus key and left
embed-resources unset. Switch to colon syntax and also embed MathJax
(self-contained-math:true) so equations render offline.

For the slide PDF, add --disable-dev-shm-usage (the usual cause of
Chromium crashing inside a container with a small /dev/shm), and have the
export controller return the compile log as text/plain on failure so a
failed PDF export shows the real decktape/Chromium error instead of an
HTML page the browser saves as 'pdf.htm'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 07:11:51 +00:00
claude c38e2b8b49 Presentation download menu: standalone HTML + faithful slide PDF (decktape)
Build and Deploy Verso / deploy (push) Failing after 24m2s
In RevealJS mode the download button becomes a 2-choice menu:

- Standalone HTML: a one-off compile with embed-resources (chalkboard and other
  runtime-only plugins are dropped, since they don't survive self-containment),
  yielding a single portable .html.
- Slide PDF: render the deck, then print it with decktape (headless Chromium)
  to a faithful one-slide-per-page PDF.

Implementation:
- Dockerfile-base: install decktape + headless Chromium (open-source; deps via
  playwright install-deps for Ubuntu-Noble correctness). Base-only change.
- QuartoRunner honours options.exportMode ('html-standalone' | 'pdf-slides');
  exportMode is threaded web ClsiManager -> CLSI RequestParser -> CompileManager
  -> runner.
- New GET /project/:id/presentation-export/:format compiles in the matching
  export mode and streams the result as a download (PresentationExportController,
  reusing ClsiManager.getOutputFileStream).
- pdf-hybrid-download-button shows the dropdown when the output is output.html;
  PDF/LaTeX projects keep the single download button.
- i18n: download_as_standalone_html / download_as_pdf_slides (en + fr +
  extracted-translations.json).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 21:00:50 +00:00
claude 899879472e Default the instance to French and translate the Verso-specific strings
Build and Deploy Verso / deploy (push) Successful in 7m35s
- Deployment: set OVERLEAF_SITE_LANGUAGE=fr so the UI defaults to French.
- fr.json: add French translations for the Verso strings — blank_/example_
  {quarto,latex,typst}_project, share_compiled_presentation(_info),
  presentation_link_{members,private,public}, reset_link, and preview (which
  was missing from fr.json). Other untranslated keys keep falling back to
  English via the translations-loader.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 20:04:42 +00:00
claude 28a578ec85 Fix untranslated UI keys (raw "snake_case" labels) + anonymous edit links
Build and Deploy Verso / deploy (push) Successful in 7m46s
The frontend bundles only the locale keys listed in
frontend/extracted-translations.json (a custom webpack translations-loader
filters en.json to that set, normally regenerated by i18next-scanner). Every
key added by hand to en.json without also adding it here renders as its raw
key — which is why "blank_quarto_project", "share_compiled_presentation", etc.
showed up literally in the New-project menu and Share dialog.

Add all introduced keys to extracted-translations.json: blank_/example_
{quarto,latex,typst}_project, share_compiled_presentation(_info),
presentation_link_{members,private,public}, reset_link.

Also enable anonymous read-AND-write share links (edit without an account) via
OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING; read-only links already worked
through OVERLEAF_ALLOW_PUBLIC_ACCESS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 19:40:47 +00:00
claude 4766071e69 Published presentations: three access tiers + per-link reset
Build and Deploy Verso / deploy (push) Successful in 7m57s
Adds a project-members-only link tier and independent link rotation.

- Three tokens per project instead of two: publicToken (anyone), loginToken
  (any logged-in user), memberToken (only users who can read the project).
  serve() resolves the token to its tier and enforces accordingly — 'member'
  requires AuthorizationManager.canUserReadProject.
- New POST /project/:id/publish-presentation/regenerate { tier } rotates a
  single tier's token (invalidating only that old link), leaving the snapshot
  and the other links intact.
- Share dialog now shows three links (members / logged-in / anyone), each with
  its own Copy and Reset buttons; Publish refreshes, Unpublish removes all.
  Preview button opens the logged-in-users link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 18:19:15 +00:00
claude 539cb877b4 Deploy: allow public (anonymous) access so share links work without login
Build and Deploy Verso / deploy (push) Successful in 1m20s
The web service installs a site-wide login gate (router.mjs: webRouter.all('*',
requireGlobalLogin)) whenever Settings.allowPublicAccess is false — which it was,
since OVERLEAF_ALLOW_PUBLIC_ACCESS wasn't set. That gate bounced every anonymous
request to /login, breaking both Overleaf's own link-sharing and the public
presentation links (the dynamic token routes can't be in the exact-match
global whitelist, so there's no per-path exemption — allowPublicAccess is the
intended knob).

Set OVERLEAF_ALLOW_PUBLIC_ACCESS=true on the verso Deployment. Per-project and
per-route authorization still applies, and private presentation links still
require a login (enforced in the serve handler), so only genuinely public
content is reachable anonymously.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 17:50:48 +00:00
claude 4d3ac2b9ea Published presentations: two fixed links (public + private) instead of a toggle
Build and Deploy Verso / deploy (push) Successful in 7m30s
Replace the single token + visibility toggle with two stable tokens per project
pointing at the same snapshot:
  - publicToken  → anyone with the link
  - privateToken → any logged-in Verso user

This fixes both reported issues: changing visibility no longer mutates a link
(there's no toggle — both links always exist), and a public link can never
become private by accident. It also fixes public links redirecting to login:
access is now decided purely by which token was used (public token = open),
not a per-record flag.

- Model: storageId (snapshot dir) + publicToken + privateToken; drop token/
  visibility.
- Manager.publish: mints both tokens once and reuses them on re-publish; serve
  resolves a token to its record and treats the public token as open.
- Controller: returns { publicUrl, privateUrl }.
- Share dialog: shows the private and public links side by side, each with its
  own copy button; Publish refreshes, Unpublish removes. Preview button opens
  the private link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:07:37 +00:00
claude 2cb81bd246 Serve published decks from a trailing-slash URL so assets load
Build and Deploy Verso / deploy (push) Successful in 7m29s
A deck served at /p/:token (no trailing slash) made the browser resolve its
relative asset references (main_files/... CSS+JS) against /p/, 404ing them —
so the deck rendered as unstyled HTML with no reveal.js. Publish links now end
in a slash, and the bare /p/:token URL 301-redirects to /p/:token/, so relative
assets resolve under /p/:token/ and load correctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 15:49:21 +00:00
claude eae5a0ebc7 Store published presentations on the writable data volume
Build and Deploy Verso / deploy (push) Successful in 2m41s
The default published-presentations folder resolved to the app dir
(/overleaf/services/web/data/published), which isn't writable by the runtime
user → EACCES on publish. Point it at the Overleaf data volume in the
production config (Path.join(DATA_DIR, 'published') = /var/lib/overleaf/data/
published), alongside compiles/output, where the app user can write (and which
persists when a volume is mounted). Overridable via PUBLISHED_PRESENTATIONS_PATH.
2026-06-01 15:41:18 +00:00
claude cb0d9ac9fa Fix publish-presentation failing right after an editor compile
Build and Deploy Verso / deploy (push) Successful in 7m49s
CompileManager.compile debounces compiles via a Redis key set on every compile
(_checkIfRecentlyCompiled), returning {status:'too-recently-compiled',
outputFiles:[]} when the editor has just auto-compiled. Publishing called
compile() and then required output.html, so it threw "did not produce an HTML
presentation" — which is why Preview/Publish errored whenever the deck was
freshly compiled.

- CompileManager.compile: honour options.bypassRecentCompileCheck to skip the
  debounce (still runs the normal autocompile-limit guards).
- PublishedPresentationManager: publish with bypassRecentCompileCheck, and put
  the compile status in the error message for diagnosis.
- Controller: catch publish errors, log them, and return the message so the
  Share dialog can show what went wrong instead of a generic error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 15:30:46 +00:00
claude 59055aa67e Publish presentations: share-modal section + Preview button (UI)
Build and Deploy Verso / deploy (push) Successful in 7m53s
Wires the two entry points to the publishing backend:

- Share dialog: a "Share compiled presentation" section (owner only) with a
  public / logged-in-users-only choice, Publish/Unpublish, and a copyable link.
- Top-right toolbar: a "Preview" button that publishes a private (logged-in-
  users-only) link in one click and opens the standalone deck in a new tab
  (opened synchronously to dodge popup blockers).

Both talk to /project/:id/publish-presentation. Reuses existing i18n
(publish/unpublish/copy/preview); adds share_compiled_presentation(_info) and
presentation_link_public/private.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 14:39:39 +00:00
claude 18f9220e73 Publish presentations as standalone shareable links (backend)
Adds the engine + API for publishing a project's compiled HTML/RevealJS deck as
a stable, standalone snapshot served at /p/:token, independent of the editor.

- PublishedPresentation model: one per project { token, visibility, buildId },
  re-publishing keeps the same token so shared links stay stable.
- Manager.publish: compiles the project, then copies the HTML deck + its _files
  assets + referenced media (now included thanks to the OutputFileFinder fix)
  into a persistent snapshot dir (Settings.path.publishedPresentationsFolder,
  override with PUBLISHED_PRESENTATIONS_PATH). Logs/aux are excluded.
- Routes: GET/POST/DELETE /project/:id/publish-presentation (owner/reader) for
  status/publish/unpublish; public GET /p/:token(/*) serves the deck full-page.
  Visibility is enforced in the handler: 'public' = anonymous, 'private' = any
  logged-in Verso user. CSP is dropped on these responses so reveal.js renders.

Frontend entry points (share-modal section + top-right Preview button) follow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 14:34:06 +00:00
claude 9b01fab383 Serve referenced media for HTML/RevealJS output
Build and Deploy Verso / deploy (push) Successful in 7m39s
OutputFileFinder excluded all incoming project resources from the output set,
and OutputCacheManager only copies outputs into the served build dir. For PDF
that's fine (media is embedded), but for HTML/RevealJS the browser fetches
images/videos/fonts from the output path at runtime — so a deck's referenced
image (a project input file) was never served and rendered broken in the
preview.

When the compile produced output.html, keep media inputs (img/video/audio/font
extensions) in the output set so they're served alongside the deck. PDF/LaTeX
compiles are unaffected. This also makes referenced media land in output.zip,
which the upcoming presentation-publishing feature relies on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 14:01:58 +00:00
claude 7c2b903e4d Warn about missing images/videos in Quarto HTML output
Build and Deploy Verso / deploy (push) Successful in 7m49s
Since we dropped --embed-resources (so RevealJS plugins like chalkboard work),
pandoc no longer tries to fetch referenced media for HTML output, so a missing
image or video produces no compile-time warning — it only renders broken in the
browser. PDF/Typst output is unaffected because Typst hard-errors on a missing
image.

After an HTML render, QuartoRunner now scans output.html for local media
references (img/video/audio/iframe src, poster, RevealJS data-background-*) and
appends a `[WARNING] Missing resource: …` line to output.log for any that don't
exist on disk. External URLs, data URIs, anchors and Quarto's own generated
<basename>_files assets are ignored. The [WARNING] prefix is recognised by the
Quarto/Typst log parser, so these show up in the Warnings tab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 13:24:56 +00:00
claude 2d4ca6f13a Fix LaTeX projects failing to compile (HTTP 500, no logs)
Build and Deploy Verso / deploy (push) Successful in 7m44s
Project.compiler defaults to settings.defaultLatexCompiler ('quarto' in this
fork), so every .tex project carried compiler='quarto'. Since the CLSI runner
is chosen by file extension, a .tex root still goes to LatexRunner, whose
_buildLatexCommand threw `unknown compiler: quarto` — surfacing as an opaque
HTTP 500 with no compile log.

- LatexRunner: fall back to pdfLaTeX when the compiler isn't a known TeX engine
  instead of throwing. Universal safety net (covers existing projects, uploads
  and GitHub imports already saved with compiler='quarto').
- ProjectCreationHandler: store a sensible compiler per flavour at creation via
  a shared _flavourConfig helper — blank/example LaTeX → 'pdflatex',
  Typst → 'typst', Quarto → 'quarto' — so the compiler dropdown reflects the
  engine and LatexRunner receives a valid one directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 13:08:57 +00:00
claude d67bc77b0e Add a Typst compiler alongside Quarto and LaTeX
Build and Deploy Verso / deploy (push) Successful in 7m37s
A project whose root file is a .typ file now compiles straight to PDF with
Typst, as a third engine beside Quarto (.qmd) and latexmk (.tex). Dispatch
stays purely extension-based.

CLSI:
- New TypstRunner.js: runs `quarto typst compile <main>.typ output.pdf` (reuses
  the Typst bundled in Quarto, so no extra binary / Docker change). stderr is
  merged into output.log.
- CompileManager: _isTypstFile + a TypstRunner branch in _getRunner, and
  TypstRunner added to the isRunning check and stopCompile kill list.
- RequestParser: 'typst' added to VALID_COMPILERS.

web:
- settings.defaults: 'typ' added to validRootDocExtensions and the text
  extensions (so .typ opens in the editor); 'typst' added to safeCompilers.
- output-files: the Quarto/Typst log parser (which already understands Typst
  `error:`/`warning:` + `┌─ file:line:col` diagnostics) now also handles .typ
  compiles, so their errors/warnings populate the log tabs.

Polish:
- New-project menu: "Blank Typst project" + "Example Typst project" in both the
  main and welcome dropdowns, backed by createBasicProject/createExampleProject
  flavour 'typst', a new mainbasic.typ template and an example-project-typst
  presentation (math, an image, a table, lists).
- Compiler dropdown gains a "Typst" option (cosmetic; dispatch is by extension).

README updated: three compilers side by side, with a Writing-a-Typst-document
section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 12:56:30 +00:00
claude 2a9c4cfe81 New-project menu: split into Quarto and LaTeX blank/example options
Build and Deploy Verso / deploy (push) Successful in 7m44s
Replace the generic "Blank project" / "Example project" entries with four
flavour-specific ones in both the New-project dropdown and the welcome-screen
dropdown:

- Blank Quarto project   -> empty main.qmd (format: typst)
- Blank LaTeX project    -> empty main.tex
- Example Quarto project -> a Reveal.js presentation showcasing images, math,
  a table, code and incremental lists (new template
  project_files/example-project-quarto/)
- Example LaTeX project  -> the existing LaTeX example

Backend: ProjectController.newProject now dispatches the `template` value
(blank_quarto/blank_latex/example_quarto/example_latex, plus the legacy
'example'/'none') to createBasicProject(flavour) / createExampleProject(flavour).
_createRootDoc takes a root-doc name so each flavour gets the right extension —
this also fixes the LaTeX example, whose root doc was wrongly created as
main.qmd, back to main.tex (matching the acceptance test). Signatures stay
backward compatible (flavour defaults: blank=quarto, example=latex).

Also refresh the README: Verso now runs Quarto and LaTeX side by side
(engine chosen by root-file extension), not Quarto instead of LaTeX.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 12:30:07 +00:00
claude 3e10d1c4ee Recolour remaining green brand-accent surfaces to the accent token
Build and Deploy Verso / deploy (push) Successful in 7m50s
The $accent knob caught primary buttons, but several places still
referenced the green ramp directly as a brand-accent colour (rather than
genuine success semantics). Repoint those at the --bg-accent-* tokens so
they too follow the single $accent knob:

- navbar Sign in / Register ("primary" + subdued/link hover) buttons
- file-tree selected-item highlight and drag background (IDE redesign,
  light and dark)
- document-outline highlighted item (IDE redesign, light and dark)
- the Visual/Code editor-switcher button mixin
- web/content hyperlinks (--link-web*), e.g. on the project dashboard;
  dark-theme variants point at the blue ramp to stay readable on dark

Genuine success/positive greens (notification success icon,
$content-positive, beta badges, etc.) are deliberately left green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:49:12 +00:00
claude b3541ba6f3 CI: skip unchanged base image and add registry build cache
Build and Deploy Verso / deploy (push) Successful in 12m40s
Two build-speed changes to the Gitea Actions deploy workflow.

(#1) Build the base image only when it changes. The base layers' only
repo input is server-ce/Dockerfile-base, so the prepare step hashes that
file and the base is tagged verso-base:base-<hash>; the app builds FROM
that exact tag. If a base with the current hash already exists in the
registry, the heavy base build (apt ~111s, TeX Live ~51s, Quarto, plus
its ~49s export/push) is skipped entirely — which is every commit that
doesn't touch Dockerfile-base.

(#2) Import/export a registry-backed layer cache (verso-cache:base and
verso-cache:app, mode=max) on both builds. Unchanged layers are reused
instead of rebuilt: yarn install is skipped when package.json is
unchanged, and only the web compile re-runs on a frontend source change.
No new cluster resources — the cache lives as extra tags in the same
in-cluster registry.

First run after this is still a full build (populates the caches and the
hash-tagged base); subsequent commits should be substantially faster.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:30:20 +00:00
claude aa3fb56458 Parse Quarto logs and make the accent colour a single knob
Build and Deploy Verso / deploy (push) Successful in 11m55s
Quarto compiles (.qmd/.md/.Rmd, dispatched to QuartoRunner) write
Typst/Pandoc/Quarto diagnostics to output.log that the LaTeX log parser
does not understand, so the Errors/Warnings tabs stayed empty. Add a
dedicated quarto-log-parser that recognises Typst `error:`/`warning:`
(+ `┌─ file:line:col`), Pandoc `[WARNING]`/`[ERROR]`, Quarto CLI/Deno
`ERROR:`/`WARNING:`, and knitr `Quitting from lines`. handleLogFiles now
routes to it when the root file is a Quarto file (mirrors CLSI dispatch),
otherwise the LaTeX path is unchanged.

Also decouple the UI accent from the green ramp. The framework already
funnels every primary/accent surface (primary buttons, Bootstrap
$primary/$success, --btn-primary-background) through the --bg-accent-*
tokens; those just happened to point at Overleaf green. Introduce a
single $accent knob in foundations/colors.scss (with auto-derived
hover/tint shades) and repoint the accent tokens at it, defaulting to
the Verso/Quarto blue. Re-skinning the whole UI is now a one-line edit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:57:06 +00:00
claude e87bbfe5b0 HTML preview: drop embed-resources and clear stale deck on failure
Build and Deploy Verso / deploy (push) Successful in 12m5s
Two HTML/RevealJS preview fixes:

1. Stop passing --embed-resources to quarto render. A self-contained
   single-file HTML breaks reveal.js plugins that load/store resources at
   runtime (chalkboard, multiplex) and is slow to transfer. Quarto now
   emits the HTML plus a sibling "<basename>_files/" asset dir referenced
   by relative paths; both are served from the same .../output/ path
   (nginx output/(.+) and web :file(.*) both capture slashes), so the
   relative links resolve. The renamed output.html still points at the
   unchanged "<basename>_files" dir. This also fixes the slow-load issue,
   since assets now load on demand instead of one giant inlined file.

2. On a failed compile that follows a successful one, the previous deck
   stayed in the iframe, making the failure look like a success. We now
   clear pdfFile when a non-success status carries a stale output.html.
   The last-good-PDF-beside-the-error behaviour is preserved for PDF
   output (only output.html is dropped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 08:40:18 +00:00
claude 56d66b109e Outline: ignore YAML frontmatter and strip Quarto attribute blocks
Build and Deploy Verso / deploy (push) Successful in 12m25s
Two fixes to the Markdown/Quarto file outline:

1. The last frontmatter line (e.g. `format: typst`) appeared as a
   heading. The Lezer Markdown grammar has no frontmatter support, so it
   reads the closing `---` of the YAML block as a Setext underline and
   promotes the line above it to a heading. Detect the leading
   `---`...`---`/`...` block and skip any heading inside it.

2. Pandoc/Quarto attribute blocks were shown in titles, e.g.
   `## Slide {.smaller auto-animate="true"}`. Strip a trailing `{...}`
   from the extracted title.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 08:30:15 +00:00
Eric Mc Sween 31fbc3daee Merge pull request #34128 from overleaf/em-library-analytics
Add analytics events to the account-level library page

GitOrigin-RevId: d0357f37a89ec29cca5b6b375a9553fbdf021b00
2026-06-01 08:05:02 +00:00
Gernot Schulz a0ca344065 Merge pull request #34127 from overleaf/gs-j-cd-hooks
Add deploy pipeline trigger hooks to Jenkinsfiles

GitOrigin-RevId: 80bb89615ae16b733009dca21a5fc41b5c30e993
2026-06-01 08:04:55 +00:00
Malik Glossop 54e122610e Merge pull request #34100 from overleaf/mg-fix-style
Stop inherited color overriding active list group item colour

GitOrigin-RevId: 7e36c2129661b4582658a5ccd9edfb15f12e701c
2026-06-01 08:04:52 +00:00
Domagoj Kriskovic 10ef1d0f34 [web] Fix empty lines being invisible in Python script output
GitOrigin-RevId: eb4b732cae74fa050384fd4cec6bd96a9caae152
2026-06-01 08:04:45 +00:00
claude b6c1a2d5ce Fix empty titles in Markdown/Quarto file outline
Build and Deploy Verso / deploy (push) Successful in 12m13s
The outline entries showed up at the right lines and jumped correctly,
but their titles were blank. The text-extraction walked the heading
node's children and collected non-HeaderMark child text — but in the
Lezer Markdown grammar a heading has NO child node for its text; the
only children are the HeaderMark nodes. The title text lives in the
gaps between marks, so the walk collected nothing.

Slice the whole heading's source instead and strip the markers:
leading/trailing '#'s for ATX headings and the '==='/'---' underline
for Setext headings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 08:04:42 +00:00
Domagoj Kriskovic 987b3a1f71 Track script-runner-opened analytics event
GitOrigin-RevId: fb95aa2f5ad649061a6b8e9797789024a3345f3b
2026-06-01 08:04:41 +00:00
domagojk 270cbaf84e Move python labs icon next to run button
Closes #33892

GitOrigin-RevId: c48d920ee982ddd5e4295fc1279b0f70096820d1
2026-06-01 08:04:37 +00:00
Jakob Ackermann 3c763015ce [monorepo] consolidate clsi-lb host/ip env-vars (#33894)
* [monorepo] consolidate clsi-lb host/ip env-vars

Target env-var is CLSI_LB_HOST. Keep CLSI_LB_IP populated for a week.

* [monorepo] sort dev-environment.env hosts

GitOrigin-RevId: 95d12753c86ffb91264f8971e1c2c412c60de790
2026-06-01 08:04:31 +00:00
Olzhas Askar b5a73efaeb Merge pull request #34060 from overleaf/oa-timeout-cta
[web] Compile timeout CTA

GitOrigin-RevId: c1dd014150964ffec1b556943f572d3e5a8069ce
2026-06-01 08:04:24 +00:00
claude 5f761c1772 Use minimal scheme-basic TeX Live install (small, fast, reversible)
Build and Deploy Verso / deploy (push) Has been cancelled
Reverts the heavy multi-collection texlive install back toward the
original upstream-Overleaf approach: install-tl with scheme-basic
(~300 MB) plus latexmk and texcount via tlmgr, no docfiles/srcfiles.
This restores the fast, small base image we had before LaTeX support
was added in full.

Tradeoff: documents needing tikz/beamer/siunitx/extra fonts won't
compile out of the box for now — those should stay in Quarto/Typst
until the project is mature enough to justify a full TeX Live.

Made deliberately easy to reverse: a header comment documents that
switching scheme-basic -> scheme-full (one line) restores the complete
toolchain, or individual packages can be appended to the tlmgr list.
Uses TEXDIR=/usr/local/texlive (unversioned) so PATH stays stable
across TeX Live releases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 08:01:07 +00:00
claude 4800a51957 Use a curated TeX Live set instead of near-full to speed up builds
Build and Deploy Verso / deploy (push) Successful in 14m42s
The previous install expanded texlive-full (minus -doc/-lang-), pulling in
essentially every CTAN package plus inkscape's large GTK GUI tree — ~20 min
and several GB. Replace it with a curated set of meta-packages that covers
the vast majority of documents: latex base/recommended/extra, recommended
fonts, plain-generic, science (math/physics), xetex, luatex, bibtex-extra,
extra-utils (texcount), plus latexmk/biber/chktex/pygments.

Smaller and faster to build. Documents needing an omitted package can have
the relevant texlive-* collection added back. Drops inkscape (only used for
auto SVG->PDF conversion) to avoid its heavy GUI dependency chain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 21:52:56 +00:00
claude 7c86657548 CI: pull deploy image via public registry address
Build and Deploy Verso / deploy (push) Has been cancelled
The cluster nodes' containerd can only pull from registry.alocoq.fr, not
the in-cluster service name. Keep pushing via the in-cluster address (to
bypass the Traefik upload-timeout), but reference registry.alocoq.fr/verso
in the test Deployment and the rolling update. Both addresses front the
same registry storage, so the pushed image resolves at the public name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 21:30:45 +00:00
claude 3af4e2f46a CI: write buildkitd.toml in-container instead of a ConfigMap
Build and Deploy Verso / deploy (push) Failing after 25m12s
The previous approach created a verso-buildkitd-config ConfigMap, but the
workflow's RBAC does not permit creating new cluster resources. Write the
buildkitd.toml (marking the in-cluster registry as http/insecure) directly
inside the buildkit container at runtime via printf, and drop the configMap
volume/mount. No new k8s resources are created.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 21:01:16 +00:00
claude 8f2f6d1684 CI: push images to in-cluster registry to bypass Traefik
Build and Deploy Verso / deploy (push) Failing after 1s
The TeX Live layer (~3.5 GB) failed to push to registry.alocoq.fr:
Traefik severed the upload mid-stream ("client disconnected during blob
PUT ... unexpected EOF"), buildkit retried at the wrong offset, and the
registry returned "blob upload invalid".

Push to the in-cluster registry Service (registry.git.svc.cluster.local:5000)
instead, so the upload never traverses Traefik. Changes:
- buildctl outputs use registry.insecure=true (registry is plain HTTP)
- add a verso-buildkitd-config ConfigMap with buildkitd.toml marking the
  registry http/insecure, so the second build can pull the base image back
- the verso Deployment and rolling update reference the in-cluster image

NOTE: the cluster nodes' containerd must also treat
registry.git.svc.cluster.local:5000 as an insecure registry, otherwise
the kubelet image pull for the test deployment will fail. That is node-
level config outside this repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:46:30 +00:00
claude 3bb293f7a7 Fix TeX Live install: texcount is not a standalone package
Build and Deploy Verso / deploy (push) Has been cancelled
The base image build failed with "E: Unable to locate package texcount".
texcount ships inside texlive-extra-utils, not as its own apt package.
Replace the bogus texcount entry with texlive-extra-utils (which provides
both texcount and latexmk). latexmk is kept explicit for clarity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 19:54:51 +00:00
claude 2ae860a1a8 Raise upload limit from 50 MB to 500 MB
Build and Deploy Verso / deploy (push) Has been cancelled
Both limits that gate uploads are bumped in tandem so they don't conflict:
- settings.defaults.js maxUploadSize: 50 MB → 500 MB (app-level check)
- nginx.conf.template client_max_body_size: 50m → 500m (proxy body limit)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 19:38:21 +00:00
claude 422ac30e6c Support LaTeX and Quarto compilation in parallel
Build and Deploy Verso / deploy (push) Has been cancelled
Verso now compiles both .tex (latexmk) and .qmd (Quarto) projects,
dispatching by the root file's extension rather than replacing one with
the other. LaTeX and Quarto projects can coexist on the same server.

CompileManager: re-import LatexRunner and add a _getRunner() dispatcher
  that returns a uniform {run, isRunning, kill} interface. .qmd/.md/.Rmd
  → QuartoRunner; everything else (.tex/.ltx/.Rtex/.Rnw) → LatexRunner.
  stopCompile now checks/kills both runners since it has no root path.

compiler-setting.tsx: restore the LaTeX engine choices (pdfLaTeX, LaTeX,
  XeLaTeX, LuaLaTeX) alongside Quarto. The dropdown still controls which
  TeX engine latexmk uses; actual engine dispatch is by file extension.

Dockerfile-base: reinstall TeX Live alongside Quarto (texlive-full minus
  -doc/-lang- packages, plus xetex/luatex/biber/latexmk/texcount/chktex/
  synctex). Restore TEXMFVAR for a writable LuaTeX cache. This brings back
  a large image, which is the accepted cost of full LaTeX+Quarto support.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 19:27:39 +00:00
claude a89b8bd282 Enable gzip for HTML/CSS/JS output in clsi-nginx
Build and Deploy Verso / deploy (push) Successful in 11m13s
RevealJS presentations are served as (currently embed-resources) HTML that
went over the wire uncompressed, because gzip_types only listed text/plain.
This made the HTML preview slow to load for heavy decks.

Add text/html, text/css, application/javascript, application/json and
image/svg+xml to gzip_types so the text-based portion of the output is
compressed. Already-compressed formats (pdf, png/jpeg/webp, woff/woff2)
are intentionally excluded to avoid wasting CPU. Also set gzip_min_length
1024 so tiny responses aren't compressed needlessly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 18:52:39 +00:00
claude a241e2c201 Pre-install popular Quarto extensions in the Docker image
Build and Deploy Verso / deploy (push) Successful in 11m22s
Dockerfile-base: after Quarto is installed, run 'quarto add --no-prompt'
  for a curated set of extensions into /opt/quarto-extensions/. Quarto
  writes _extensions/<author>/<name>/ in the working dir, giving us a
  clean shared store. Extensions included:
    - igorlima/charged-ieee      — IEEE paper format (Typst)
    - quarto-ext/fontawesome     — Font Awesome icons
    - quarto-ext/attribution     — attribution footer on RevealJS slides
    - quarto-ext/pointer         — laser pointer for presentations
    - quarto-ext/drop            — drop-down overlay for RevealJS
  Adding more: one extra '&& quarto add --no-prompt <author>/<repo>' line.

QuartoRunner: before quarto render, merge /opt/quarto-extensions/_extensions/
  into the compile dir's _extensions/ with 'cp -rn' (no-clobber). This
  makes all pre-installed extensions available to every project without
  any user action. Project-uploaded _extensions/ files take precedence
  since cp -n never overwrites existing files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 16:54:56 +00:00
claude 4460c1d9d6 Fix README copyright: Aloïs Coquillard, 2026
Build and Deploy Verso / deploy (push) Successful in 10m53s
2026-05-31 16:37:59 +00:00
claude 0407e17c68 Rewrite README for Verso
Build and Deploy Verso / deploy (push) Has been cancelled
Replace Overleaf's original README with a Verso-specific one covering:
the project's purpose (collaborative Quarto editor), output formats
(typst/PDF and revealjs/HTML), quick-start Docker instructions, service
architecture overview, a minimal .qmd example, key env vars, and a
clear description of what differs from upstream Overleaf.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 16:30:17 +00:00
claude 24cd4bf13d Fix HTML compile success check: size is undefined for non-PDF outputs
Build and Deploy Verso / deploy (push) Has been cancelled
collectOutputPdfSize() only calls stat() and sets .size on output.pdf.
All other output files (including output.html) keep size: undefined.
The previous check required file.size > 0 for both PDF and HTML, so
undefined > 0 always evaluated false for output.html, making every
RevealJS compile report 'failure' even when the file was produced.

Fix: require size > 0 only for output.pdf; accept output.html
regardless of size (it is always non-empty if Quarto succeeded).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 16:27:02 +00:00
claude 090018c191 Fix QuartoRunner mv: use relative paths to avoid $COMPILE_DIR replacement bug
Build and Deploy Verso / deploy (push) Successful in 11m1s
LocalCommandRunner.replace() uses String.replace() which only substitutes
the FIRST occurrence of '$COMPILE_DIR' in the shell script string. The mv
commands had two more occurrences that stayed as literal '$COMPILE_DIR',
which the shell expanded to '', making 'mv /main.pdf /output.pdf' fail
silently. The file was produced (Quarto logged 'Output created: main.pdf')
but never renamed to output.pdf, so the pipeline reported failure.

Fix: mv uses relative filenames since the shell CWD is already the compile
directory (set by LocalCommandRunner via the spawnCwd option).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 15:51:01 +00:00
claude 48fd24a6b2 Add HTML/RevealJS preview alongside existing PDF preview
Build and Deploy Verso / deploy (push) Successful in 11m0s
clsi-nginx.conf: the types{} block was overriding all nginx defaults,
  leaving HTML/CSS/JS/fonts as application/octet-stream. Add the full
  set of web MIME types so RevealJS assets are served correctly. Also
  needed for X-Content-Type-Options: nosniff to pass.

CompileController.js: success was hardcoded to require output.pdf.
  Also accept output.html so a RevealJS compile is reported as
  'success' rather than 'failure'.

QuartoRunner.js: remove hardcoded --to typst --output output.pdf.
  Instead run `quarto render` without --to/--output so the YAML
  frontmatter decides the format (typst → PDF, revealjs → HTML, etc.).
  Pass --embed-resources so HTML output is self-contained (flag is
  silently ignored by the typst backend). After render, rename
  main.pdf → output.pdf or main.html → output.html so the pipeline
  finds the standard canonical filename.

output-files.ts: handleOutputFiles now falls back to output.html when
  output.pdf is absent. Download URL uses outputFile.path instead of
  the hardcoded 'output.pdf' string.

pdf-viewer.tsx: when pdfUrl contains output.html, bypass PDF.js
  entirely and render a sandboxed iframe (allow-scripts for RevealJS
  interactivity, allow-presentation for fullscreen).

Usage: set `format: revealjs` in the .qmd YAML frontmatter to get
  an HTML presentation preview; set `format: typst` for PDF.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 15:32:00 +00:00
claude 141cf95f9e Apply Verso brand identity: Quarto palette, logo, UI text
Build and Deploy Verso / deploy (push) Successful in 10m58s
Color palette: introduce Quarto's five brand colours ($verso-blue
  #447099, $verso-blue-dark #1B3B6F, $verso-blue-light #75AADB,
  $verso-green #72994E, $verso-orange #EE6331) as CSS custom
  properties alongside the existing layout vars.

Logo: replace all Overleaf SVG assets (icon, wordmarks, favicons,
  horizontal logos) with the Verso mark — a circle split into four
  Quarto-coloured quadrants (Quarto DNA) with a bold white V
  letterform (Verso identity). Filenames kept so imports stay intact.
  Status favicons keep their layout; brand green #046530#447099.

UI text:
  - appName / nav.title default → 'Verso'
  - Footer copyright → '© Verso'; remove Overleaf social links;
    thin-footer attribution → 'Built on Overleaf' (with OSS link)
  - mask-icon colour → #447099
  - interstitial logo alt → 'Verso'
  - Key locale strings (welcome, agree terms, go-to) → Verso;
    SaaS-specific strings (subscriptions, AI Assist) left as-is
    since CE users never see them

Env var names (OVERLEAF_*) intentionally untouched to avoid breaking
  the build. Code comments citing Overleaf origin preserved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 15:06:36 +00:00
claude 1e5ce6c068 Add document outline support for Markdown/Quarto files
Build and Deploy Verso / deploy (push) Successful in 10m47s
outline.ts: export NestingLevel so it can be used outside the file.

markdown/document-outline.ts: new enterMarkdownNode function that walks
  the Lezer Markdown syntax tree and extracts ATXHeading1-6 and
  SetextHeading1-2 nodes, mapping them to the same NestingLevel enum
  used by the LaTeX outline (Section→SubSection→SubSubSection…).
  Wrapped in makeProjectionStateField for incremental updates.

markdown/index.ts: register markdownDocumentOutline as a CodeMirror
  extension in the Markdown LanguageSupport so the StateField is active
  whenever a .qmd file is open.

codemirror-outline.tsx: fall back to markdownDocumentOutline when the
  LaTeX documentOutline StateField is not present in the editor state
  (i.e. when the active language is Markdown, not LaTeX).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 14:35:01 +00:00
claude ce0572e01e Revert QuartoRunner: restore --output output.pdf
Build and Deploy Verso / deploy (push) Successful in 11m9s
The previous compile logs confirm Quarto handles --to typst --output
output.pdf correctly: pandoc produces main.typ, typst compiles it to
main.pdf, then Quarto renames to output.pdf. The mv-based approach was
unnecessary and incorrect.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 14:11:03 +00:00
claude 824b873c69 Fix QuartoRunner: drop --output flag to let Quarto run full typst→PDF pipeline
Build and Deploy Verso / deploy (push) Failing after 12m18s
--to typst combined with --output output.pdf caused Quarto to write a
Typst source file (.typ content) named output.pdf instead of invoking
the typst compiler, producing a text file that the PDF viewer could not
render (hence 'markdown not rendered' — it was literally showing the raw
.typ markup). Fix: let Quarto name the PDF after the input file
(main.qmd → main.pdf) and rename to output.pdf with mv afterwards.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 14:08:08 +00:00
claude b2b2ed13aa Fix Quarto cache permission: create /var/www/.cache/quarto for www-data
Build and Deploy Verso / deploy (push) Successful in 10m56s
Quarto resolves its cache dir as $HOME/.cache/quarto. The process runs
as www-data (home=/var/www) but that directory is root-owned, so Quarto
crashed immediately with PermissionDenied on mkdir. Pre-create the cache
dir and chown it to www-data at image build time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 13:39:02 +00:00
claude 09f5329a07 Fix compile error propagation: 'failure' instead of HTTP 500
Build and Deploy Verso / deploy (push) Successful in 10m37s
LocalCommandRunner: attach captured stdout to the error object when
  exit code is 1, so callers can read Quarto's output even on failure.

QuartoRunner: stop propagating plain 'exited' errors from Quarto up
  to CompileManager. A Quarto exit-code-1 is a compile failure, not a
  server error — CLSI already detects failure by the absence of
  output.pdf and returns status='failure' (HTTP 200). Previously it
  fell through to the generic error handler (HTTP 500), which caused
  the frontend to show "Server Error" instead of the log panel.
  Only true process-level errors (terminated, timedout) are propagated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 13:22:58 +00:00
claude 0323fd4813 Fix Quarto compile pipeline: install, logs, template, draft mode
Build and Deploy Verso / deploy (push) Successful in 10m58s
Dockerfile-base: remove TeX Live (no longer needed), install Quarto
  1.6.39 which bundles Typst for PDF output. This was the root cause
  of all compile failures — the server-ce monolith never had Quarto.

QuartoRunner: run quarto via /bin/sh so stderr is merged into stdout
  with 2>&1; write combined output to output.log (not output.stdout)
  so the PDF-preview log panel picks it up and shows raw output.
  Also write the log on error so failures are always visible.

CompileManager: guard DraftModeManager behind an isLatexFile check —
  injecting LaTeX preamble commands into a .qmd file corrupts it and
  causes a guaranteed compile failure when draft mode is requested.

ProjectCreationHandler + mainbasic.qmd: new projects now create
  main.qmd with a minimal Quarto/Typst frontmatter instead of the
  LaTeX main.tex; _createRootDoc names the file main.qmd accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 12:59:43 +00:00
claude af54b5fd49 Fix Quarto compiler integration: UI, root doc detection, type safety
Build and Deploy Verso / deploy (push) Successful in 11m58s
- compiler-setting.tsx: replace hardcoded LaTeX compiler list with a
  single Quarto option; drop now-unused getMeta/lodash imports
- project-settings.ts: add 'quarto' to ProjectCompiler union type
- ClsiManager: detect main.qmd as a default root document (preferred
  over main.tex); replace hasMainFile boolean with detectedMainFile
  so we know which filename to use
- settings.defaults.js: add 'qmd' to validRootDocExtensions so .qmd
  files appear as selectable root documents in the UI
- ProjectRootDocManager: sort main.qmd before main.tex in the root
  doc candidate list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 12:29:12 +00:00
claude 5b5a54f7b1 Merge branch 'ai/quarto-investigation': replace LaTeX with Quarto
Build and Deploy Verso / deploy (push) Successful in 12m28s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 12:08:11 +00:00
claude a7c2403c4a Replace LaTeX compiler with Quarto (typst PDF output)
- Add QuartoRunner.js: runs `quarto render --to typst --output output.pdf`,
  using Typst (bundled with Quarto >= 1.4) so no separate LaTeX install needed
- Swap LatexRunner for QuartoRunner in CompileManager; remove latexmk-specific
  stats, fdb metrics, and performance sampling that no longer apply
- Add 'quarto' to VALID_COMPILERS in RequestParser and set it as the default;
  change default rootResourcePath from main.tex to main.qmd
- Add 'quarto' to safeCompilers and set it as the default in web settings
- Replace with-texlive Dockerfile stage with with-quarto (Quarto deb install);
  add Quarto to the default final stage as well

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 12:08:07 +00:00
alois d52821e7cc Actualiser .gitea/workflows/deploy-verso.yml
Build and Deploy Verso / deploy (push) Successful in 9m49s
2026-05-30 15:00:42 +00:00
alois d3a9259b42 Actualiser .gitea/workflows/deploy-verso.yml
Build and Deploy Verso / deploy (push) Failing after 9m59s
2026-05-30 14:23:03 +00:00
alois a2db1f04be Actualiser server-ce/Dockerfile
Build and Deploy Verso / deploy (push) Has been cancelled
2026-05-30 12:08:40 +00:00
alois 070d0c1352 Actualiser .gitea/workflows/deploy-verso.yml
Build and Deploy Verso / deploy (push) Has been cancelled
2026-05-30 12:01:00 +00:00
alois 016da98027 Actualiser .gitea/workflows/deploy-verso.yml
Build and Deploy Verso / deploy (push) Failing after 17m13s
2026-05-30 10:54:21 +00:00
alois 1aae2fb3fc switch to minimal dockerfile
Build and Deploy Verso / deploy (push) Has been cancelled
2026-05-30 08:59:24 +00:00
alois 12290c5d93 Ajouter server-ce/Dockerfile-base-minimal
Build and Deploy Verso / deploy (push) Has been cancelled
2026-05-30 08:54:39 +00:00
alois 85ecaf1ff6 Actualiser .gitea/workflows/deploy-verso.yml
Build and Deploy Verso / deploy (push) Successful in 9m57s
2026-05-29 21:57:51 +00:00
alois 3b8879b9ca Actualiser .gitea/workflows/deploy-verso.yml
Build and Deploy Verso / deploy (push) Successful in 9m39s
2026-05-29 20:58:02 +00:00
alois f341d6e64d Actualiser .gitea/workflows/deploy-verso.yml
Build and Deploy Verso / deploy (push) Successful in 9m39s
2026-05-29 20:33:30 +00:00
alois ecbf7d4bdc Actualiser .gitea/workflows/deploy-verso.yml
Build and Deploy Verso / deploy (push) Successful in 9m38s
2026-05-29 19:39:37 +00:00
alois ba5d159f28 Fix dockerfile ARG
Build and Deploy Verso / deploy (push) Has been cancelled
2026-05-29 20:40:57 +02:00
alois a851f53005 Merge branch 'main' of https://git.alocoq.fr/alois/verso
Build and Deploy Verso / deploy (push) Has been cancelled
2026-05-29 20:26:46 +02:00
alois e4db4fe458 Fix corepack yarn install for build 2026-05-29 20:22:40 +02:00
alois 4424286dfd Actualiser .gitea/workflows/deploy-verso.yml
Build and Deploy Verso / deploy (push) Has been cancelled
2026-05-29 18:01:08 +00:00
alois 0771eeab43 Actualiser .gitea/workflows/deploy-verso.yml
Build and Deploy Verso / deploy (push) Failing after 1s
2026-05-29 17:58:05 +00:00
alois 3fe806fc0e Actualiser .gitea/workflows/deploy-verso.yml
Build and Deploy Verso / deploy (push) Has been cancelled
2026-05-29 17:46:19 +00:00
alois cc0db0813f Ajouter .gitea/workflow/deploy-verso.yml 2026-05-29 17:43:06 +00:00
Miguel Serrano f07212337f [web] Replace pro with commons wording on institutional subscript… (#34078)
* [web] Replace `pro` with `commons` wording on institutional subscriptions

Replaces the wording in several places:
- subscription settings
- email tags
- features tooltip
- institution portal

GitOrigin-RevId: 1b9a0e51245ed8a41865300d9e9d555bc05e6c17
2026-05-29 08:06:32 +00:00
Miguel Serrano 63852c5934 [web] bump js-yaml in reference-parser (#33953)
* [web] bump `js-yaml` in `reference-parser`

`.yarn/patches/referer-parser-npm-0.0.3.patch` bumps the `js-yaml` dependency,
but yarn patches don't take that into account the patched package.json for dependency resolution.

* Add RequestHelper test

GitOrigin-RevId: 8246f8ab54956897cc361d7c02b65e5363ad43ec
2026-05-29 08:06:27 +00:00
Andrew Rumble 05895dc0dc Merge pull request #34073 from overleaf/mj-remove-error-log
[web] Stop logging handled errors

GitOrigin-RevId: 7a60a576032a0dd389ef200e22c860b2ea9e8ed8
2026-05-29 08:06:02 +00:00
Andrew Rumble bb7643f697 Merge pull request #33482 from overleaf/ar-mixpanel-labs-project
[web/analytics] Send labs user's events to separate mixpanel project

GitOrigin-RevId: 42612b71d2d7a082ffbe1ff614499a0b94553b90
2026-05-29 08:05:57 +00:00
Kristina 5f7e81aafc [web] add stats to process_notifications cron (#34049)
GitOrigin-RevId: ea6890f2726cba268f1e5eead0643d03757b8dff
2026-05-29 08:05:42 +00:00
Mathias Jakobsen 5f1a71580b Merge pull request #34072 from overleaf/mj-toolbar-borders
[web] Add borders to dropdowns in editor toolbar

GitOrigin-RevId: e6199736559f755bde79341d78e6d8cd2d4c1ca1
2026-05-29 08:05:38 +00:00
Tom Wells fa2b70ffc6 Shared CSS cleanup, empty state polish, fix translation (#33998)
GitOrigin-RevId: eadaec851774b51912d45d18e7efca9981122628
2026-05-29 08:05:33 +00:00
Kate Crichton 54385384b9 Merge pull request #34053 from overleaf/kc-domain-capture-active-labels
[web] replace Capture column icon with Active/Inactive badges

GitOrigin-RevId: 1986a703b24f1d648bd054ce7def04bccb0007ea
2026-05-29 08:05:21 +00:00
Kate Crichton 0630245bb6 Merge pull request #34029 from overleaf/kc-domain-capture-status-note
[web] Add domain capture status note to group settings

GitOrigin-RevId: dbe6a67d088c3e19207ed7bee127d2b33ff9fdcc
2026-05-28 08:08:22 +00:00
Kate Crichton 3677e5d08f Merge pull request #33673 from overleaf/kc-pending-verification-notification
[web] verification notification updates

GitOrigin-RevId: 29c4284b4de4e150c021a25e5f485312b1b37dc7
2026-05-28 08:08:18 +00:00
Mathias Jakobsen 51ca5c0156 Merge pull request #33972 from overleaf/mj-web-show-pandoc-error
[web] Expose conversion errors during project exports

GitOrigin-RevId: 2e808bd65f03e81405db4727f2f5773d3b14cbe7
2026-05-28 08:08:14 +00:00
Jakob Ackermann e7a202a0bf [clsi] fix duplicate import of Errors module (#34068)
GitOrigin-RevId: e1dec5e5d439e5d178f9f7400c0873e0be6e90f8
2026-05-28 08:08:10 +00:00
Jakob Ackermann 7e4820f0b0 [clsi] migrate convert project to document to compile from history (#33985)
* [clsi] add request flag for isCompileFromHistory

* [clsi] derive cacheKey for history snapshot from compile dir

* [clsi] migrate convert project to document to compile from history

* [clsi] address review feedback

* [web] determine root doc at the time of converting the project

* [web] wait for flush before starting document conversion

* [saas-e2e] add tests for root doc override when converting project

GitOrigin-RevId: 71c578030949b89f3a74e7f7ab882dfa9c98c17a
2026-05-28 08:08:06 +00:00
Jakob Ackermann 666788be70 [clsi] nginx: use a fixed project id for the content domain access check (#33291)
GitOrigin-RevId: 7801fa001e42b1b96d851f74efff396bc6471980
2026-05-28 08:08:02 +00:00
Mathias Jakobsen 68e9572fbb [clsi] Forward pandoc errors to web (#33971)
* [clsi] Forward pandoc errors to web

* [clsi] Remove unused import

* [clsi] Align warning logs

* [clsi] Update HTTP response for errors

* [clsi] Update acceptance test with 422

* [clsi] Always return json body on 422

* [clsi] Include stderr in logs for non user facing errors

GitOrigin-RevId: 4284c8d4e8b7b45eac4997cd9e52ca4894b20412
2026-05-28 08:07:58 +00:00
Lucie Germain 5e47353ad4 [Security upgrade] Pin @babel/plugin-transform-modules-systemjs to 7.29.4 via resolutions (GHSA-fv7c-fp4j-7gwp) (#33650)
* Pin @babel/plugin-transform-modules-systemjs to 7.29.4 via resolutions (GHSA-fv7c-fp4j-7gwp)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix missing comma in package.json resolutions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jakob Ackermann <jakob.ackermann@overleaf.com>
GitOrigin-RevId: d6f3e72234d64fd0afb8676b8652cc03b0cddbe0
2026-05-28 08:07:54 +00:00
Lucie Germain a4e7d90cf1 [Security upgrade] pin js-cookie to 3.0.7 (#33960)
* pin js-cookie to 3.0.7 in root yarn.lock

* drop unnecessary js-cookie resolution; ^3.0.5 already permits 3.0.7

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
GitOrigin-RevId: e7803a04aa76daea574e6b1e67b3d6c42134945e
2026-05-28 08:07:49 +00:00
Copilot 016296cc07 web: add info/cause support to BackwardCompatibleError (OError-compatible) (#33766)
GitOrigin-RevId: 736ca3245f85f14df5a01e5c4a342b99742118e8
2026-05-28 08:07:29 +00:00
Andrew Rumble 9daa56becc Merge pull request #33079 from overleaf/ar-remove-request-from-clsi
[clsi] remove request library

GitOrigin-RevId: 4690c476157fc2829e516d91f688f9424f8c162f
2026-05-28 08:07:11 +00:00
Andrew Rumble 26b9d029f3 Merge pull request #33708 from overleaf/jlm-stripe-stage5-fixes
Stripe migration improvements from stage 5

GitOrigin-RevId: 897941bf1a51f8f1639489fcd3f542af671b7ac7
2026-05-28 08:07:04 +00:00
Andrew Rumble dddefc9e30 Merge pull request #33962 from overleaf/lg-uuid-security-upgrade
[Security upgrade] pin uuid to 11.1.1 in root yarn.lock

GitOrigin-RevId: 873da40311c0e67fc3eb7eb0c484475d1e515148
2026-05-28 08:06:59 +00:00
Andrew Rumble 08975f4ba2 Merge pull request #33997 from overleaf/ar-require-cookie-consent-for-mixpanel-autocapture
[web] require cookie consent for mixpanel autocapture

GitOrigin-RevId: 6898b72079cc6c286ce15a276979381a23c32ebe
2026-05-28 08:06:55 +00:00
Miguel Serrano 21902e7a55 [CE/SP] Remove non-production dependencies (#33949)
Some non-production dependencies were bundled in the CE and SP images:
- `lint` was pulled as production dependency by `eslint-plugin`. Moving to peer-dependency, which is the usual strategy, addresses the issue.
- Yarn cache wasn't purged. By adding `/usr/local/share/.cache/yarn` to the mounts we ensure it's also cleaned.

GitOrigin-RevId: f328592c8f8de7193295839578e239a975fe30aa
2026-05-28 08:06:51 +00:00
Jakob Ackermann d610f404e7 [history-v1] increase timeout for downloading the latest content as zip (#34045)
Remove the default timeout as it's too low and a big footgun.

GitOrigin-RevId: 42e26a2a288ad3e38252bc98b909a4bc8b10f70c
2026-05-28 08:06:47 +00:00
roo hutton 4f192564f2 Merge pull request #33345 from overleaf/rh-split-group-role
Update group_role in customer.io when changed

GitOrigin-RevId: d21866a9fe324a0468de74a45b6932dda27de8a1
2026-05-28 08:06:43 +00:00
roo hutton 5d0becf76b Merge pull request #33572 from overleaf/rh-cio-past-due
Expose past_due status to customer.io

GitOrigin-RevId: 5b1b03db0e1068f1ae444585e4a9e732470f0ffa
2026-05-28 08:06:39 +00:00
MoxAmber ad193d81c5 Merge pull request #33840 from overleaf/as-link-sharing-schema
[web] Set ProjectInvite privileges type to Union

GitOrigin-RevId: a68e732bec5f6a7752b1110075736cb33454e6eb
2026-05-28 08:06:35 +00:00
David b6b03a2091 Merge pull request #33471 from overleaf/copilot/fix-git-integration-prompts
Fix incorrect auth error messages shown to git bridge users

GitOrigin-RevId: 52888e991cf1ec7f3ae28c67fcd70fa2a1a9fad9
2026-05-28 08:06:31 +00:00
Maria Florencia Besteiro Gonzalez bc8008737c Merge pull request #33847 from overleaf/lg-async-http-client-security-upgrade
[Security upgrade] bump async-http-client to 3.0.10 in git-bridge

GitOrigin-RevId: c2283a78055f1dce0644f2278167399e65a12937
2026-05-28 08:06:23 +00:00
Malik Glossop 94894779cf Merge pull request #32575 from overleaf/mg-worktree-symlink
Add worktree-setup and worktree-teardown scripts for dev environment

GitOrigin-RevId: 8cce26ef4f6f45bd9e39b2c46f314366d6414cab
2026-05-28 08:06:19 +00:00
Malik Glossop a47f6443f8 Merge pull request #33932 from overleaf/mg-select-style
Replace text label with icon in "Select style" toolbar button

GitOrigin-RevId: 52b93a29db47e99609a90294e53abe1057a6c71d
2026-05-28 08:06:15 +00:00
Jakob Ackermann 984b8e3f4a [web] add a hook for discovering the current root doc (#34027)
GitOrigin-RevId: d1930e6b13ca18dbae927dc15a5c6507351f71c8
2026-05-28 08:06:08 +00:00
Miguel Serrano 14b04ad4b8 [project-history] Removed request dependency (#32686)
* [project-history] Removed `request` dependency

GitOrigin-RevId: 086bbbf2efeea6026127653a1f68ca6bf0476de6
2026-05-28 08:06:04 +00:00
Kristina 78dd0a8681 [web] update email base templates (#33791)
* update email template design
* gate email template on split test

GitOrigin-RevId: 2e0a1d9abf11a0c3f16e103511191d43d542b8a4
2026-05-28 08:05:59 +00:00
Malik Glossop fa26367aa9 Merge pull request #33805 from overleaf/mg-ai-paywall-analytics
Add paywall-prompt and paywall-click events to ai paywalls

GitOrigin-RevId: aa7de15a990ad1833e3dda65d5fb50f60bb7c9e3
2026-05-28 08:05:52 +00:00
Malik Glossop 8af5c2c346 Merge pull request #33600 from overleaf/worktree-mg-bullet-list-compact
Combine toolbar list controls and render indentation controls conditionally

GitOrigin-RevId: 48d7c52983449566bfa21b5572915d79e595c704
2026-05-28 08:05:48 +00:00
Miguel Serrano 4cfaea4621 [clsi] Fix compile stop detection in local runner (#33024)
* [clsi] Fix compile stop detection in local runner

* Tighten compilation cancelled message

* Cleanup process PID on `error`

GitOrigin-RevId: 61495466b98c127402b256d629ea58e7b9be6df7
2026-05-28 08:05:44 +00:00
Miguel Serrano 8ce9d184cb [migrations] pin underscore version to 1.13.8 (#33951)
`east` is the only workspace pointing to an old version of `underscore`, which is now pinned to `1.13.8` the same way it's done for `argparse`.

GitOrigin-RevId: a938067ba62aca7b73e15f030d9c341f9337c26d
2026-05-28 08:05:30 +00:00
Tim Down 287e619967 Merge pull request #33663 from overleaf/td-en-json-sort-hook
Sort staged locale files in the pre-commit hook

GitOrigin-RevId: fb3a155be52495305372c5c5cc54c2f50e88c417
2026-05-28 08:05:22 +00:00
Eric Mc Sween 52a8a447aa Merge pull request #33909 from overleaf/em-remove-tpdsworker
Remove tpdsworker service

GitOrigin-RevId: 9014d7d5bdc4e3cc7a7168d93b90ef8aa72d0c1a
2026-05-27 08:08:00 +00:00
Tom Wells 0565a778d8 Library: Design Changes (#33933)
GitOrigin-RevId: b45ea92adc424e2864e952cd7f157509e10ffb7d
2026-05-27 08:07:47 +00:00
Kristina 577ff63eca [document-updater] batch collaborator lookups in project_notifications.mts (#33974)
* Refactor project collaborator check to handle multiple project IDs and optimize Redis caching
* Batch mongo queries and use promiseMapWithLimit

---------

Co-authored-by: Domagoj Kriskovic <dom.kriskovic@overleaf.com>
GitOrigin-RevId: 7d568b4b05894465c930595ebd3f214ebc5c72c0
2026-05-27 08:07:35 +00:00
Jakob Ackermann c7b56ff295 [monorepo] remove contacts service (#33550)
GitOrigin-RevId: 15478243e4d6a56b81eee28f76f9ef7dc54a45d7
2026-05-27 08:07:19 +00:00
Kate Crichton 01624f1387 Merge pull request #33311 from overleaf/kc-remove-domain-verification-ui
[web] remove domain verification UI

GitOrigin-RevId: 71e4869180cd6573870d4ba48b7a66bade99eab3
2026-05-27 08:07:02 +00:00
Daniel Kontšek f4565a677b Merge pull request #33922 from overleaf/dn0-gke02-rt-23-staging
[gke-02] Migrate staging real-time pool to n4d-highcpu-2

GitOrigin-RevId: 5dcdc8d5074cc49e2027ba06e86a0751eab71bf4
2026-05-27 08:06:18 +00:00
Kristina 3bd908d963 [document-updater] update logging in project_notifications script (#33924)
* reduce logs for project_notifications script
* include cache hit/miss stats

GitOrigin-RevId: 6aac74a18f2ad352b0e8609f0ad2b6c831bd9da5
2026-05-25 08:05:57 +00:00
Eric Mc Sween 3d71f51435 Merge pull request #33856 from overleaf/em-library-write-and-cite
[web] Add account-level library as a source for write & cite autocomplete

GitOrigin-RevId: 3182d516c1fab68e5bbf5d77d60e4c431d54b73e
2026-05-25 08:05:53 +00:00
Antoine Clausse 09f03381fd [web] Fix preview next-invoice date for cadence-change upgrades (#33697)
* [web] Fix preview next-invoice date for cadence-change upgrades

When upgrading from a monthly plan to an annual plan (or vice versa) the
user pays for a full new-cadence term today, so the next payment is one
new-term-length from now — not the current cycle's period end. Previously
we always echoed subscription.periodEnd in the preview, which surfaced
the stale current-cycle date and misled the user into thinking they'd
be charged again ~25 days later.

makeChangePreview now compares the current and next plans' annual flag:
on a cadence flip it returns now + 1 year or now + 1 month; otherwise it
keeps the existing behaviour.

Closes #33283.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Format

* Fix next invoice date using priceincents

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
GitOrigin-RevId: 05b660ecb518c04b60e88f2ddc7531733245bdde
2026-05-25 08:05:49 +00:00
Tim Down ab4f23ab86 Merge pull request #33788 from overleaf/td-payment-preview-ai
Add AI to features list on upgrade checkout page

GitOrigin-RevId: 0b5b94fc4961ac2e8e2d2812bb80c1041f4c8c24
2026-05-25 08:05:41 +00:00
Tim Down b8e689b3fe Merge pull request #33902 from overleaf/revert-33567-td-briefly-onboarding-screen
Remove Briefly ad from onboarding flow

GitOrigin-RevId: e762d550a802b7bf01b5e22d5025ebc9f304df42
2026-05-25 08:05:37 +00:00
Malik Glossop 9ae5663423 Merge pull request #33160 from overleaf/copilot/fix-typeerror-out-of-memory
fix: normalize string errors at pdf-caching call sites before passing to OError.tag()
GitOrigin-RevId: 0259de81cca72e3b9c304f68b087a627db8f1980
2026-05-25 08:05:33 +00:00
Malik Glossop eb9d586bdb Merge pull request #32968 from overleaf/worktree-labs-feature-preview
Add labs preview modal to editor

GitOrigin-RevId: 0df33135febc8e94129bcdfdfb5c4981326dfab0
2026-05-25 08:05:28 +00:00
Alf Eaton 24ba0b86b1 Set npmMinimalAgeGate in yarnrc (#33639)
GitOrigin-RevId: 69745c2c5606ff90d7a2a8b904f850007082b84a
2026-05-25 08:05:24 +00:00
Lucie Germain ae00bcbeca [Security Upgrade]: pin @xmldom/xmldom to 0.8.13 (#33373)
Adds a resolution in root package.json to force all consumers to
@xmldom/xmldom@0.8.13, fixing GHSA-wh4c-j3r5-mjhp, GHSA-j759-j44w-7fr8,
GHSA-x6wf-f3px-wcqx, GHSA-f6ww-3ggp-fr8h, and GHSA-2v35-w6hq-6mfw.

The vulnerable 0.7.13 entry in yarn.lock is replaced by 0.8.13
(minimum safe version across all five advisories).

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
GitOrigin-RevId: e1a301e3a1d637894284f35238ca0e8c23534276
2026-05-25 08:05:19 +00:00
Lucie Germain 6fa708982b Pin argparse/underscore to 1.13.8 via yarn resolution (#33364)
Fixes GHSA-cf4h-3jhx-xvhq (critical, arbitrary code execution) and
GHSA-qpx9-hpmf-5gmw (high, DoS via _.flatten/_.isEqual).

Vulnerable underscore@1.7.0 came from js-yaml@2.1.3 → argparse@0.1.16.
All other instances were already ≥1.13.8.

GitOrigin-RevId: b2ab4bc2682e19709694b7dd686134a439ade90c
2026-05-22 08:06:48 +00:00
Kate Crichton afaef6a1ef Merge pull request #33499 from overleaf/kc-update-add-domain-modal-ux
[web] update domain verification modal ux

GitOrigin-RevId: e7abcb569e8c956e8f22f09c90dd8e9998fc6255
2026-05-22 08:06:30 +00:00
Domagoj Kriskovic bf1dd6986f Record project notification timestamp in Redis on applyUpdate (reverted) (#30814)
* Revert "Revert "Record project notification timestamp in Redis on applyUpdate (#29509)""

This reverts commit 31c88ee836fb5e1ab3950da590c28e24b1397edb.

GitOrigin-RevId: fc012324f6035156585fab468aca72b900a9710b
2026-05-22 08:06:26 +00:00
Miguel Serrano 496f110465 [CE] Update phusion image version to 1.0.3 (#33885)
GitOrigin-RevId: cc707258e145849f3bc1ddb6b44f7eca8c904d74
2026-05-22 08:06:15 +00:00
Mathias Jakobsen fc3dbc1aad Merge pull request #33883 from overleaf/mj-command-palette-border
[web] Tweak border color of command palette

GitOrigin-RevId: 7b37b9c8503402da62e2fd2b334f28dc14760b75
2026-05-22 08:06:11 +00:00
Kristina 88eb599d4e [document-updater] increase scanStream COUNT for project notifications (#33833)
* increase scanStream COUNT for project notifications
* fix Bull queue.add delay option being ignored
* parse timestamp to number before adding to notification queue
* fix outdated comments in project_notifications script

GitOrigin-RevId: 98bb638228550b2f6f2de90280a06c47e022cf96
2026-05-22 08:06:03 +00:00
ilkin-overleaf 192fd7c28c Merge pull request #33542 from overleaf/ii-dropdown-flicker
Fix dropdown menu flicker on open

GitOrigin-RevId: ddf826b30fcac3322d86067e5950731e7dc1a2d4
2026-05-22 08:05:59 +00:00
Domagoj Kriskovic 803ba735ca Show toast when Python script saves output files to project
GitOrigin-RevId: 9ca5201645953f86c3ac8e83f545dfbcdac2b35c
2026-05-22 08:05:55 +00:00
Olzhas Askar 014ac37704 Merge pull request #33814 from overleaf/oa-adjust-tooltip
[web] Adjust tooltip

GitOrigin-RevId: a180fb9872c9fc85b5ea7e3821e8e8c8393bab9d
2026-05-22 08:05:50 +00:00
Olzhas Askar 793d5c79fb Merge pull request #33775 from overleaf/oa-remove-addon-section
[web] Remove Add-ons section

GitOrigin-RevId: 82009dd6aeb3588f46cabacdb7313c01f3afc27e
2026-05-22 08:05:46 +00:00
Mathias Jakobsen 9d79cc89ec Merge pull request #33855 from overleaf/mj-command-palette-tweaks
[web] command palette tweaks

GitOrigin-RevId: 454d6916043d3317e60302379bcf9707fb8d4dcb
2026-05-21 08:07:12 +00:00
Mathias Jakobsen eddec90cb1 Merge pull request #33649 from overleaf/mj-command-palette
[web] Add command palette

GitOrigin-RevId: 5bf1903836810ca5f0e2bc7f6c00a4b1da797ea2
2026-05-21 08:07:04 +00:00
Simon Gardner 5cfd7b6c6a [migration] re-use paypal methods if billing agreement ids match (#33720)
GitOrigin-RevId: 4a324c1cdde84dabf620a2616a0aa27242cf041e
2026-05-21 08:06:38 +00:00
Olzhas Askar bb0dc07d22 Merge pull request #33741 from overleaf/lg-sanitize-html-upgrade
[Security upgrade] Upgrade sanitize-html to 2.17.4 (GHSA-rpr9-rxv7-x643)

GitOrigin-RevId: 40a11361eac35d44a6fd7069e0d0d7c02a6628ec
2026-05-21 08:06:33 +00:00
Miguel Serrano ad651a22fa Revert "[web] Add SVG support to file-view panel (#32155)" (#33832)
This reverts commit e80c491a10db6f5757c568430e17d9cbb613c5b4.

GitOrigin-RevId: dbe0de698bc7349e5b8f9712d1d13998e41ab528
2026-05-21 08:06:28 +00:00
Miguel Serrano 35681dd3b2 [web] Add SVG support to file-view panel (#32155)
* [web] Add SVG support to file-view panel

Adds support by reading the content of the downloaded SVG, then creating a blob and rendering it as native HTML.

GitOrigin-RevId: e80c491a10db6f5757c568430e17d9cbb613c5b4
2026-05-21 08:06:16 +00:00
Miguel Serrano f9c53fe147 [web] Added DEFAULT_LATEX_COMPILER env (#32455)
This is mainly intended to be used in CE/Server Pro

GitOrigin-RevId: 277f9afca389a1e7b00db2d987129432fb1707b5
2026-05-21 08:06:12 +00:00
Miguel Serrano 107189cd5f [web] Clear hardcoded password in external SP auth (#33597)
registerExternalAuthAdmin() now generates a random password on admin registration.

A migration clears the password for existing installs only in CE/SP

GitOrigin-RevId: 94a82d35dc8cd46915c31fb24f477c19367025eb
2026-05-21 08:06:07 +00:00
Simon Gardner 2233ac9b1d Harden shell quoting in CI pipeline definitions (#33789)
GitOrigin-RevId: a21c02f632b1e357ba18a86378d796a0d93fa484
2026-05-20 08:07:46 +00:00
Mathias Jakobsen 5d4f38e57a Merge pull request #33629 from overleaf/lg-fast-uri-resolution
[Security upgrade] Pin fast-uri to 3.1.2 via resolutions (GHSA-q3j6-qgpj-74h6, GHSA-v39h-62p7-jpjc)

GitOrigin-RevId: 154e742e12cb68e8b1c8d5b88e1a188160746784
2026-05-20 08:07:34 +00:00
Liam O'Brien b4a76fee6d [web] Implementing library search (#33604)
* Initial working version of library search

draft fetch allowing optional search param

draft debounce search

draft search bar

draft using  for search

draft search params

draft data index creation

draft prefix-regex search

draft add fields only on search

draft index setup

draft search tests

draft search tests for extra params

draft using correct display value from bib entry for tokenization

* Library search handles diacritics

* Library styling and refreshing table data without
reloading table

* Updating mongo search query and creating migration
scripts for existing data

* Using Mongo query for sorting results

* Moving copied files into shared directory

* Addressing review comments

* Pulling changes from bibtex-search-token for consistency with migration

* Fixing lint

* Using mongo collation for handling case and diacritics in search queries

* Boosting citation keys with  check for tokens

* Removing double foldLatinDigraphs call

* Matching figma designs for Library search component

* Adding cursor for paginated Library search results

* Re-fixing flash after searching library

* Unit test for cursor search

* Using same cursor object for search and get all results

* Data migration moved to manual script

GitOrigin-RevId: b7e6a1f07f775c8450dd97e7269cab3b68ca0eb3
2026-05-20 08:07:29 +00:00
Jessica Lawshe 9acf3b8e7f Merge pull request #33610 from overleaf/jel-hostname-capturedByGroup
[web] Check `capturedByGroup` when adding new email

GitOrigin-RevId: f9ef3d4cc7387dc0139a70aecd6cfcb20170abc6
2026-05-20 08:07:18 +00:00
Eric Mc Sween 2f32b9d61e Merge pull request #32706 from overleaf/em-dropbox-queue
[third-party-datastore] Apply Dropbox→Overleaf updates inline, removing queue hop

GitOrigin-RevId: 1ea17eefe57aaf32634ce3395682f7eac2e53dc5
2026-05-20 08:07:10 +00:00
Lucie Germain 2f08f6f6eb Bump mongoose to 8.22.1 (GHSA-wpg9-53fq-2r8h) (#33648)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: f092e8d914ea5825e285fe4741bb42dd2c5d5fa3
2026-05-20 08:07:05 +00:00
Alf Eaton 34d272afa9 Reapply "Wrap PDF setDocument in startViewTransition (#33346)" (#33633)
GitOrigin-RevId: 11dc65d8a8195c8cd6e6e2b58905a0f8b7b218f4
2026-05-20 08:06:53 +00:00
Alf Eaton 1d959af16e Process zip import entries in parallel (#33176)
GitOrigin-RevId: f77c2b08d4c085b51a8608d2621dd5bbe1134258
2026-05-20 08:06:48 +00:00
Alf Eaton 7eed283b11 Ignore entries in __MACOSX folder when importing zip archive (#33147)
GitOrigin-RevId: e990d593d96085e13a209d4155823097b0814276
2026-05-20 08:06:40 +00:00
Alf Eaton efab968153 Treat qmd and rmd extensions as Markdown (#33786)
GitOrigin-RevId: 89d79e958ea08f3388bde8dc561b04f87a1b6549
2026-05-20 08:06:36 +00:00
Alf Eaton 7c8e89923d Add .qmd to editable file extensions (#33785)
GitOrigin-RevId: 868a96e66e8d408ceb2f5b29e59f0d1ee12992dd
2026-05-20 08:06:31 +00:00
Alf Eaton d8c33cc34c Allow multiple concurrent reference searches (#33739)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 403d4f5900a8c4ccdc64032d365adb285a191b71
2026-05-20 08:06:23 +00:00
Alf Eaton c0acddbfaf Use sharejs doc type in Storybook stories (#33565)
GitOrigin-RevId: a2bf64d3ce376dd05e740796e8f1ea74913ed8f0
2026-05-20 08:06:19 +00:00
Alf Eaton 3dbbf95fbb Disable Yarn telemetry (#33561)
GitOrigin-RevId: f835277a689e142b7d336ee38af3f142e37c6f5c
2026-05-20 08:06:14 +00:00
Alf Eaton 30edd837e1 Use resolve.tsconfig (#31639)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
GitOrigin-RevId: 1c7da49e14af5935f85f1927186a825b116bb4e9
2026-05-20 08:06:05 +00:00
Evelyn b5654c5a01 fix: chown /var/lib/overleaf mount point to www-data (#33764)
The init script chowns all subdirectories but not the mount point
itself. When the host volume is owned by a non-www-data user with
restrictive permissions (e.g. 770), the web process cannot traverse
the directory and crashes with EACCES, causing a 502.

Fixes #1325 and #1465

COPYBARA_INTEGRATE_REVIEW=https://github.com/overleaf/overleaf/pull/1475 from ev-not-eve:patch-1 269a80500f

Co-authored-by: Evelyn <evansvevelyn@gmail.com>
GitOrigin-RevId: 959051861246c9f3958e56861821b92d84167926
2026-05-19 08:04:55 +00:00
Mathias Jakobsen ce6f9b8e8c Merge pull request #33705 from overleaf/mj-clsi-cwd-for-conversions
[clsi] Add cwd argument to CommandRunner and use to simplify conversions

GitOrigin-RevId: 5333e3262a99e602ab5470ae1e23facb5b28a170
2026-05-19 08:04:51 +00:00
Jakob Ackermann c0111fec29 [monorepo] run format_fix and trigger prettier on .agents changes (#33759)
* [monorepo] run format_fix and trigger prettier on .agents changes

* [monorepo] cleanup stale prettier ignore rule

* [monorepo] tweak format:monorepo-check:fix

GitOrigin-RevId: e6c29a0c601fbf388a048eb42706f9bd0a18344f
2026-05-19 08:04:48 +00:00
Jakob Ackermann 1f8371e0a3 [document-updater] flush_all: log progress after every 1k projects (#33757)
GitOrigin-RevId: b5b68f6f53bece51234799fb626d0d6a2a5b590c
2026-05-19 08:04:41 +00:00
Jakob Ackermann 293d89a4cb [web] inline contacts service into web (#33546)
GitOrigin-RevId: d5e84d4f80f5ad4e951934d6dcdc332b0d26f3d0
2026-05-19 08:04:34 +00:00
Miguel Serrano b79d432deb [web] Conditionally show items in insert figure toolbar (#33721)
Removes the options for inserting an image from another project or by downloading an external URL if the features are disabled.

GitOrigin-RevId: ffa64e5929e254d8a236c8e9aca4eb8210f444c9
2026-05-19 08:04:21 +00:00
Jakob Ackermann 99148d5956 [web] silence customer.io integration when not configured in dev/CI (#33731)
GitOrigin-RevId: c9498f57f0dacb3d18cd7617388df11d5cf029de
2026-05-19 08:04:17 +00:00
Brian Gough 60860aa202 Merge pull request #33576 from overleaf/bg-jpa-convert-document-to-file
Modify convertDocToFile to bypass docstore

GitOrigin-RevId: 3ec789034a369d39d223450462394c8f303caa07
2026-05-19 08:04:13 +00:00
Jakob Ackermann b1a0bb16db [migrations] delete expired oauth access tokens after 24h (#33575)
Co-authored-by: Brian Gough <brian.gough@overleaf.com>
GitOrigin-RevId: 7f67a7e6949472c66f5f75a6053161d8e359f5df
2026-05-19 08:04:09 +00:00
Kristina 2f5d838e0f Merge pull request #33704 from overleaf/kh-add-reject-change-preference-check
[web] add reject tracked change preference check

GitOrigin-RevId: b55dba21b3d4f42e68528d2b5906862c57794cd1
2026-05-18 08:06:57 +00:00
Andrew Rumble e9aedce4ab Merge pull request #33625 from overleaf/ar-update-vitest
[monorepo] bump vitest to 4.1.5

GitOrigin-RevId: 22ba2249ae384fd59347c9aa45c70f51ccdf8890
2026-05-18 08:06:49 +00:00
Andrew Rumble 19ad00c329 Merge pull request #33743 from overleaf/lg-systeminformation-upgrade
[Security Upgrade] Upgrade systeminformation to 5.31.6 (GHSA-hvx9-hwr7-wjj9)

GitOrigin-RevId: bd75d2bc59e183d23972e367f40f753c08ca6967
2026-05-18 08:06:41 +00:00
Eric Mc Sween 2913e462ec Merge pull request #33665 from overleaf/copilot/fix-error-logging-in-git-bridge
git-bridge: Log WrongBranchException and ForcedPushException at WARN instead of ERROR
GitOrigin-RevId: 7aaa934a0df614e336ce3c20b892af1af0cd070f
2026-05-18 08:06:29 +00:00
Eric Mc Sween a3682af6e4 Merge pull request #33710 from overleaf/em-tpds-config
Clean up tpdsworker config from web and third-party-datastore

GitOrigin-RevId: 3856126d9dc856fea4bc4133b11402c35b10630b
2026-05-18 08:06:25 +00:00
Antoine Clausse 9e42d3a530 [web] Address design QA items on pricing page (#33682)
- pricing table: integration icons gap uses --spacing-06 (horizontal),
  integrations content gap uses --spacing-04 (vertical)
- Student card no longer renders with the green stroke highlight
- Interstitial H1 wrapped in .main-heading-section so its spacing
  matches the pricing page

Part of #33619.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 12ddd223f68c776c06a3d5dc5faa841819baae90
2026-05-18 08:06:17 +00:00
Antoine Clausse 0089c0af08 [web] Space skip link from disclaimer on interstitial (#33464)
The "continue with free plan" skip link sat directly under the
disclaimer with no separation. Add spacing-08 margin-top to match
the disclaimer's own padding-top above the cards.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: ba7334785757a39ca0bdff309ded224e6cb8e3bf
2026-05-18 08:06:09 +00:00
Olzhas Askar 868da835b6 Merge pull request #32434 from overleaf/oa-babelfish
[web] Project Babelfish

GitOrigin-RevId: da8c47c0831eaab3e2c74a9507892ae9571919e8
2026-05-18 08:05:57 +00:00
Olzhas Askar d388e48a99 Merge pull request #33679 from overleaf/oa-plan-names
[web] Get plan names from the settings

GitOrigin-RevId: 1e61975c3306c025f33e05686f9d2b57964b4f65
2026-05-18 08:05:52 +00:00
Olzhas Askar 6c267e68d3 Merge pull request #33707 from overleaf/oa-learn-links
[web] Learn Overleaf links

GitOrigin-RevId: af9f72da008ad8b8c86e4c355268123eb6c40bcd
2026-05-18 08:05:48 +00:00
Andrew Rumble 25dfaab2a1 Merge pull request #33641 from overleaf/lg-fast-xml-builder-resolution
[Security upgrade] Pin fast-xml-builder to 1.1.7 via resolutions (GHSA-5wm8-gmm8-39j9, GHSA-45c6-75p6-83cc)

GitOrigin-RevId: ab13841bd8c20da98a136567cf7436ebb9f73722
2026-05-15 08:08:40 +00:00
Noel Schenk ba016d798e Upgrade MongoDB image from 6.0 to 8.0 (#33579)
sharelatex  | The MongoDB server has version 6.0.27, but Overleaf requires at least version 8.0. Aborting.

COPYBARA_INTEGRATE_REVIEW=https://github.com/overleaf/overleaf/pull/1480 from noel-schenk:patch-1 4a13e4fbcd

Co-authored-by: Noel Schenk <schenknoel@gmail.com>
GitOrigin-RevId: 9035d16f2c34edcb39c0da99e9d02b9ed8a9f6fa
2026-05-15 08:08:35 +00:00
Mathias Jakobsen ac961f1d40 Merge pull request #33687 from overleaf/mj-temporary-tabs-fix
[web] Only consider real key presses to make tab permanent

GitOrigin-RevId: 50ab453445e111de2b317f50470f9f4eec39a66f
2026-05-15 08:08:28 +00:00
Mathias Jakobsen 6538c00742 Merge pull request #33690 from overleaf/mj-prune-deleted-tabs
[web] Prune non-existent tabs when file tree changes

GitOrigin-RevId: 97e68a88a201acc2d1e582911ca64e1f72f9bfe1
2026-05-15 08:08:19 +00:00
Copilot 3980b9e580 Fix IDOR in exports by adding token verification (Issue #31637) (#32883)
* Fix IDOR in exports by adding token verification

Implement jdleesmiller's suggested fix for Issue #31637:
- V1: Return export token in create response
- V1: Verify token in get_export using secure_compare
- Web: Pass token through fetchExport and fetchDownload
- Web: Return token from exportProject to frontend
- Frontend: Pass token as query param on status/download requests
- Add tests for both services

Agent-Logs-Url: https://github.com/overleaf/internal/sessions/7ba5f535-fba2-49a8-91d4-c87bd332d3a0

Co-authored-by: briangough <7457354+briangough@users.noreply.github.com>

Fix window.location.pathname to .href to preserve query params

Code review correctly identified that window.location.pathname strips
query parameters. Switch to window.location.href so the token query
parameter is preserved in download URLs.

Agent-Logs-Url: https://github.com/overleaf/internal/sessions/7ba5f535-fba2-49a8-91d4-c87bd332d3a0

Co-authored-by: briangough <7457354+briangough@users.noreply.github.com>

Fix test mocks to include token in POST responses

Agent-Logs-Url: https://github.com/overleaf/internal/sessions/0350c6ef-0fff-4e98-8464-812cd92c523f

Co-authored-by: briangough <7457354+briangough@users.noreply.github.com>

fix formatting

Fix token assignment in initiateExport to use pollResponse token if available

Add requireExportToken config setting and tests for invalid/missing token cases

Agent-Logs-Url: https://github.com/overleaf/internal/sessions/059bdba2-4f7a-4407-a5a5-cfcffd888739

Co-authored-by: briangough <7457354+briangough@users.noreply.github.com>

fix formatting

Add tests for export status and token validation in ExportsController and MockV1Api

Co-authored-by: Copilot <copilot@github.com>

* Update services/v1/main/app/controllers/api/v1/overleaf/exports_controller.rb

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix linting

* fix fetchString response handling in ExportsHandler tests

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Brian Gough <briangough@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Brian Gough <brian.gough@overleaf.com>
GitOrigin-RevId: 399aef8eaa15ab3655f0905482f3a31fe94e2251
2026-05-15 08:08:04 +00:00
Miguel Serrano 5a886aa9fb [web] Add extra details to flexible license sales email (#32929)
* [web] Add extra details to flexible license sales email

GitOrigin-RevId: fbd41adae21c55c5e97f9531565100e1ae911808
2026-05-15 08:07:56 +00:00
Tim Down 248e149701 Default interstitial to monthly plans (#33706)
* Default interstitial to monthly plans except for upgrade, which defaults to user's existing subscription period

* Add tests for interstitial page period toggle defaults

GitOrigin-RevId: fa0ac41e7d8a7bf858b53e0940287b28ef21253d
2026-05-15 08:07:49 +00:00
Eric Mc Sween 529c332159 Merge pull request #33658 from overleaf/em-fix-docker-tag-length
build: truncate branch names to 96 chars for Docker image tags
GitOrigin-RevId: 9db313244e78a6d4e0aa5d8c08d25f1aac83318b
2026-05-14 08:06:45 +00:00
Jakob Ackermann 0c8e93bb33 [server-pro] fix tag name for branches with slash (#33685)
GitOrigin-RevId: 25ee2d340b17ce7c758ec8c7e156a67928ab6c73
2026-05-14 08:06:38 +00:00
Davinder Singh a3a508d193 [WEB] Add analytics events for importing and exporting to different file types (#33614)
* adding events for success and failure for import and export from latex

* adding the operation property to capture the import/export keyword

GitOrigin-RevId: 2e5482b3c7517b402fc151966975ca8718729683
2026-05-14 08:06:30 +00:00
Jakob Ackermann 75a12dda17 [web] resync_projects: use the secondaries for all reads (#33684)
* [docstore] add useSecondary flag to projectHasRanges

The rev-check for unarchiving always consults with the primary.

Two extra changes:
- Add a projection argument to peekDoc in order to skip lines download
   from projectHasRanges.
- Add one retry to peekDoc to reduce chances of surfacing a rev-check
   violation.

* [web] resync_projects: use the secondaries for all reads

* [web] add default value for useSecondary

* [docstore] add default value for useSecondary

* [k8s] docstore: set MONGO_HAS_SECONDARIES=true

GitOrigin-RevId: f15ec4fdc1cabe74c1eab87bec85f28d6f7a587d
2026-05-14 08:06:26 +00:00
Domagoj Kriskovic ff53705bfa Refactor Python output pane toolbar for improved layout and styling
GitOrigin-RevId: b6d838e5c9bd8023bf12df976dad0c50564a0b2f
2026-05-14 08:06:22 +00:00
renovate[bot] fc66bbfb26 [CoreI] Update dependency axios to v1.15.2 from 1.15.0 [SECURITY] (#33398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
GitOrigin-RevId: 567d0e7463084e872187a72085714f68d84dc5b6
2026-05-14 08:06:04 +00:00
Alf Eaton d203a62834 Fix (un)fold all shortcuts on macOS (#33630)
GitOrigin-RevId: db0911cdfdeb19c90bd601e6173973d884859b09
2026-05-14 08:06:00 +00:00
Alf Eaton 6776f82952 Wait for parsing and syntax highlighting in autocomplete Cypress tests (#33667)
* Wait for parsing and syntax highlighting in autocomplete Cypress tests

* Wait for syntax highlighting in write-and-cite Cypress tests

GitOrigin-RevId: d48f10c864b0a170b4a02e95e3a989fdc4137dbb
2026-05-14 08:05:55 +00:00
Alf Eaton 00ddd8185c Upgrade webpack and related dependencies (#31638)
GitOrigin-RevId: e188a6ab9f7a024c1769a85e1d4e40ccb5d02213
2026-05-14 08:05:51 +00:00
Jakob Ackermann b62d4814c3 [monorepo] turn throw statements in callback code into callback calls (#33524)
* [eslint-plugin] add rule for throw inside callback code

* [monorepo] enable our custom eslint plugins globally

* [monorepo] fix running make lint from root

* [monorepo] turn throw statements in callback code into callback calls

* [monorepo] add eslint-plugin libraries to all the Dockerfiles

* [monorepo] install eslint-plugin library at the root level

* [linked-url-proxy] add eslint-plugin library into Dockerfile

* [latexqc] add our eslint-plugin to eslint config

GitOrigin-RevId: b05e3ebbefb62370f2422e83880dd3913815270d
2026-05-14 08:05:47 +00:00
Kristina d8df893593 [web] rm unnecessary webpack configuration (#33587)
GitOrigin-RevId: d9f305e59af2585db096a83c4cbd41ba5f785184
2026-05-14 08:05:39 +00:00
Jimmy Domagala-Tang bc2f5ae746 Reject tracked changes notifications (#32917)
* [web] Reject tracked changes notifications

feat: adding new tests

feat: adding rejected changes notifications

feat: adding tests for rejectchanges

feat: updating tests for rejecting notifications;

feat: adding in rejecting user, and improving subject and activity line

fix: moving to a params object instead of positionals for email building

feat: updating to use events triggered from applyUpdate in document-updater

feat: updating to send rejected author ids with rejected change notification instead of change ids

feat: moving rejected author notification determination to updateManager instead of RangesManager, which is used by other paths

feat: only map to author if changes were made

* fix: gate by user status not project status

* fix: unit tests post-rebase

---------

Co-authored-by: Kristina Hjertberg <kristina.hjertberg@overleaf.com>
GitOrigin-RevId: f992e1885c47d1a6cf776740769d6d4763f3cb7c
2026-05-14 08:05:35 +00:00
Andrew Rumble 5e3561aedc Merge pull request #33655 from overleaf/lg-ip-address-resolution
Pin ip-address to 10.1.1 via resolutions (GHSA-v2v4-37r5-5v8g)

GitOrigin-RevId: c0233698549fee7f32c8a95a17b793b8535922c1
2026-05-14 08:05:30 +00:00
Jakob Ackermann 7c50dc9990 [history-v1] add endpoint for downloading latest zip (#33181)
* [history-v1] add endpoint for downloading latest zip

* [web] address review feedback

* [web] tests: do not overwrite db.projects.overleaf, extend it

* [web] set includeReferer flag from downloading zip

GitOrigin-RevId: e63e549f004230086f82eccf03b43fd62bde6071
2026-05-13 08:06:53 +00:00
Jakob Ackermann b1931d0b3b [web] cleanup archived split-test assignments from user record on login (#33365)
* [web] cleanup archived split-test assignments from user record on login

Co-authored-by: Anna Claire Fields <anna.fields@overleaf.com>

* [migrations] purge archived split tests from all users

Co-authored-by: Anna Claire Fields <anna.fields@overleaf.com>

* [web] add missing mock and update snapshot test

* [web] gracefully access db.users.splitTests

---------

Co-authored-by: Anna Claire Fields <anna.fields@overleaf.com>
GitOrigin-RevId: bd185074a402556d7b7c812208cf834dd52b27a5
2026-05-13 08:06:49 +00:00
Antoine Clausse 13e426b14c [web] Share third-party integration icons + add Papers icon (#33537)
* [web] Add Papers/ReadCube icon to plans page integrations

Closes #33493

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Delete old 200kB zotero logo

* Allow png use in logos

* Allow wrap

* [web] Share third-party integration icon list across plans and onboarding pages

Extract the icons array to services/web/app/src/util/third-party-icons.mjs
so the plans-2026 feature table and the try_premium onboarding page render
from a single source. The try_premium page now also includes Papers and
follows the plans page ordering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* [web] Allow ciam try-premium logo row to wrap

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: f5a52418cbe01d9e343092b552183dffa3ae78bd
2026-05-13 08:06:44 +00:00
Antoine Clausse e04be692e2 [web] Use content-secondary for plan card body text (#33466)
Plan cards inherited neutral-60 (=neutral-60) for description and
include-list text, which can fail WCAG contrast on the light card
background. Switch to content-secondary (=neutral-70) per Vee's
short-term recommendation; affects the free plan description, card
include items (e.g. "Basic AI allowance") and the in-card group
picker labels.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 42aff473a779b4b4f36b6c648d86097a79f820c8
2026-05-13 08:06:40 +00:00
Eric Mc Sween d48af6dc1b Merge pull request #33213 from overleaf/em-git-bridge-structured-logging
git-bridge: structured JSON logging for GCP Cloud Logging
GitOrigin-RevId: 7ff06202cab6fe0e35c4a4f757d0b9ad04e5431a
2026-05-13 08:06:31 +00:00
Tim Down 85e55ebf5a Implement Briefly ad in onboarding flow (#33567)
GitOrigin-RevId: 78c7c38878024aaaf79def6ac7ca164d92a59a57
2026-05-13 08:06:02 +00:00
Tim Down 2f25793609 Merge pull request #33353 from overleaf/td-pricing-tooltip-esc
Dismiss Bootstrap tooltips via Esc key

GitOrigin-RevId: 2368a0691fd811180f908309f99b1f9a02c225ee
2026-05-13 08:05:58 +00:00
Alf Eaton 5e94f8abce Remove -- from test scripts (#33622)
GitOrigin-RevId: a33f09fb9924e4d02de6db0550c22585b2d331b6
2026-05-13 08:05:50 +00:00
jmescuderowritefull c383674cd8 Fix suggestions blocked modal (#33571)
GitOrigin-RevId: fde20822d884678c729ed93b672b5ad131901938
2026-05-12 08:07:03 +00:00
Eric Mc Sween 943961ca18 Merge pull request #33539 from overleaf/em-doi-import
Library: accept DOIs in the paste references dialog
GitOrigin-RevId: bcef3cd654a1ac34f7d372930ec21116d460cd74
2026-05-12 08:06:59 +00:00
Eric Mc Sween 569f36d01b Merge pull request #33315 from overleaf/em-library-api-pagination
Add cursor-based pagination to GET /library/references

GitOrigin-RevId: 1acec69031b0ca82ef6e1e05eddb165acaf05003
2026-05-12 08:06:55 +00:00
Eric Mc Sween aca60c02c0 Merge pull request #33391 from overleaf/em-bibtex-projection-32449
Use a projected state field for BibTeX entries in the editor

GitOrigin-RevId: 5034be8bdc0cb4b9d854135ac117046c1b3750e7
2026-05-12 08:06:47 +00:00
Jessica Lawshe 0f3ae5ac5b Merge pull request #33335 from overleaf/jel-add-email-captured-by-group
[web] Group with domain capture takes priority over Commons when adding new email

GitOrigin-RevId: 40eb561018f4be0badf9f3885d24553c5f8bbde7
2026-05-12 08:06:37 +00:00
Jessica Lawshe ba13ccdb11 Merge pull request #33202 from overleaf/jel-domain-captured-by-group-settings-page
[Domain capture] Check `domainCapturedByGroup` for existing emails on user settings

GitOrigin-RevId: 5ac86b89969b186cce0cac410c2957e5aa1b9703
2026-05-12 08:06:33 +00:00
Jakob Ackermann 6a911e4ec3 [web] do not send a second response from api error handler (#33526)
GitOrigin-RevId: 6974f5d5f7042d5170eb2a755715b2d139f06130
2026-05-12 08:06:25 +00:00
Jakob Ackermann 1df98c028d [web] add includeReferer flag to SplitTestHandler.getAssignment (#33235)
* [web] add includeReferer flag to SplitTestHandler.getAssignment

* [web] tests: migrate User.getSplitTestAssignment to async/await

I don't want to fight with callbacks and optional arguments. Just move
it to async/await. New tests should use async/await, so there is no
point in making this work in callback-hell.

* [web] remove unused URL import

GitOrigin-RevId: 6251001e6ba7354f704fa663be8ef365ca0b9d23
2026-05-12 08:06:21 +00:00
Mathias Jakobsen 6b28a4ee5a Merge pull request #33560 from overleaf/mj-conversion-cleanup
[clsi+web] Small cleanups and improvements to conversions / exports

GitOrigin-RevId: 300adfbb91e89f754ee7f835db792ccb50b27613
2026-05-12 08:06:17 +00:00
Mathias Jakobsen 62d92b70dd Merge pull request #33341 from overleaf/mj-two-step-export-web
[web] Add two-step pandoc conversion download

GitOrigin-RevId: 093f435a497a7583d2b4d23558415cc442f84553
2026-05-12 08:06:13 +00:00
Alf Eaton 64d706f114 Revert "Wrap PDF setDocument in startViewTransition (#33346)" (#33580)
This reverts commit 353ab865de3c7872363a61592d86390dfc34dacc.

GitOrigin-RevId: dd103eb413a51861b31cd77542ca541e10df0c6c
2026-05-12 08:06:05 +00:00
renovate[bot] 5c5a80923a [Platform] Update dependency dompurify to v3.4.0 from 3.3.3 [SECURITY] (#33227)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
GitOrigin-RevId: da9d98ac0b4d3690bd2db18f7c4f61cf45fb379a
2026-05-12 08:05:58 +00:00
Alf Eaton 17dd108ce1 Wrap PDF setDocument in startViewTransition (#33346)
* Set scale synchronously on pagesinit to prevent 1.333 DPI flash

PDF.js resets its internal scale to 1.0 when setDocument() is called,
causing pages to momentarily render at the default 96/72 DPI scale
(1.333) before the React restore effect can apply the correct value.
Setting currentScaleValue directly in the pagesinit handler eliminates
this one-frame wrong-scale flash.

* Override .page display to block to prevent horizontal jump on recompile

Overleaf's global .loading class sets display:inline-flex, which
collides with PDF.js's transient 'loading' class on .page elements.
When the loading class is applied, inline-flex breaks margin:auto
centering, causing the page to jump horizontally. Forcing display:block
at higher specificity prevents the global rule from taking effect.

* Fix scrollToPosition offset using marginTop instead of borderWidth

scrollPageIntoView aligns the page content edge with the container top,
leaving scrollTop equal to the page's top margin (12px) rather than 0.
The previous correction used borderWidth (effectively 0) so the margin
offset was never compensated. Using marginTop scrolls back the correct
amount so the margin above the first page is visible.

* Prevent PDF viewer collapsing during recompile by preserving height

When setDocument() is called with a new PDF, _resetView() synchronously
clears all page elements, briefly collapsing the .pdfViewer div to the
viewport height. This produces a visible flicker before pagesinit fires
and pages are re-added.

Fix: record the current height and pin it as min-height on the .pdfViewer
element before calling setDocument(). A one-shot pagesinit listener
removes the constraint once the new pages are initialised at the correct
scale, by which point the element is already at its correct final height.

* Suppress PDF.js page-level loading spinner in Overleaf viewer

The PDF.js loadingIcon/loading classes briefly add a ::after pseudo-element
with display:block and contain:strict to each page div. Overleaf has its
own loading state UI so the spinner is redundant, and its activation was
the root cause of the shifts 4-5 height oscillation (the display change
broke CSS margin collapse on .pdfViewer, adding 2x page margins to its
computed height).

The display:block rule already added to .page prevents the direct cause
(Overleaf's .loading{display:inline-flex} colliding with the PDF.js class).
This rule makes the intent explicit by zeroing the ::after entirely.

* Wrap PDF setDocument in startViewTransition

---------

Co-authored-by: Brian Gough <brian.gough@overleaf.com>
GitOrigin-RevId: 353ab865de3c7872363a61592d86390dfc34dacc
2026-05-12 08:05:53 +00:00
Alf Eaton b906de86db Add info to NotFoundError (#33440)
* Add info to errors in ProjectLocator

* Update ProjectLocator.test.mjs

* Add info to errors in SSOConfigManager

* Update SSOConfigManager.test.mjs

GitOrigin-RevId: 5a13350af1808f3a16a4bc8a9946cbe8f15e6b3a
2026-05-12 08:05:49 +00:00
Alf Eaton 8be321fd73 Add @modules alias to frontend test module resolver (#33491)
GitOrigin-RevId: 929180b0887695b0d04456cfa66ccf87b4cd51c0
2026-05-12 08:05:45 +00:00
Alf Eaton c9d4edbb5b Use CSSProperties for button style prop (#33492)
GitOrigin-RevId: 0f531c58d8ec82930bd603c19af840338f0b512e
2026-05-12 08:05:41 +00:00
Kristina b3beca0e8d [web] isolate cypress webpack caches (#33516)
GitOrigin-RevId: 65f8bf162df4fa3b67c8ac19f36fd578251e88f4
2026-05-12 08:05:24 +00:00
Tom Wells ea94771624 Add SplitTestBadge to file tree for .bib files (#33460)
GitOrigin-RevId: 7ae109844d5d0b984eeec25ea22572b304375c47
2026-05-12 08:05:19 +00:00
renovate[bot] 931ce5f590 [CoreII] Update bouncycastle.crypto.version to v1.84 from 1.83 [SECURITY]
GitOrigin-RevId: ce579e2bc46bb23075754ddfd0760269d11c00b0
2026-05-12 08:05:15 +00:00
Simon Gardner 8869dd5f32 Consistently import Stripe prices with interval: 12 monthly rather than yearly. (#33536)
* ensure imported annual prices are 12-monthly
* script to convert annual prices to 12-monthly

GitOrigin-RevId: b7af88156bde510ecdf080fc97384463fa77db13
2026-05-11 08:06:24 +00:00
Andrew Rumble 45005d2783 Merge pull request #33483 from overleaf/ar-remove-unused-sandboxed-module-deps
[monorepo] remove sandboxed-module from services that don't use it

GitOrigin-RevId: dbb9c3b11f4b5436a447942713ce02ff3efb0b50
2026-05-11 08:06:20 +00:00
Malik Glossop 7bf7102b98 Merge pull request #32712 from overleaf/mg-recreate-corrupted-blob
Recover from corrupted blobs during hard resync

GitOrigin-RevId: 7cc764e3bcc8557689c040c8f042991d97f897bc
2026-05-11 08:06:15 +00:00
Eric Mc Sween e0488a8d3b Merge pull request #33534 from overleaf/em-fix-autocomplete-32913
Fix autocomplete dropdown closing on blur and input re-click

GitOrigin-RevId: 82f45f0f1ae9e2b3846906d962a3f16e5b2963e4
2026-05-11 08:06:11 +00:00
Malik Glossop 893005cb88 Merge pull request #33281 from overleaf/jd-back-to-pdf-button
Change the "close logs" icon button to a "Back to PDF" button with a label

GitOrigin-RevId: 30b8d3e314fb9b8c901b90055832b88687827e62
2026-05-11 08:05:55 +00:00
Brian Gough 3940f8c2a7 Merge pull request #33504 from overleaf/bg-upgrade-yauzl
Upgrade yauzl library in web to version 3.3.0

GitOrigin-RevId: 82b4158db7a432f4257bd48402840f07801c6d07
2026-05-11 08:05:47 +00:00
Domagoj Kriskovic 11d35bd065 Test that pyodide package.json version matches fetch script
GitOrigin-RevId: e04ae191d2b1e5a08ba2e27518e61899d0e2d490
2026-05-11 08:05:39 +00:00
Domagoj Kriskovic 5d171066c2 Add analytics events for Python script runner
GitOrigin-RevId: 53f0fec09fc2a4ccdf1a6f77345741bed29d8a8b
2026-05-11 08:05:34 +00:00
Jimmy Domagala-Tang c37e46e1ad Add audit log entries when users max out their AI usage (#32886)
* feat: adding audit log entries when users max out their AI usage

* feat: also log when user hits quota exactly, since support wants to know that

* feat: moving audit logging to the rate limiters themselves

* feat: moving to single quota breach event with tool in info

* feat: adding audit log for ai quota tests

GitOrigin-RevId: 64056632f142a9ea22a703b7621234f93e9f6ec7
2026-05-08 08:10:23 +00:00
renovate[bot] 47f80317e4 [CoreI] Update dependency nodemailer to v8.0.5 [SECURITY] (#32703)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
GitOrigin-RevId: 3ae15cc3adad3d0212c46b5c478210dc9f20ef08
2026-05-08 08:10:18 +00:00
Eric Mc Sween 2bb35fafb8 Merge pull request #33490 from overleaf/em-parse-req-errors-2
Reintroduce custom error types in request validation

GitOrigin-RevId: 1985ca04c8fe693fb836b042517d94700343bc46
2026-05-08 08:10:13 +00:00
jmescuderowritefull 8c0589df7f Remove onboarding guide (#33474)
GitOrigin-RevId: a6a71cca0634ebba43f4ba18a5b40f79e3a81fb0
2026-05-08 08:09:31 +00:00
Mathias Jakobsen 32da6548c8 Merge pull request #33277 from overleaf/mj-pandoc-clsi-two-step-download
[clsi] Use clsi-nginx for downloading pandoc exports

GitOrigin-RevId: b6013fae6f53c7af714634d700ceed491d724653
2026-05-08 08:09:18 +00:00
Mathias Jakobsen ae31ad218c Merge pull request #33104 from overleaf/mj-pandoc-arguments
[clsi] Add pandoc arguments for better conversions

GitOrigin-RevId: 76cddc5959237d6d2801c56471d8d3f63d111200
2026-05-08 08:09:12 +00:00
Mathias Jakobsen 5dc67db403 Merge pull request #33089 from overleaf/ds-export-md-files-pandoc
[WEB + CLSI] Download as markdown

GitOrigin-RevId: 181eddf2513e9c5edacbab37e93f9cac2191ee1a
2026-05-08 08:09:07 +00:00
Mathias Jakobsen eddcc5a42e Merge pull request #32857 from overleaf/ds-pandoc-import-md
[WEB + CLSI] Import markdown files using pandoc

GitOrigin-RevId: adad7831ddb13a8fcb8063871166bde13cbbf1b6
2026-05-08 08:09:02 +00:00
Jakob Ackermann 44efc9d745 [monorepo] avoid corepack network requests (#33502)
* [monorepo] avoid corepack network requests

- Download yarn via corepack as the first step in all the docker files
- Turn off networking in corepack
- Do not run things in the upstream node image
  Instead, use the monorepo image, or base layer in all the services.
- Always build the base layer when running tests (uses cache)

* [monorepo] install corepack in shared place

* [clsi-lb] remove unrelated changes

* [web] add missing DC_RUN_FLAGS

* [monorepo] only rebuild test images locally

Also remove spurious build config in docker-compose.ci.yml.

* [server-ce] test: make yarn files available to host-admin and e2e

* [monorepo] put the corepack install snippet in a few more places

GitOrigin-RevId: 38005016ae5a708e12295e246269d6c18fece937
2026-05-08 08:08:57 +00:00
Chris Dryden 95f1e711da Merge pull request #32789 from overleaf/renovate-async.http.client.version
[CoreII] Update dependency org.asynchttpclient:async-http-client to v3.0.9 from 3.0.6 [SECURITY]

GitOrigin-RevId: 0490d7653fd08d8b7f26c94a036edae69911b7fb
2026-05-08 08:08:52 +00:00
Mathias Jakobsen c2c8b1d1f1 Merge pull request #32688 from overleaf/mj-chai-messages
[overleaf-editor-core] Use chai messages instead of try-catch in fuzzing tests

GitOrigin-RevId: b6db81e2fdbaac730ddca2bfb555983685396b43
2026-05-08 08:08:39 +00:00
Kristina bd604063e6 [web] add preferences to control all implemented notifications (#33320)
* feat: add granular controls for other features
* feat: add filtering to notifications that were missing them
* refactor: rm duplicate thread fetches
* fix: reduce permissions to fit spec (all === new comments/tracked changes, replies === only if also a participant)
* fix: include mentions in types

GitOrigin-RevId: b4a09ef59e5cf4de2e07d5b9a13c31fc1cf81a31
2026-05-08 08:08:30 +00:00
Kristina 40954ae2dc Merge pull request #32949 from overleaf/kh-default-invitees-to-replies-only
[web] default invitees to replies only

GitOrigin-RevId: e3198403917e2679e49e27aaa87ae111675dc974
2026-05-08 08:08:17 +00:00
roo hutton 498af9b07b Merge pull request #33467 from overleaf/rh-cio-prev-plan-type-fix
Only set previous_plan_type when normalised plan type changes

GitOrigin-RevId: 43133fc248bfb32b921da68bee91b445ca44eb1f
2026-05-08 08:08:11 +00:00
Mathias Jakobsen 0d40b7aca0 [web] Add dark mode variants to AI paywall notifications (#33469)
GitOrigin-RevId: 4c7b8fc0493b448fd565ac8b8521ee1777e60202
2026-05-07 08:08:41 +00:00
Mathias Jakobsen 76fbb56107 [web] Delay suggest fix paywall until suggest button has been clicked (#33458)
GitOrigin-RevId: 11d2ec0c9c33aea3fedff57d5f1a74d6ce774017
2026-05-07 08:08:36 +00:00
l-obrien-overleaf ff6ad4b41e Correct styling for visual bib elements (#32953)
* Correct styling for visual bib elements

* reverting outlined icon

* re-applying padding to optional fields

* citation key not shown on empty form

GitOrigin-RevId: 77c670e6687c6c60acf9f691e4c1d77e3390ac46
2026-05-07 08:08:24 +00:00
Jessica Lawshe fc4e17d30f Merge pull request #32816 from overleaf/jel-domain-captured-by-group
[web] Check `domainCapturedByGroup` on domain instead of `group.domainCaptureEnabled` only for project/dash redirect

GitOrigin-RevId: a6389da9c943327e5941eaa24eb274106526f80b
2026-05-07 08:08:07 +00:00
Miguel Serrano d3f5738158 [web] Update pro -> premium wording (#33445)
* [web] Update `pro` -> `premium` wording

Updated translations to reflect change on
plan denominations

GitOrigin-RevId: 39e9c8257f29540d33769e960b5b81fb08c47c62
2026-05-07 08:07:59 +00:00
ilkin-overleaf faec27d7b0 Merge pull request #33163 from overleaf/ii-share-modal-give-feedback
[web] Share modal give feedback link

GitOrigin-RevId: 5e83dec6c6b97c172b7600d8ded285db49178a64
2026-05-07 08:07:54 +00:00
renovate[bot] de9b07f0b9 [Platform] Update dependency lodash to v4.18.1 from 4.17.23 [SECURITY] (#33229)
* Upgrade lodash resolution to 4.18.1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* update lodash in rest of packages

---------

Co-authored-by: Eric Mc Sween <5454374+emcsween@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Anna Fields <acfields11@gmail.com>
GitOrigin-RevId: 66ce1610993a592899c25155757ca3267ebcd5c1
2026-05-07 08:07:41 +00:00
roo hutton 5c348078c2 Merge pull request #33340 from overleaf/rh-pause-block
Prevent calls to pause endpoint if pause-subscription not enabled

GitOrigin-RevId: 6efd00391576441b3104e34def2e5ad110dcc853
2026-05-07 08:07:36 +00:00
Brian Gough a6c8ce32c3 Merge pull request #33312 from overleaf/copilot/send-clear-site-data-header
Send `Clear-Site-Data` header on account deletion

GitOrigin-RevId: c3f0b5f27cc80a1927518c56436c3a808b144fb7
2026-05-07 08:07:20 +00:00
Brian Gough 61f480ca4e Merge pull request #33436 from overleaf/bg-fix-lint-in-codespaces
fix web linting commands to run in codespaces

GitOrigin-RevId: 106572bab1b2a4e13f1da298253c35e4af0b1316
2026-05-07 08:07:11 +00:00
Chris Dryden df01c7e9e1 Merge pull request #33162 from overleaf/cd-upload-generated-files-to-filetree
Cd upload generated files to filetree

GitOrigin-RevId: 2d2774e57c42452fba3a2582fde7153ffcde59bf
2026-05-07 08:07:06 +00:00
Domagoj Kriskovic 8f0979d6a2 Trigger reject tracked changes notification from document-updater
GitOrigin-RevId: 9ac47490d052b3058931ca250f4090e6576f56b2
2026-05-07 08:06:57 +00:00
Domagoj Kriskovic ad58ec2c79 Consolidate duplicate Pyodide output limit tests
GitOrigin-RevId: 8ec631b740736158d6e1e75ccab90136813ffa15
2026-05-07 08:06:52 +00:00
Domagoj Kriskovic 672e01c703 Enforce output file count and size limits in the Pyodide worker
GitOrigin-RevId: 2cc61613381243d810a8cb9e1c2c32fa9e751da7
2026-05-07 08:06:46 +00:00
Jakob Ackermann d97a659f92 [web] make double compile test parameters configurable via env vars (#33406)
* [web] make double compile test parameters configurable via env vars

* [k8s] web: enable double compile test for free compiles on n4 instances

GitOrigin-RevId: 3a5cb8ed6d044fcf3f4c0d2b9d252326bac48511
2026-05-06 08:07:28 +00:00
Tim Down c63d048dd3 Merge pull request #33422 from overleaf/td-pricing-copy-fixes
New pricing page copy fixes

GitOrigin-RevId: cef71065ce7228594c2fb58c77273a607e3ba414
2026-05-06 08:07:23 +00:00
Alf Eaton 37a68a9c5e Reapply "Add Vertex as an AI provider (#32450)" (#33339)
GitOrigin-RevId: d506c99cf32fae97b6721923256bd980120fbeed
2026-05-06 08:07:19 +00:00
Copilot 32f5ac48c7 Use a Firefox-allowlisted Material Symbols family for unfilled editor icons (#32945)
GitOrigin-RevId: 61b7e4044f9a57a2083c5467fa0ec6eaa0f9ae1e
2026-05-06 08:07:14 +00:00
Brian Gough d658dba53c Merge pull request #33421 from overleaf/bg-use-plain-test-output-in-ci
add NO_COLOR environment variable to web CI for cleaner output

GitOrigin-RevId: 1c089333e2002e63b62d55d0e6b4def29e844aa6
2026-05-06 08:07:10 +00:00
Eric Mc Sween 3da2d53acb Merge pull request #33413 from overleaf/revert-33040-em-parse-req-errors
Revert "Replace isZodErrorLike with custom error types in request validation"

GitOrigin-RevId: 1f51fe9e14ffabf283f1229552d3887136420f8f
2026-05-06 08:06:41 +00:00
Eric Mc Sween 98def949ec Merge pull request #33040 from overleaf/em-parse-req-errors
Replace isZodErrorLike with custom error types in request validation

GitOrigin-RevId: 9cb453a2cde595a00f5049e4829ea9e3dbe17b28
2026-05-06 08:06:37 +00:00
Jakob Ackermann 1aa6f03b3c [web] fix make test_unit_watch (#33370)
* [web] check for missing module tsconfig in CI

* [web] add missing tsconfig into test_unit container

GitOrigin-RevId: 7b861f8e68f961e3455b72b5660cf3825389c656
2026-05-06 08:06:26 +00:00
jmescuderowritefull 637ff3aa88 Feedback for new quota messages in new plans (#33357)
GitOrigin-RevId: 41f06b31cd9b60844b2136bffecf966ef378c1c9
2026-05-06 08:06:21 +00:00
Davinder Singh 8b47dedbea [WEB] Update copy for docx import (#33239)
* removing link from translation weve_converted_your_content_to_latex

* adding the translations on choose document modal

* adding beta icon

GitOrigin-RevId: b734447474e41e57efacb589aadf67e4124d4924
2026-05-06 08:06:10 +00:00
Tim Down 77d25eed14 Merge pull request #33342 from overleaf/td-pricing-features-corrections
Fix errors in 2026 pricing page features list

GitOrigin-RevId: 2b62f3dbf0e88676f63818a6f243916178ac9373
2026-05-06 08:06:05 +00:00
Tim Down 479e302027 Merge pull request #33372 from overleaf/td-pricing-geo-banner-margin
Add margin to geo banner in new pricing and interstitial pages

GitOrigin-RevId: 39fafa1c8a5447e20776fcee34dabc54c5ca33bc
2026-05-06 08:05:57 +00:00
Antoine Clausse 590c2ab2e2 [web] Fix AI quota paywall upgrade CTA to use interstitial page (#33376)
* Update hrefs

* Revert link on expired subscriptions

* Revert hrefs of other buttons

* Use `plans-2026-phase-1` feature flag

GitOrigin-RevId: 3fe489c6ec192adc2fb836b07429dc2a11f9a57f
2026-05-06 08:05:53 +00:00
Antoine Clausse cff35c743f [web] Fix wrong price shown in future payments preview when upgrading over a pending downgrade (#33305)
* fix(web): show correct plan in future payments preview when upgrading over a pending downgrade

When a user had a scheduled plan downgrade and then immediately upgraded
to a higher plan, makeChangePreview() always used the pending (stale)
plan code/name/price for the future payments display rather than the
newly selected plan.

Check whether the current change is a plan change (premium-subscription
or group-plan-upgrade type) and if so use subscriptionChange's plan
details instead of pendingChange's, since the immediate upgrade overrides
the scheduled downgrade.

Closes #33299

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(web): add unit tests for makeChangePreview pending-change plan override

Covers the four cases: premium-subscription and group-plan-upgrade types
use subscriptionChange plan (not pendingChange), add-on-purchase type
defers to pendingChange plan, and no-pending-change falls back to
subscriptionChange as before.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
GitOrigin-RevId: cc2f9c88e5dfdfb89370798e857ea98caf8fcf85
2026-05-06 08:05:49 +00:00
Andrew Rumble f434b1fc28 Merge pull request #33149 from overleaf/ar-ja-remove-i18next-additional-packages
[web] remove i18next additional libraries

GitOrigin-RevId: 98fc17b409090db32b02bb66953f1c2e6efee608
2026-05-06 08:05:41 +00:00
Jimmy Domagala-Tang 6a6bb625db feat: removing duplicated error messaging when user hits paywall, now leave any limit messaging to the paywall instead of also handling it in the error assist components (#33337)
GitOrigin-RevId: d3a915e75e5ba4f10c109e8e971b00a84177109a
2026-05-05 08:06:37 +00:00
Jimmy Domagala-Tang cf0f4cb339 feat: updating content and adding new variant for groups (#33289)
GitOrigin-RevId: afecbd92c6a9f224226f3918d94396e2927f104a
2026-05-05 08:06:30 +00:00
Jakob Ackermann d61e3f5521 [web] fix caching of session.analyticsId (#33300)
* [web] fix caching of session.analyticsId

* [web] disable analyticsIdMiddleware tests for Server Pro/CE

GitOrigin-RevId: 2acf76f937adabd62b1e9f876a858211ef7a13c6
2026-05-05 08:06:09 +00:00
Jakob Ackermann 37cc65ec7e [web] consolidate clsi downloads and add zod validation (#33069)
* [web] consolidate clsi downloads and add zod validation

* [validation-tools] make prettier happy

* [web] make clsiServerId optional

* [web] fix type of buildId

* [web] gracefully handle ObjectId

* [web] fix type of buildId

* [monorepo] address review feedback

- cjs export
- update module path in comments
- skip adding ?clsiserverid if not set
- allow nested output file download for submissions and add tests

* [web] address review feedback

* [web] cache one more zod schema

* [web] fix unit tests

GitOrigin-RevId: 0a1e618955983e035defd6d3c0528b81e0e85c95
2026-05-05 08:06:05 +00:00
Malik Glossop e2de08ca86 Merge pull request #33146 from overleaf/copilot/fix-code-folding-bug
Fix code folding when a comment or blank line precedes an indented sectioning command

GitOrigin-RevId: 2a955311c1ce073b2eb80fdfbf45d00705e22d69
2026-05-05 08:05:57 +00:00
Malik Glossop 47473bc5f4 Merge pull request #33044 from overleaf/worktree-mg-writefull-setting
Add writefull "AI Assistance" section

GitOrigin-RevId: c6d4cb60601c0b808cde96f29f6b79b26f631906
2026-05-05 08:05:53 +00:00
Maria Florencia Besteiro Gonzalez ed3f9517fd Merge pull request #33367 from overleaf/mfb-revert-revert-bibtex-loading-spinner
Reapply "BibTeX visual editor: show loading spinner when switching bib files"

GitOrigin-RevId: e71c44948b17cfa9aa9d38aa87842dba05697c38
2026-05-05 08:05:49 +00:00
ilkin-overleaf 5727643852 Merge pull request #33065 from overleaf/ii-share-modal-send-invites
[web] Add send invites input and role selection to share modal

GitOrigin-RevId: f43654e1ca0d8000b2327f1f398fd062ef1b74e4
2026-05-05 08:05:45 +00:00
Kristina fdc939fe0a [web] emit project:membership:changed when removing self from project (#33143)
GitOrigin-RevId: 5a101add69e0077f667f98f5b95c2476ad3085d4
2026-05-05 08:05:34 +00:00
Copilot 799dcf618b [web] Add text-align: left to .notification-content (#33142)
GitOrigin-RevId: 6470f6453f3c31e335863a67d3738972c84fc515
2026-05-05 08:05:30 +00:00
Kristina a6ac7bdd41 [web] catch failed requests to project/doc metadata endpoints (#33139)
Unhandled promise rejections from these fire-and-forget calls were
surfacing in Sentry. Add .catch(debugConsole.error) to suppress them.

GitOrigin-RevId: a14cd0a3956a2b551210723ad56e7ec5e354a7a7
2026-05-05 08:05:26 +00:00
Kristina d73e2b063a [web] add email notification surveys (#33063)
* feat: update survey link in settings modal
* feat: add survey link to email footer

GitOrigin-RevId: acd22281931bb98eebafa7072dca1c54d48cd972
2026-05-05 08:05:21 +00:00
Maria Florencia Besteiro Gonzalez f977a813db Merge pull request #33359 from overleaf/revert-33145-mfb-bug-switching-files-editor-not-immediately-refresh
Revert "BibTeX visual editor: show loading spinner when switching bib files"

GitOrigin-RevId: 509ba1f07e7ff418ad2a30683980100dacb77cc6
2026-05-05 08:05:17 +00:00
Maria Florencia Besteiro Gonzalez 04b6a9762f Merge pull request #33145 from overleaf/mfb-bug-switching-files-editor-not-immediately-refresh
BibTeX visual editor: show loading spinner when switching bib files

GitOrigin-RevId: 0047e6a75273e322490dde0b0fc7889b46f6d469
2026-05-04 08:06:02 +00:00
Simon Gardner feb4f49859 [fix] use Stripe productId rather planCode when adding prices (#33344)
GitOrigin-RevId: 6e24317a0086332145c88a9be3b700b96d7a1187
2026-05-04 08:05:50 +00:00
Kristina 9a670805c4 [scripts] handle creating custom prices (#32607)
GitOrigin-RevId: 3950698ca21de5d8ee87d0531e1e9f562aeb76f8
2026-05-04 08:05:46 +00:00
Andrew Rumble 4954c738da Merge pull request #33328 from overleaf/ar-handle-more-than-40-saas-modules
[web] make sure acceptance tests run when there are more than 40 modules

GitOrigin-RevId: 6aad027448f2dcdc5c0a8e0bbd4120c514b9a0ca
2026-05-04 08:05:41 +00:00
Mathias Jakobsen c67885919b Merge pull request #33141 from overleaf/mj-tutorials-show-one
[web] Ensure only one tutorial shows at once

GitOrigin-RevId: 797c677a3d45635451485d79ed1c0705819ed5ad
2026-05-01 08:07:29 +00:00
Tom Wells ffafccdba3 Don't preselect entry type when adding a BibTeX reference (#33193)
GitOrigin-RevId: a809b277fb0db8962ea0eb0e8c22af6775b2c832
2026-05-01 08:07:24 +00:00
Andrew Rumble 30bedf3913 Merge pull request #33278 from overleaf/ar-mixpanel-autocapture
[web] mixpanel autocapture

GitOrigin-RevId: 81f6a11ae968da4c13a28e202dd3ed1113f365d4
2026-05-01 08:07:20 +00:00
Mathias Jakobsen 42f7bca37e Merge pull request #33317 from overleaf/mj-menu-bar-role
[web] Fix aria roles for nested menu bar dropdowns

GitOrigin-RevId: 1c285c2ef8ed0c589bd6b0df6112c054c8662ca4
2026-05-01 08:07:09 +00:00
Olzhas Askar 823f11426b Merge pull request #33109 from overleaf/oa-upgrade-path
[web] Upgrade path

GitOrigin-RevId: 532993e613bdc42cf92a7b10e629aa94596d854e
2026-05-01 08:07:01 +00:00
Davinder Singh 30e0e6adaf adding side menu for download option (#33307)
* adding side menu for download option

* fixing the E2E tests to adapt the new behaviour

GitOrigin-RevId: d96df4906a40006d36ac0ea525d74a1644ec4085
2026-05-01 08:06:57 +00:00
Alex Vizcaino 2ebe0bd513 fix: add unique key to GetPremium component in ReplacementsCard (#33303)
* fix: add unique key to GetPremium component in ReplacementsCard

* fix: update paywall messages for clarity in English and Spanish

GitOrigin-RevId: 3422ef2fbf049fe1c2cc20f6f8d224b4d67374ca
2026-05-01 08:06:49 +00:00
Antoine Clausse 8da6222a89 [web] Fix plans CTA plain-link and edu discount clickable area overflow (#33243)
* Update .plans-cta-plain-link styles so the clickable area doesn't overflow

* Update .plans-educational-discount-label styles so the clickable area doesn't overflow

* Fix lint

GitOrigin-RevId: cedbaa78a079fd4f7cefe2be9b39252d30ba6355
2026-05-01 08:06:45 +00:00
Antoine Clausse dd44f4e2e8 [web] Remove stale "You already have a subscription" notification (#33187)
* Remove stale "You already have a subscription" notification after cancel/plan change

The notification was derived from a server-rendered meta tag set at page load,
so it persisted through cancel and plan-change flows. Now derived directly from
the URL param on the client; the param is stripped on cancel button click
(replaceState) and before plan-change reloads (location.replace via
reloadWithoutHasSubscription helper).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix format

* Update services/web/test/frontend/features/subscription/components/dashboard/subscription-dashboard.test.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix change-plan tests after location.reload → location.replace migration

reloadWithoutHasSubscription calls location.replace() not location.reload(),
so update assertions accordingly. Also stub toString() to return the jsdom
origin so FlashMessage's replaceState call doesn't throw a SecurityError.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Guard reloadWithoutHasSubscription against empty URL

When called after component unmount, useLocation's toString() returns '',
causing new URL('') to throw. No-op early to avoid the exception.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Guard against empty URL in history state replacement for subscription cancellation

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
GitOrigin-RevId: 8408ee971adf038e2d819eae5df060ace62a7e14
2026-05-01 08:06:41 +00:00
Antoine Clausse 353c681d51 [web] Disable AI Assist add-on purchase for plans-2026-phase-1 users (#33178)
Users in the plans-2026-phase-1=enabled split test can no longer
purchase the AI Assist add-on via crafted HTTP requests. The preview
and purchase endpoints return 404/redirect for these users.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
GitOrigin-RevId: 2c75eb622cf44dc91019a692290ac646b51fd72c
2026-05-01 08:06:37 +00:00
Alf Eaton e3f88791da Revert "Add Vertex as an AI provider (#32450)" (#33309)
This reverts commit 20d895350ee13a7683f178bc83b87f0e765c7af6.

GitOrigin-RevId: 6be06b0fee0b038c42db45fce2377efd5d5a47dc
2026-05-01 08:06:32 +00:00
Alf Eaton f00dab5cc0 Add Vertex as an AI provider (#32450)
GitOrigin-RevId: 20d895350ee13a7683f178bc83b87f0e765c7af6
2026-05-01 08:06:25 +00:00
Alf Eaton 94018faafc Ensure tsconfig.json in all modules/*/app/src folders (#33060)
GitOrigin-RevId: 998d9c32bd11d77ee371e8bfd96fa201cf6950b2
2026-05-01 08:06:17 +00:00
Alf Eaton 664cc65ab0 Use CSF Next (#32998)
GitOrigin-RevId: 498e15e3ae44f0a7b85e889e199d71607c420e12
2026-05-01 08:06:13 +00:00
Brian Gough c19c25d113 Merge pull request #32874 from overleaf/bg-promisify-archive-manager
Promisify ArchiveManager

GitOrigin-RevId: 7a3ee3dfb2f07dc06ee894cfce0a9196622c878e
2026-05-01 08:06:05 +00:00
Brian Gough 8a8679eb78 Merge pull request #33297 from overleaf/bg-fix-acceptance-tests-in-dev-env
fix: update migration commands to use yarn instead of east
GitOrigin-RevId: 9cfcc393cfc1855e1edba6ba90ebb7b3a76ecb6b
2026-05-01 08:06:01 +00:00
Brian Gough 7f7556cf6a Merge pull request #33219 from overleaf/bg-fix-collapsible-panel-flicker
Fix flicker when switching between editor and PDF views

GitOrigin-RevId: 1f6543c0046dc458fa174aa9b54985934a7437fa
2026-05-01 08:05:56 +00:00
Brian Gough eec3be362b Merge pull request #33172 from overleaf/bg-fix-project-upload-unlinks
fix: use fsPromises ins ProjectUploadController async functions for consistency
GitOrigin-RevId: beb858d9b6cf50431fb14626dfd7cddfaf093882
2026-05-01 08:05:52 +00:00
roo hutton 970bc85b78 Merge pull request #33247 from overleaf/rh-cio-fix-ai-group-enabled
Base group ai enabled cio attribute on group policy

GitOrigin-RevId: 2b2411aec3ffc694d2570e6031e9a876a1575e2c
2026-05-01 08:05:44 +00:00
Mathias Jakobsen a478c1a829 [web+project-history] Add lastResyncedAt field to projects and set after resync (#33240)
Co-authored-by: Jakob Ackermann <jakob.ackermann@overleaf.com>
GitOrigin-RevId: f679a190b88694a46f7816d51eff96446f338dec
2026-04-30 08:06:43 +00:00
Jakob Ackermann 50abfe8f0c [migrations] add migration for back filling db.users.analyticsId (#33115)
* [migrations] add migration for back filling db.users.analyticsId

Co-authored-by: Davinder Singh <davinder.singh@overleaf.com>

* [web] add acceptance test for backfilling db.users.analyticsId

---------

Co-authored-by: Davinder Singh <davinder.singh@overleaf.com>
GitOrigin-RevId: a0840969ac0c4c84e874c6f00cf0a78857a4bb06
2026-04-30 08:06:38 +00:00
Jakob Ackermann 4e138e6f99 [web] make labs releases available to E2E tests/Codespaces (#33242)
GitOrigin-RevId: a4e6a6bd6ffb7f3e9331aa81daf19e22b894d771
2026-04-30 08:06:34 +00:00
Jimmy Domagala-Tang 41b96ec8d6 feat: ai quotas should reset when a new plan purchase is made or upgraded (#33095)
GitOrigin-RevId: 9034800e067426fc22f8f86f9d7309699797d02e
2026-04-30 08:06:30 +00:00
Jimmy Domagala-Tang 730ff8f0ea feat: add a utilitty script for setting a users AI usage, useful for testing in staging (#33216)
GitOrigin-RevId: 71d4948235ccf37971142a4ed917cff2ef50cea9
2026-04-30 08:06:26 +00:00
Jimmy Domagala-Tang 994932b8e3 [Web + Doc-Updater] Add track changes accepted notification (#32752)
* feat: update doc manager to return a list of contributors to the accepted change

* feat: add new notification type for accepting a tracked change

* update email with tracked changes accepted

* feat: update tests

* fix: feedback on consistent api and returns

* feat: adding new tests

* feat: self accepted changes shouldnt trigger notification, and using existing changesAccepted hook

* Add better subject and activity list for track change accepted (#33094)

* feat: add better activity list entry and subject header for accepted changes, to match other notifications

* feat: updating tests

* feat: updating accepting_user_id to just user_id

* fix: adding users in emailBuilder test to userCache

GitOrigin-RevId: 6114f77916b5f503b7bbbb5ca8fed99e58edc31b
2026-04-30 08:06:19 +00:00
Olzhas Askar fe59b9cdb5 Merge pull request #33245 from overleaf/oa-faq
[web] 2026 FAQ fixes

GitOrigin-RevId: e70a6080341ea63d3668794f50baf7538f09f55b
2026-04-30 08:06:02 +00:00
jmescuderowritefull 35dc3ab790 Enhance AI usage messaging (#33105)
GitOrigin-RevId: 0bd325d806d945366abb6e7d8cc3cd177cb66ef2
2026-04-30 08:05:57 +00:00
Kate Crichton 20f08ab8c1 Merge pull request #32855 from overleaf/kc-move-token-rendering
[web] move domain verification token rendering to collapsible section

GitOrigin-RevId: c34b0f66f56c4e9942a53c2a2383d7a8cbdfdf7c
2026-04-30 08:05:48 +00:00
Kate Crichton e7cfcbdc66 Merge pull request #32503 from overleaf/kc-domain-verification-error-messages
[web] domain verification error messages

GitOrigin-RevId: a5a04934a8675741c4dc6a4779c2c90cbf54c04d
2026-04-30 08:05:37 +00:00
Domagoj Kriskovic 117a783f21 [web] Add .py to defaultTextExtensions
GitOrigin-RevId: 0880c01f6e3b4267f6cb790a44c6094738ab2229
2026-04-30 08:05:30 +00:00
Domagoj Kriskovic 9e677a2c1e Use overleaf CDN for loading pyodide packages
GitOrigin-RevId: e17ff3387166421a546a9519786d77ba12cdffc4
2026-04-30 08:05:23 +00:00
Kristina a46ca0705f [web] update refund policy in zh-CN from 30 days to 14 days (#33236)
GitOrigin-RevId: 27f6b116b2cc344449417e7e32f7b93784919977
2026-04-30 08:05:18 +00:00
Chris Dryden 6c9560cd4e Merge pull request #32943 from overleaf/cd-auto-install-python-packages
Auto-install python packages from the executing python script

GitOrigin-RevId: e343312d61e1804d927688bf4e0de00b2bdb5382
2026-04-30 08:05:14 +00:00
Olzhas Askar 7c0595f9a9 Merge pull request #32063 from overleaf/oa-group-plans
[web] Rename group plans

GitOrigin-RevId: 8a3097dc1724709b5d7b163e0f9d968c21d63831
2026-04-30 08:05:10 +00:00
Mathias Jakobsen 92bbba5e50 Merge pull request #33046 from overleaf/mj-batch-history-resync-script
[web] Add script for batch history resyncs

GitOrigin-RevId: 2409475fa1ba12dadfaae9641a5fafdaa6c88e47
2026-04-29 08:06:12 +00:00
Copilot e59cbc61cf Merge pull request #33134 from overleaf/copilot/fix-autocomplete-subparagraph
GitOrigin-RevId: 2ca0b9ab5de393b9b0a256c81450f57d933376fc
2026-04-29 08:06:08 +00:00
Andrew Rumble 2f51ad5180 Merge pull request #32782 from overleaf/ar-allow-types-to-pass-through-for-parseReq
[web] fix typing of parseReq

GitOrigin-RevId: 6f10ed8682af7c999497e3a9fbd77a9d25bd7c28
2026-04-29 08:06:04 +00:00
Anna Claire Fields 9f8e5582d5 Fix metrics pages broken by Yarn 4 migration (#33190)
GitOrigin-RevId: 9bb3462248a16bfab425f6ebc884e9210d872b9c
2026-04-29 08:05:49 +00:00
Jakob Ackermann da11cf2669 [monorepo] fix running tests locally (#33186)
* [monorepo] fix running tests locally

* [web] fix east invocation

* [web] move corepack into entrypoint

* [web] fix running module acceptance tests locally

* [web] fix merged module targets

* [web] remove spurious change

GitOrigin-RevId: 84cf7f8d768d3429c8a98c789acf76f6cecd6e3e
2026-04-29 08:05:42 +00:00
Olzhas Askar 396e158ecd Merge pull request #31991 from overleaf/oa-rename-student-plans
[web] Rename Individual plan names

GitOrigin-RevId: f0133fc4e06542d7c68e0a0211a2ecc32afa733d
2026-04-29 08:05:37 +00:00
Malik Glossop da027f46cf Merge pull request #32530 from overleaf/mg-stuck-sync-repro
Detect and auto-clear stuck sync states

GitOrigin-RevId: 680861a33e42432dab7d40ad421980b707eb6089
2026-04-29 08:05:33 +00:00
Malik Glossop ba182f8275 Merge pull request #32710 from overleaf/mg-project-history-metrics
Add diagnostic annotations to LazyStringFileData toEager errors

GitOrigin-RevId: 47575586bb869d65e4eb443cc9f1215b6f245255
2026-04-29 08:05:28 +00:00
Tim Down 6296e7911c Merge pull request #33169 from overleaf/td-pricing-interstitial-fixes
Interstitial 2026: wire up upgrade CTAs and fix card highlights

GitOrigin-RevId: 2feea397846452ff79c7cd01c931627097a03954
2026-04-29 08:05:20 +00:00
Jakob Ackermann 68c41e9b66 [history-v1] fix re-running test_acceptance_run (#33183)
* [history-v1] fix re-running test_acceptance_run

Preserve the contents of the migrations collection. Deleting the entries
does not "undo" the migrations. On re-run east would try applying all
migrations and fail as things were already applied.

* [history-v1] remove guard migration before running tests

GitOrigin-RevId: e6eeafd58215e5148dd70c37c7a87d84e0a12bf3
2026-04-29 08:05:16 +00:00
Jakob Ackermann f3ec774e6b [github] code spaces: add extra-split-tests.json (#33015)
* [github] code spaces: add extra-split-tests.json

* [github] address review feedback

* [github] add missing bind mount

* [monorepo] mount split test configs with :ro

* [monorepo] only mount backup/split-tests into web

GitOrigin-RevId: 6334a44599a2c8bb79bf1ad698e656c8bee3992b
2026-04-29 08:05:08 +00:00
Anna Claire Fields 0d64a88a46 Yarn 4 Migration (#32253)
Migrates the Overleaf monorepo package manager from npm (v11) to Yarn 4 (v4.9.1) using node-modules linker mode.

GitOrigin-RevId: 50d32ab01955c15e29679eff9e9e9cfb897fab2d
2026-04-28 08:52:37 +00:00
Lucie Germain ed0fb0110a Merge pull request #33083 from overleaf/lg-npm-audit-fix-quay-logging
Run npm audit fix --force in quay-logging

GitOrigin-RevId: 6dbca8519ce0e3077f29bec17127c00086545500
2026-04-28 08:52:27 +00:00
Eric Mc Sween 557da351c4 Merge pull request #32755 from overleaf/em-tpds-merge-worker
Add tpdsworker functionality to third-party-datastore and redirect web traffic

GitOrigin-RevId: 94a514a6a1b10ce6126feb27ce604bcd4f42cda1
2026-04-28 08:07:40 +00:00
Brian Gough 50648d6ed9 Merge pull request #33138 from overleaf/bg-acf-web-graceful-shutdown
[web] add fast shutdown for development environment on SIGTERM

GitOrigin-RevId: eb82171144bfe4d4f6bafa5e20773a008eeb13af
2026-04-28 08:07:35 +00:00
Brian Gough 18b2308887 Merge pull request #32835 from overleaf/bg-fix-potential-race-condition-in-archive-manager
fix potential race condition in extractZipArchive

GitOrigin-RevId: 6dc77443e8a58172825d2b03645da05a9887e468
2026-04-28 08:07:26 +00:00
l-obrien-overleaf 6b78b42469 visual updates to optional fields in manual bib entry (#33102)
* visual updates to optional fields in manual bib entry

* omitting onClose from button auto complete props

GitOrigin-RevId: 922695e8eaec83702b482123dc4b4483fe957b78
2026-04-28 08:07:21 +00:00
Domagoj Kriskovic 7eee5809bb [pyodide] collect output files from worker and include in RunCodeResult
GitOrigin-RevId: fa9d501933ee32729e3d083183cd2a14ff969e95
2026-04-28 08:07:16 +00:00
l-obrien-overleaf 966a2f9bfe BibTeX editor: add preview screen to "Paste references" dialog (#32928)
* Adding preview panel for bibtex entry

* Removing bibtex-editor module directory and moving tests to library module

* Format fix

* Adding accessibility headers and hardening tests

* Simplfying preview logic for bibtex titles

* Only show author placeholder if no author present

* Fixing build failures

* Fixing broken test

GitOrigin-RevId: 23d2d4ff48fe8135687578cd3efdf8ba9395e411
2026-04-28 08:07:11 +00:00
Domagoj Kriskovic 4e51eee4f8 scss: use content-info variable
GitOrigin-RevId: 925d1ea2dc3496194388b6a347d1c0ab41ced52b
2026-04-28 08:07:02 +00:00
Domagoj Kriskovic e3a1bce113 Add tests for stderr output handling and execution interruption in PythonOutputPane
GitOrigin-RevId: d383e90dffbabbb597a9cf793a6fabeeff9d6b1a
2026-04-28 08:06:57 +00:00
Domagoj Kriskovic 3a232c44eb Extract OutputStream type for python runner streams
GitOrigin-RevId: 9a26c847ec7daa4cd446e9fec0407e64e6f40916
2026-04-28 08:06:52 +00:00
Domagoj Kriskovic 09af91a936 info stream
GitOrigin-RevId: 5d4cb01c250768ca00e15368b9c616b467e4f9ba
2026-04-28 08:06:47 +00:00
ilkin-overleaf 6ccbefc3f8 Merge pull request #32941 from overleaf/ii-share-modal-manage-access
[web] Share modal redesign: invited people screen

GitOrigin-RevId: bf7f83dccc245e41eca8087fc27c1b411f025b0d
2026-04-28 08:06:42 +00:00
Brian Gough e6861ab6fa Merge pull request #33023 from overleaf/copilot/add-warning-for-unprocessed-projects
Clarify clear_deleted.js completion output for force mode and partial processing

GitOrigin-RevId: 11d9595e5b43bb8df8e1c7f664f4c08c3fbd4509
2026-04-28 08:06:36 +00:00
Simon Gardner 8a331bc943 fix: [web] managed group admins unable to re-subscribe (#29634)
GitOrigin-RevId: 5e3d46c39f4657fcc737403a80093b870bc42240
2026-04-28 08:06:27 +00:00
Tom Wells 73cc1b571b Add DS nav page switcher behind overleaf-library flag (#33112)
* Add DS nav page switcher behind overleaf-library flag

- Add shared DsNavPageSwitcher component (Library/Projects nav links + logo)
- Show page switcher in projects sidebar when overleaf-library flag enabled
- Hide 'All projects' filter and sidebar New Project button behind flag
- Move New Project button to content area header when flag enabled
- Prevent full page reload when clicking active nav item
- Change Upgrade button to premium variant when flag enabled
- Add overleaf-library split test to ProjectListController
- Add library-page class to remove rounded corner on /library
- Add Cypress component tests for DsNavPageSwitcher

Closes #33092

GitOrigin-RevId: 2e348da8307bf944d481b54b3a2bcc2eb319e18e
2026-04-28 08:06:22 +00:00
ilkin-overleaf 816f8c45eb Merge pull request #32924 from overleaf/ii-share-modal-basic-layout
[web] Share modal redesign base layout

GitOrigin-RevId: 0f011d03762c6669a0fd3b1fc2af32c9026c7ea4
2026-04-28 08:06:13 +00:00
ilkin-overleaf 5d3d58e8f4 Merge pull request #32801 from overleaf/ii-fix-mobile-trash-delete
[web] Fix Delete/Leave dropdown items not working on mobile trashed projects list

GitOrigin-RevId: ce7c79f0c77bb1f0df023159ee6c463c577e26e1
2026-04-28 08:06:07 +00:00
Kristina b556fd40b5 [web] support email sender customization for email notifications (#33035)
GitOrigin-RevId: 9aa298c233c1f314ef3bdb381c20692bd0d4e212
2026-04-27 08:06:35 +00:00
Kristina 4db3982c08 [web] rename BaseWithHeaderEmailLayout -> BaseEmailLayout (#33026)
GitOrigin-RevId: 16967d34d5128a34ec9ddf382eb664e5a8e45065
2026-04-27 08:06:31 +00:00
Eric Mc Sween a52c47ebee Merge pull request #33090 from overleaf/em-fix-library-entry-list
Show library entry list on /library page

GitOrigin-RevId: 4336ec6803cca656092c190c3bce9f92d6923a47
2026-04-27 08:06:17 +00:00
Antoine Clausse e8ea298ee4 [web] Update paywall copy for plans-2026 AI tiers (#33070)
* [web] Update paywall copy for plans-2026 AI tiers

Replace AI-related strings in upgrade/paywall UI when the
plans-2026-phase-1 flag is enabled:

- Add `higher_ai_allowance` ("Higher AI allowance") for paywall bullet
  lists and body text; keep `higher_ai_limits` ("Higher AI limits") for
  the subscriptions dashboard, which uses different wording per spec
- Update three locale strings to use "allowance" instead of "limits":
  `access_all_premium_features_ai`, `plus_additional_collaborators_and_more`,
  `upgrade_to_add_more_collaborators_and_more`
- Fix casing: `upgrade_to_review` "Upgrade to Review" → "Upgrade to review"
- Switch upgrade-benefits and onboarding prompt from `higher_ai_limits`
  to `higher_ai_allowance`
- Fix subscriptions free-plan to show `higher_ai_limits` instead of
  `get_unlimited_ai` under the plans2026 flag
- Fix compile-timeout button to show "Start free trial" under plans2026
  instead of "Start a free trial"
- Update two Cypress assertions for the new track-changes modal title

* Update extracted translations for higher AI allowance

* Remove "start a free trial" with exclamation

* Remove "get unlimited ai" translation entries

GitOrigin-RevId: 12300d94dc81c5407a21d4682d5714d7284c31b0
2026-04-27 08:06:13 +00:00
Antoine Clausse 8fcf007a73 [web] plans-2026: responsive card layout (mobile 1-col, md 2-col, lg 4-col) (#32905)
* plans-2026: wrap cards to 2-col at md, 4-col at lg

Replaces horizontal scroll at md/lg with a wrapping CSS grid so cards
stack in rows instead of requiring horizontal scrolling. The 4-column
layout now kicks in at lg (~992px) rather than xl (~1200px).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* plans-2026: replace mobile horizontal scroll with 1-col stack

Removes the overflow-x scroll behaviour at xs/sm (and the
scrollbar-overlap margin/padding hack that came with it). Cards now
stack in a single column on small screens, consistent with the
2-col/4-col grid above.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix lint

* plans-2026: fix student mode 2-col at lg, remove mobile min-width

Student-mode 2-col override was only kicking in at xl; move it to lg so
the two visible cards (free + student) fill the row correctly. Also drop
the 280px min-width on .plans-card-col — now that mobile uses a 1-col
grid instead of horizontal scroll, it causes unnecessary overflow on
narrow viewports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update breakpoint for plans cards layout to lg for improved responsiveness

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
GitOrigin-RevId: 5ef923dd3795353bd946c1857e78f4b5f7063ab0
2026-04-27 08:06:09 +00:00
Tim Down fb6e2f17cb Initial version of 2026 interstitial plans page with upgrade mode (#33036)
GitOrigin-RevId: e9da7c1464c6e05e9c70a11d9ad7a3632f189de2
2026-04-27 08:05:55 +00:00
Jakob Ackermann 633705e604 [server-ce] tests: extend debug logs (#33098)
GitOrigin-RevId: 5283eb8ab39f94ae8389cc4ccc3f288d401d3e52
2026-04-27 08:05:51 +00:00
Tim Down 901ec15bc1 Merge pull request #32916 from overleaf/td-pricing-features-follow-up
2026 pricing page feature comparison table polish

GitOrigin-RevId: 5ebad4f1bd7c296dff2277a7f2ddd6324597b244
2026-04-27 08:05:46 +00:00
Mathias Jakobsen c46fba951e [web] Re-introduce orphan detection in detached PDF (#32994)
GitOrigin-RevId: 07a58d6f7e3c6db8465c62b390e34270c2b4fd67
2026-04-27 08:05:42 +00:00
Mathias Jakobsen 5e675664c6 [web] Cleanup PDFjs instances in visual editor (#33022)
GitOrigin-RevId: 2aa9ab01f88196fb56dc41749977ca33295c964f
2026-04-27 08:05:38 +00:00
jmescuderowritefull 5c7db11d9b fix: fixes for new plans (#33051)
GitOrigin-RevId: 07ff096da2bc72483eab9a5c57417ced950f56c4
2026-04-27 08:05:34 +00:00
Copilot 713a0c15ef Remove prefetch of 5.0.1-RC1 image in Server Pro E2E tests (#32991)
* Initial plan

* Remove prefetch of 5.0.1-RC1 image in Server Pro E2E tests

Agent-Logs-Url: https://github.com/overleaf/internal/sessions/db75e124-b3a1-463a-a41b-bd570af482ef

Co-authored-by: das7pad <17931887+das7pad@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: das7pad <17931887+das7pad@users.noreply.github.com>
GitOrigin-RevId: 9afbba3495706ed11a9392979138c06386b66c88
2026-04-24 08:07:18 +00:00
Eric Mc Sween 60cc551c4d Merge pull request #33085 from overleaf/revert-32956-em-library-entry-list
Revert "Show library entry list on /library page"

GitOrigin-RevId: c2bb4d240f5e07c5a3ddfca62fc5749a3e5c56ab
2026-04-24 08:07:13 +00:00
Eric Mc Sween 2fdecb5d19 Merge pull request #32956 from overleaf/em-library-entry-list
Show library entry list on /library page

GitOrigin-RevId: 6c1c4e3ef66ea002525e5d5adb7943123f5d2587
2026-04-24 08:07:08 +00:00
Andrew Rumble 54d953cfff Merge pull request #32743 from overleaf/ar-new-v1-api
[web] new v1 api client

GitOrigin-RevId: 7ba0deef0d10526198a2a6ba997c2dcff7b7e5a5
2026-04-24 08:07:03 +00:00
Olzhas Askar d3b60bcb46 Merge pull request #31750 from overleaf/oa-update-student-features
[web] Update student collaborators

GitOrigin-RevId: fa4631c4d6e95eaa4823d1d5db125231c6cd0c00
2026-04-24 08:06:58 +00:00
Maria Florencia Besteiro Gonzalez 879c082587 Merge pull request #32923 from overleaf/mfb-replace-placeholders-with-helper-text-bibtex-form
[web] Replace placeholders with helper text in BibTeX entry form

GitOrigin-RevId: b7d4fef0414bc79abda6fba2c4c9594edd9f05fc
2026-04-24 08:06:53 +00:00
Jessica Lawshe 7ff114bbef Merge pull request #32396 from overleaf/jel-unlink-sso
[web] Add button so user can unlink Commons SSO

GitOrigin-RevId: 46e0607549341a98beca3873ea63bf091a883e85
2026-04-24 08:06:48 +00:00
Miguel Serrano 08701ca5aa [web] Add update_group_policy script (#31715)
* [web]  Add `update_group_policy` script

The script allows to update a policy for a subscription, or update
multiple policies via a file containing a list of subscription ids

GitOrigin-RevId: f3b3a6666eb62bb93d13c4c75d60b99f21f23db4
2026-04-24 08:06:35 +00:00
Davinder Singh be5a7b56c8 [WEB + CLSI] Download as docx file feature (#32851)
* using CLSI logic for fetching the project contents and skip the .zip export

* Use unique conversion directory for project-to-docx export to avoid corrupting the shared compile
  directory when a compile runs concurrently

* Remove X-Accel-Buffering header — not needed as CLSI does not run behind nginx

* moving log before sending the data

* Return CLSI stream directly instead of buffering to disk on web

  Previously convertProjectToDocx wrote the CLSI response to a temp file
  on disk, then the controller read it back to stream to the client.
  Now the stream is returned directly and piped to the response,
  avoiding unnecessary disk I/O on the web server.

* Use href redirect for docx export instead of fetching blob into memory

* making functions and files more generic so they can be used in future for other documents exports as well

* adding export-docx split test

* adding unit tests

* adding cypress E2E test

* format:fix

* renaming the route to download from convert

* adding new icon for export docx button

* format:fix

* remove unused showExportDocumentErrorToast export and adding guard against invalid Content-Length header from CLSI

* format:fix

* refactor(clsi): move promisify(parse) into RequestParser

* refactor: generic conversion endpoint with type as route
  param

* refactor: use type→extension map for validated conversion types

* refactor(clsi): remove --standalone flag and fix rejection test

* fixing the href in cypress test

* renaming function

* adding type to Metrics.inc

* fix: rename exportProjectDocument, add WithLock wrapper and metrics type label

* format:fix

* fix: hide docx export from anonymous users and add WithLock wrapper

* format fix

* remove redundant Content-Length validation from DocumentConversionManager

* format:fix

* removing trailing icon

GitOrigin-RevId: e9764fefac2c4b625d23be9e942ea4a8b283c70d
2026-04-24 08:06:10 +00:00
Kristina b6ec7945f4 [web] update copy in email notifications (#32912)
* add footerMessage to base email template
* add customized subject line and CTA
* add _getBundledActivityList

GitOrigin-RevId: e70c0955485b0892f31e20daa0430faef80b0d64
2026-04-23 08:07:01 +00:00
Kristina a64d1bbb6a [web] display global off treatment in settings modal (#32942)
* display global disabled state
* show loading indicator while project notification preferences load

GitOrigin-RevId: d7e905aaa3fc7b15b54bf99caeabf60c1e5d8050
2026-04-23 08:06:56 +00:00
Kristina 9a129e7cab Merge pull request #32939 from overleaf/kh-update-settings-modal-language
[web] update copy & layout for notifications settings

GitOrigin-RevId: 13b6b78b0b2bddd9d0137362a14bfdbdba9d7d20
2026-04-23 08:06:47 +00:00
Eric Mc Sween 6ad758830a Merge pull request #32555 from overleaf/em-bibtex-warnings
Fix duplicate errors in BibTeX entry list tooltip

GitOrigin-RevId: de7214a7f5d403552505f960f140768bb4f2c1ca
2026-04-23 08:06:31 +00:00
Jakob Ackermann 5c317644bd [server-ce] test: Add debugging in Jenkins (#33019)
GitOrigin-RevId: dd38d580109e870a966d1442a7c4133cd05ca3a3
2026-04-23 08:06:22 +00:00
Brian Gough faf2fd2287 Merge pull request #32996 from overleaf/copilot/fix-race-condition-in-stopcompile
Stabilize stopCompile Cypress helper by waiting for enabled “Stop compilation” action

GitOrigin-RevId: 16997aaccd8d65d5ff0a0a1af73a0bf5d6803832
2026-04-23 08:06:17 +00:00
roo hutton dece22ba92 Merge pull request #32871 from overleaf/rh-cio-comms-attributes
Expose remaining marketing properties to customer.io

GitOrigin-RevId: 6956e1faf90ecc650108404fe13b2f6de2eb4d0c
2026-04-23 08:06:04 +00:00
Mathias Jakobsen 2e5d7675ba Merge pull request #32983 from overleaf/mj-change-layout-button-tests
[web] Add tests for ChangeLayoutButton

GitOrigin-RevId: 30299625fc7f3977eedcb86a99937e3a9321bd2c
2026-04-22 08:06:54 +00:00
Mathias Jakobsen 1d37483e73 Merge pull request #32984 from overleaf/mj-test-runner-cypress-env
[skills] Describe cypress environment variables for running specific cypress tests

GitOrigin-RevId: db64ed3f0a4d9ecbe6b7d63c9275314da0df7421
2026-04-22 08:06:49 +00:00
Anna Claire Fields 36a8447bb3 update handlebars: package json and package lock (#32963)
GitOrigin-RevId: b39d2189f08b76ed61b14e77f2af20f36c9a2968
2026-04-22 08:06:44 +00:00
Alf Eaton b378f2b094 Remove flaky "doc version recovery" SaaS E2E Cypress test (#32986)
GitOrigin-RevId: b5fe75ee45539fabac2a05177a9e6fce36087f70
2026-04-22 08:06:39 +00:00
Alf Eaton fdadfaab45 Remove non-LaTeX file extensions (#32339)
GitOrigin-RevId: 370a27838ddc017b5b0926e47d550729d54d73aa
2026-04-22 08:06:35 +00:00
Alf Eaton ce4ca192ee Upgrade Storybook to v10 (#30442)
GitOrigin-RevId: 9f51624bc2b34b6746d1854969173b44c9c9cf9a
2026-04-22 08:06:26 +00:00
Alf Eaton f45eec25f4 Upgrade Cypress (#28858)
GitOrigin-RevId: a5bb02d4f13e5d1aa4426debd4861205d51597f3
2026-04-22 08:06:21 +00:00
Brian Gough f07aaf1979 Merge pull request #32954 from overleaf/fix/security-oauth2-server-web
[web] upgrade @node-oauth/oauth2-server to 5.3.0

GitOrigin-RevId: e6b25a6fb3dfaef1e8690fc1dd434daab35b798d
2026-04-22 08:06:12 +00:00
Alexandre Bourdin 77b9d2fd1b Merge pull request #32959 from overleaf/ab-fix-32861-spam-safe-project-name
[saas-e2e] Fix project name generation still producing + and / characters

GitOrigin-RevId: 2367c8d45ee9ec4441e9ee2a1d74c2fd281f9e90
2026-04-22 08:06:04 +00:00
Jakob Ackermann 926d6bccd7 [clsi] reduce write traffic to clsi-cache from free users (#32958)
GitOrigin-RevId: c01cc21c3c82f361c7d82843e65bb087b21434ed
2026-04-22 08:05:59 +00:00
Andrew Rumble 98dbb48c59 Merge pull request #32797 from overleaf/upgrade/path-to-regexp-0.1.13-8.4.0
[monorepo] upgrade path-to-regexp

GitOrigin-RevId: 27a6e32594957407807e88c5d9eb62e9399ccaf5
2026-04-22 08:05:50 +00:00
Andrew Rumble bddc1d3fab Merge pull request #32906 from overleaf/ar-upgrade-protobufjs-7.5.5
[monorepo] upgrade protobufjs 7.5.5

GitOrigin-RevId: 7a8038737f8160e55031fa0365242f22c2dd97f5
2026-04-22 08:05:45 +00:00
Andrew Rumble b98688b190 Merge pull request #32795 from overleaf/upgrade/defu-6.1.5
[monorepo] upgrade defu to 6.1.5

GitOrigin-RevId: db1fb6f0150b67785760726d9c681703061d729d
2026-04-22 08:05:40 +00:00
1163 changed files with 89601 additions and 80000 deletions
+385
View File
@@ -0,0 +1,385 @@
name: Build and Deploy Verso (prod)
# Production deploy. Triggered only by pushes to the `prod` branch — keep `main`
# for day-to-day work and fast-forward `prod` when a build is stable.
#
# Differences from the test deploy (deploy-verso.yml):
# - Runs in the `verso` namespace (test runs in `test`).
# - Mongo / Redis / app data live on PersistentVolumeClaims and are applied
# idempotently: this workflow NEVER deletes them, so data survives deploys.
# - The replica set is initialised only once.
# - Builds/pushes a distinct image tag (verso:stable) so prod and test never
# clobber each other's image.
# - SMTP comes from the `verso-smtp` Secret (create it with kubectl); email is
# optional so the app still boots before the secret exists.
# - Public self-registration stays off (CE default): friends-only, admin
# creates accounts / sends invites.
#
# Out of band (do once): create the PVCs (server-ce/k8s/verso-prod-pvcs.yaml,
# with your storageClass), the `verso-smtp` Secret, and a verso.alocoq.fr
# Ingress (see server-ce/k8s/verso-prod-ingress.example.yaml) + DNS.
on:
push:
branches:
- prod
workflow_dispatch:
env:
SITE_URL: https://verso.alocoq.fr
jobs:
deploy:
runs-on: native
timeout-minutes: 240
steps:
- name: Build and push Verso prod image with BuildKit
run: |
kubectl -n ci delete job verso-buildkit-prod --ignore-not-found=true --wait=true
cat <<'EOF' | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: verso-buildkit-prod
namespace: ci
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
initContainers:
- name: prepare
image: alpine/git:latest
command: ["sh", "-c"]
args:
- |
set -eux
REG=registry.git.svc.cluster.local:5000
git clone --depth 1 --branch prod https://git.alocoq.fr/alois/verso.git /workspace/repo
# Build the base image only when Dockerfile-base changes
# (content-hash tag); otherwise reuse the cached base.
BTAG=$(sha256sum /workspace/repo/server-ce/Dockerfile-base | cut -c1-16)
printf '%s' "$BTAG" > /workspace/base_tag
if wget -qO- "http://$REG/v2/verso-base/tags/list" 2>/dev/null | grep -q "\"base-$BTAG\""; then
echo "Base image base-$BTAG already present - skipping base build"
else
touch /workspace/build-base
echo "Base image base-$BTAG not found - base will be built"
fi
volumeMounts:
- name: workspace
mountPath: /workspace
containers:
- name: buildkit
image: moby/buildkit:latest
securityContext:
privileged: true
command: ["sh", "-c"]
args:
- |
set -eux
REG=registry.git.svc.cluster.local:5000
mkdir -p /etc/buildkit
printf '[registry."%s"]\n http = true\n insecure = true\n' "$REG" > /etc/buildkit/buildkitd.toml
BTAG=$(cat /workspace/base_tag)
BASE_REF="$REG/verso-base:base-$BTAG"
if [ -f /workspace/build-base ]; then
buildctl-daemonless.sh build \
--frontend=dockerfile.v0 \
--local context=/workspace/repo \
--local dockerfile=/workspace/repo/server-ce \
--opt filename=Dockerfile-base \
--import-cache type=registry,ref=$REG/verso-cache:base \
--export-cache type=registry,ref=$REG/verso-cache:base,mode=max \
--output type=image,name=$BASE_REF,push=true,registry.insecure=true
else
echo "Reusing existing base image $BASE_REF"
fi
# App image → verso:stable (prod tag).
buildctl-daemonless.sh build \
--frontend=dockerfile.v0 \
--local context=/workspace/repo \
--local dockerfile=/workspace/repo/server-ce \
--opt filename=Dockerfile \
--opt build-arg:OVERLEAF_BASE_TAG=$BASE_REF \
--import-cache type=registry,ref=$REG/verso-cache:app \
--export-cache type=registry,ref=$REG/verso-cache:app,mode=max \
--output type=image,name=$REG/verso:stable,push=true,registry.insecure=true
volumeMounts:
- name: workspace
mountPath: /workspace
volumes:
- name: workspace
emptyDir: {}
EOF
- name: Wait for build
run: |
kubectl -n ci wait --for=condition=complete job/verso-buildkit-prod --timeout=14400s
- name: Show build logs
if: always()
run: |
kubectl -n ci logs job/verso-buildkit-prod -c prepare || true
kubectl -n ci logs job/verso-buildkit-prod -c buildkit || true
- name: Ensure data services (Mongo + Redis, never deleted)
run: |
# Mongo/Redis. Applied idempotently — this step must never delete
# these, so project data survives every deploy. The namespace and the
# PVCs (server-ce/k8s/verso-prod-pvcs.yaml) are provisioned out of
# band, so the runner only needs namespaced rights in `verso` (like
# `test`). This step assumes the namespace and the
# mongo-data / redis-data / verso-data PVCs already exist.
cat <<'EOF' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: mongo
namespace: verso
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: mongo
template:
metadata:
labels:
app: mongo
spec:
containers:
- name: mongo
image: mongo:8
command: ["mongod", "--replSet", "rs0", "--bind_ip_all"]
ports:
- containerPort: 27017
volumeMounts:
- name: mongo-data
mountPath: /data/db
volumes:
- name: mongo-data
persistentVolumeClaim:
claimName: mongo-data
---
apiVersion: v1
kind: Service
metadata:
name: mongo
namespace: verso
spec:
selector:
app: mongo
ports:
- name: mongo
port: 27017
targetPort: 27017
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: verso
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7
# AOF persistence so a restart doesn't drop in-flight edits
# before they're flushed to Mongo.
command: ["redis-server", "--appendonly", "yes"]
ports:
- containerPort: 6379
volumeMounts:
- name: redis-data
mountPath: /data
volumes:
- name: redis-data
persistentVolumeClaim:
claimName: redis-data
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: verso
spec:
selector:
app: redis
ports:
- name: redis
port: 6379
targetPort: 6379
EOF
kubectl -n verso rollout status deployment/mongo --timeout=300s
kubectl -n verso rollout status deployment/redis --timeout=300s
- name: Initialise Mongo replica set (only if not already initialised)
run: |
kubectl -n verso exec deploy/mongo -- mongosh --quiet --eval '
try {
rs.status()
print("replica set already initialised")
} catch (e) {
if (e.codeName === "NotYetInitialized" || /no replset config/i.test(e.message)) {
rs.initiate({ _id: "rs0", members: [{ _id: 0, host: "mongo:27017" }] })
print("replica set initiated")
} else {
throw e
}
}
'
kubectl -n verso exec deploy/mongo -- mongosh --quiet --eval '
while (rs.status().myState !== 1) { sleep(1000) }
print("Mongo replica set is PRIMARY")
'
- name: Ensure Verso deployment + service
run: |
# Stamp the instance name with this build number, e.g. "Verso V0.12 Alpha".
NAV_TITLE="Verso V0.${GITHUB_RUN_NUMBER:-${GITEA_RUN_NUMBER:-0}} Alpha"
cat <<'EOF' | sed "s|__NAV_TITLE__|${NAV_TITLE}|g" | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: verso
namespace: verso
spec:
replicas: 1
# RWO data volume → can't run two pods at once; recreate on update.
strategy:
type: Recreate
selector:
matchLabels:
app: verso
template:
metadata:
labels:
app: verso
spec:
securityContext:
# App runs as www-data (uid/gid 33); make the data volume
# group-writable by it.
fsGroup: 33
initContainers:
- name: init-data-perms
image: busybox:latest
command: ["sh", "-c"]
args:
- |
set -eux
mkdir -p /data/template_files /data/user_files \
/data/compiles /data/cache /data/output /data/published
chown -R 33:33 /data
volumeMounts:
- name: verso-data
mountPath: /data
containers:
- name: verso
image: registry.alocoq.fr/verso:stable
# :stable is a fixed tag, so force a pull on every rollout to
# pick up the freshly built image.
imagePullPolicy: Always
ports:
- containerPort: 80
env:
- name: OVERLEAF_MONGO_URL
value: mongodb://mongo:27017/sharelatex?replicaSet=rs0
- name: OVERLEAF_REDIS_HOST
value: redis
- name: REDIS_HOST
value: redis
- name: OVERLEAF_APP_NAME
value: Verso
- name: OVERLEAF_NAV_TITLE
value: "__NAV_TITLE__"
- name: OVERLEAF_SITE_URL
value: https://verso.alocoq.fr
- name: OVERLEAF_SITE_LANGUAGE
value: fr
# Allow anonymous visitors so public published-presentation
# links and read-only share links work without login.
- name: OVERLEAF_ALLOW_PUBLIC_ACCESS
value: "true"
# NB: anonymous read-AND-write sharing is intentionally NOT
# enabled (compiles are unsandboxed → only trusted accounts
# may trigger them). Public self-registration is also off
# (CE default): admin creates accounts / sends invites.
- name: OVERLEAF_ENABLE_PROJECT_PYTHON_VENV
value: "true"
- name: OVERLEAF_LATEX_SHELL_ESCAPE
value: "true"
# (SMTP email vars are loaded below via envFrom.)
# SMTP for password-reset / invite emails. All
# OVERLEAF_EMAIL_* vars come from the optional 'verso-smtp'
# Secret (its keys must be named exactly like those env
# vars). Optional, so the app boots before the secret exists.
envFrom:
- secretRef:
name: verso-smtp
optional: true
volumeMounts:
- name: verso-data
mountPath: /var/lib/overleaf/data
volumes:
- name: verso-data
persistentVolumeClaim:
claimName: verso-data
---
apiVersion: v1
kind: Service
metadata:
name: verso
namespace: verso
spec:
selector:
app: verso
ports:
- name: http
port: 80
targetPort: 80
EOF
- name: Deploy Verso image
run: |
kubectl -n verso set image deployment/verso \
verso=registry.alocoq.fr/verso:stable
kubectl -n verso rollout restart deployment/verso
kubectl -n verso rollout status deployment/verso --timeout=600s
- name: Create initial admin (only if no users exist)
run: |
COUNT=$(kubectl -n verso exec deploy/mongo -- mongosh sharelatex --quiet --eval 'db.users.countDocuments()' | tr -d '[:space:]')
if [ "$COUNT" = "0" ]; then
echo "No users yet — creating the initial admin account"
kubectl -n verso exec deploy/verso -- bash -lc '
cd /overleaf/services/web
node modules/server-ce-scripts/scripts/create-user \
--admin \
--email=alois.coquillard@gmail.com
'
else
echo "Users already exist ($COUNT) — skipping admin creation"
fi
+341
View File
@@ -0,0 +1,341 @@
name: Build and Deploy Verso
on:
push:
branches:
- main
workflow_dispatch:
env:
SITE_URL: https://test.alocoq.fr
jobs:
deploy:
runs-on: native
timeout-minutes: 240
steps:
- name: Build and push Verso images with BuildKit
run: |
kubectl -n ci delete job verso-buildkit --ignore-not-found=true --wait=true
cat <<'EOF' | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: verso-buildkit
namespace: ci
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
initContainers:
- name: prepare
image: alpine/git:latest
command: ["sh", "-c"]
args:
- |
set -eux
REG=registry.git.svc.cluster.local:5000
git clone --depth 1 https://git.alocoq.fr/alois/verso.git /workspace/repo
# (#1) Build the base image only when it actually changes.
# The base layers' only repo input is Dockerfile-base, so
# we key on a content hash of that file: the base is tagged
# verso-base:base-<hash> and the app builds FROM that exact
# tag. If a base with this hash is already in the registry,
# the heavy base build (apt, TeX Live, Quarto) is skipped.
BTAG=$(sha256sum /workspace/repo/server-ce/Dockerfile-base | cut -c1-16)
printf '%s' "$BTAG" > /workspace/base_tag
if wget -qO- "http://$REG/v2/verso-base/tags/list" 2>/dev/null | grep -q "\"base-$BTAG\""; then
echo "Base image base-$BTAG already present - skipping base build"
else
touch /workspace/build-base
echo "Base image base-$BTAG not found - base will be built"
fi
volumeMounts:
- name: workspace
mountPath: /workspace
containers:
- name: buildkit
image: moby/buildkit:latest
securityContext:
privileged: true
command: ["sh", "-c"]
args:
- |
set -eux
# Push to the in-cluster registry (plain HTTP) to bypass
# the Traefik ingress, whose read timeout was killing the
# multi-GB TeX Live layer upload mid-stream. Mark the
# registry http+insecure so both push and the base pull
# for the app build treat it as plain HTTP. Written inside
# the container so no extra k8s resources are needed.
REG=registry.git.svc.cluster.local:5000
mkdir -p /etc/buildkit
printf '[registry."%s"]\n http = true\n insecure = true\n' "$REG" > /etc/buildkit/buildkitd.toml
BTAG=$(cat /workspace/base_tag)
BASE_REF="$REG/verso-base:base-$BTAG"
# (#1) Base build, only when prepare flagged it changed.
# (#2) Import/export a registry layer cache so that, when
# the base does change, unchanged layers (e.g. apt) are
# still reused instead of rebuilt from scratch.
if [ -f /workspace/build-base ]; then
buildctl-daemonless.sh build \
--frontend=dockerfile.v0 \
--local context=/workspace/repo \
--local dockerfile=/workspace/repo/server-ce \
--opt filename=Dockerfile-base \
--import-cache type=registry,ref=$REG/verso-cache:base \
--export-cache type=registry,ref=$REG/verso-cache:base,mode=max \
--output type=image,name=$BASE_REF,push=true,registry.insecure=true
else
echo "Reusing existing base image $BASE_REF"
fi
# App image, built FROM the content-pinned base tag.
# (#2) The registry cache lets yarn install be skipped when
# package.json is unchanged; the web build only re-runs
# when the frontend source actually changes.
buildctl-daemonless.sh build \
--frontend=dockerfile.v0 \
--local context=/workspace/repo \
--local dockerfile=/workspace/repo/server-ce \
--opt filename=Dockerfile \
--opt build-arg:OVERLEAF_BASE_TAG=$BASE_REF \
--import-cache type=registry,ref=$REG/verso-cache:app \
--export-cache type=registry,ref=$REG/verso-cache:app,mode=max \
--output type=image,name=$REG/verso:latest,push=true,registry.insecure=true
volumeMounts:
- name: workspace
mountPath: /workspace
volumes:
- name: workspace
emptyDir: {}
EOF
- name: Wait for build
run: |
kubectl -n ci wait --for=condition=complete job/verso-buildkit --timeout=14400s
- name: Show build logs
if: always()
run: |
kubectl -n ci logs job/verso-buildkit -c prepare || true
kubectl -n ci logs job/verso-buildkit -c buildkit || true
- name: Recreate test dependencies
run: |
kubectl -n test delete deployment mongo redis --ignore-not-found=true --wait=true
kubectl -n test delete service mongo redis --ignore-not-found=true --wait=true
cat <<'EOF' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: mongo
namespace: test
spec:
replicas: 1
selector:
matchLabels:
app: mongo
template:
metadata:
labels:
app: mongo
spec:
containers:
- name: mongo
image: mongo:8
command: ["mongod", "--replSet", "rs0", "--bind_ip_all"]
ports:
- containerPort: 27017
volumeMounts:
- name: mongo-data
mountPath: /data/db
volumes:
- name: mongo-data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: mongo
namespace: test
spec:
selector:
app: mongo
ports:
- name: mongo
port: 27017
targetPort: 27017
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: test
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7
ports:
- containerPort: 6379
volumeMounts:
- name: redis-data
mountPath: /data
volumes:
- name: redis-data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: test
spec:
selector:
app: redis
ports:
- name: redis
port: 6379
targetPort: 6379
EOF
kubectl -n test rollout status deployment/mongo --timeout=180s
kubectl -n test rollout status deployment/redis --timeout=180s
sleep 5
kubectl -n test exec deploy/mongo -- mongosh --eval '
rs.initiate({
_id: "rs0",
members: [{ _id: 0, host: "mongo:27017" }]
})
'
kubectl -n test exec deploy/mongo -- mongosh --eval '
while (rs.status().myState !== 1) {
sleep(1000)
}
print("Mongo replica set is PRIMARY")
'
- name: Ensure Verso deployment exists
run: |
# Stamp the instance name with this build number, e.g. "Verso V0.83 Alpha".
NAV_TITLE="Verso V0.${GITHUB_RUN_NUMBER:-${GITEA_RUN_NUMBER:-0}} Alpha"
cat <<'EOF' | sed "s|__NAV_TITLE__|${NAV_TITLE}|g" | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: verso
namespace: test
spec:
replicas: 1
selector:
matchLabels:
app: verso
template:
metadata:
labels:
app: verso
spec:
containers:
- name: verso
# Pull via the public address: the cluster nodes' containerd
# is configured for registry.alocoq.fr, not the in-cluster
# service name. Both front the same registry storage, so the
# image pushed via the in-cluster address resolves here too.
image: registry.alocoq.fr/verso:latest
ports:
- containerPort: 80
env:
- name: OVERLEAF_MONGO_URL
value: mongodb://mongo:27017/sharelatex?replicaSet=rs0
- name: OVERLEAF_REDIS_HOST
value: redis
- name: REDIS_HOST
value: redis
- name: OVERLEAF_APP_NAME
value: Verso
- name: OVERLEAF_NAV_TITLE
value: "__NAV_TITLE__"
- name: OVERLEAF_SITE_URL
value: https://test.alocoq.fr
# Default UI language for the instance.
- name: OVERLEAF_SITE_LANGUAGE
value: fr
# Allow anonymous visitors to reach the site so link
# sharing and public presentation links work without a
# login. Per-project and per-route access checks still
# apply; private presentation links still require login.
- name: OVERLEAF_ALLOW_PUBLIC_ACCESS
value: "true"
# Also let anonymous visitors use read-AND-write share
# links (edit without an account). Read-only links only
# need OVERLEAF_ALLOW_PUBLIC_ACCESS above.
- name: OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING
value: "true"
# Let Quarto Python cells use a project's requirements.txt:
# the compiler installs it into a cached venv. Gated to the
# project owner + invited collaborators (never anonymous /
# link-sharing users).
- name: OVERLEAF_ENABLE_PROJECT_PYTHON_VENV
value: "true"
- name: OVERLEAF_LATEX_SHELL_ESCAPE
value: "true"
---
apiVersion: v1
kind: Service
metadata:
name: verso
namespace: test
spec:
selector:
app: verso
ports:
- name: http
port: 80
targetPort: 80
EOF
- name: Deploy Verso image
run: |
kubectl -n test set image deployment/verso \
verso=registry.alocoq.fr/verso:latest
kubectl -n test rollout restart deployment/verso
kubectl -n test rollout status deployment/verso --timeout=300s
- name: Create admin user
run: |
sleep 20
kubectl -n test exec deploy/verso -- bash -lc '
cd /overleaf/services/web
node modules/server-ce-scripts/scripts/create-user \
--admin \
--email=test@example.com || true
'
- name: Cleanup
if: always()
run: |
kubectl -n ci delete job verso-buildkit --ignore-not-found=true --wait=true
@@ -0,0 +1,16 @@
diff --git a/dist/index.mjs b/dist/index.mjs
index b781835b37a1262510bdd66d8d3b399a076e9d68..312c314186d85bb3bc2ab2620e99bca259ef7167 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -44,10 +44,9 @@ import { z as z2 } from "zod/v4";
// src/tool/types.ts
import { z } from "zod/v4";
-var LATEST_PROTOCOL_VERSION = "2025-11-25";
+var LATEST_PROTOCOL_VERSION = "2025-06-18";
var SUPPORTED_PROTOCOL_VERSIONS = [
LATEST_PROTOCOL_VERSION,
- "2025-06-18",
"2025-03-26",
"2024-11-05"
];
@@ -0,0 +1,831 @@
diff --git a/dist/index.cjs b/dist/index.cjs
index 39215ae..b44cb76 100644
--- a/dist/index.cjs
+++ b/dist/index.cjs
@@ -187,16 +187,23 @@ Helper function that returns a transaction spec which inserts a
completion's text in the main selection range, and any other
selection range that has the same text in front of it.
*/
-function insertCompletionText(state$1, text, from, to) {
+function insertCompletionText(state$1, text, from, to, extend) {
let { main } = state$1.selection, fromOff = from - main.from, toOff = to - main.from;
return Object.assign(Object.assign({}, state$1.changeByRange(range => {
if (range != main && from != to &&
state$1.sliceDoc(range.from + fromOff, range.from + toOff) != state$1.sliceDoc(from, to))
return { range };
- let lines = state$1.toText(text);
+ let change = {
+ from: range.from + fromOff,
+ to: to == main.from ? range.to : range.from + toOff,
+ insert: text instanceof state.Text ? text : state$1.toText(text),
+ };
+ if (extend) {
+ extend(state$1, change);
+ }
return {
- changes: { from: range.from + fromOff, to: to == main.from ? range.to : range.from + toOff, insert: lines },
- range: state.EditorSelection.cursor(range.from + fromOff + lines.length)
+ changes: change,
+ range: state.EditorSelection.cursor(change.from + change.insert.length)
};
})), { scrollIntoView: true, userEvent: "input.complete" });
}
@@ -389,7 +396,9 @@ const completionConfig = state.Facet.define({
filterStrict: false,
compareCompletions: (a, b) => a.label.localeCompare(b.label),
interactionDelay: 75,
- updateSyncTime: 100
+ updateSyncTime: 100,
+ // overleaf: default to at top which is default CM6 behaviour
+ unfilteredResultsAtEnd: false
}, {
defaultKeymap: (a, b) => a && b,
closeOnBlur: (a, b) => a && b,
@@ -744,6 +753,7 @@ function score(option) {
(option.type ? 1 : 0);
}
function sortOptions(active, state) {
+ var _a;
let options = [];
let sections = null;
let addOption = (option) => {
@@ -763,7 +773,8 @@ function sortOptions(active, state) {
let getMatch = a.result.getMatch;
if (a.result.filter === false) {
for (let option of a.result.options) {
- addOption(new Option(option, a.source, getMatch ? getMatch(option) : [], 1e9 - options.length));
+ let defaultScore = conf.unfilteredResultsAtEnd ? -1e9 : 1e9;
+ addOption(new Option(option, a.source, getMatch ? getMatch(option) : [], defaultScore - options.length));
}
}
else {
@@ -790,15 +801,42 @@ function sortOptions(active, state) {
}
}
let result = [], prev = null;
+ const priorityIndices = new Map();
let compare = conf.compareCompletions;
for (let opt of options.sort((a, b) => (b.score - a.score) || compare(a.completion, b.completion))) {
+ // overleaf: Deduplicate results with dedup options
+ // The goal is to keep only the highest priority option, in the
+ // highest scoring position.
+ const key = (_a = opt.completion.deduplicate) === null || _a === void 0 ? void 0 : _a.key;
+ if (key) {
+ // Handle merging specifically for deduplicated items item
+ const currentOptionIndex = priorityIndices.get(key);
+ if (currentOptionIndex === undefined) {
+ priorityIndices.set(key, result.length);
+ result.push(opt);
+ prev = opt.completion;
+ }
+ else {
+ if (result[currentOptionIndex].completion.deduplicate.priority < opt.completion.deduplicate.priority) {
+ result[currentOptionIndex] = opt;
+ if (currentOptionIndex === result.length - 1) {
+ prev = opt.completion;
+ }
+ }
+ }
+ continue;
+ }
+ // overleaf: end
let cur = opt.completion;
- if (!prev || prev.label != cur.label || prev.detail != cur.detail ||
- (prev.type != null && cur.type != null && prev.type != cur.type) ||
- prev.apply != cur.apply || prev.boost != cur.boost)
+ if (!prev || prev.label != cur.label)
+ result.push(opt);
+ // overleaf: we're already handling deduplication, so skip extra merges
+ else if (prev.deduplicate)
result.push(opt);
else if (score(opt.completion) > score(prev))
result[result.length - 1] = opt;
+ else if (opt.completion.info)
+ result[result.length - 1] = opt;
prev = opt.completion;
}
return result;
@@ -817,8 +855,9 @@ class CompletionDialog {
: new CompletionDialog(this.options, makeAttrs(id, selected), this.tooltip, this.timestamp, selected, this.disabled);
}
static build(active, state, id, prev, conf, didSetActive) {
- if (prev && !didSetActive && active.some(s => s.isPending))
- return prev.setDisabled();
+ // Overleaf: avoid setting the previous completion state to disabled while completion sources are pending
+ // if (prev && !didSetActive && active.some(s => s.isPending))
+ // return prev.setDisabled()
let options = sortOptions(active, state);
if (!options.length)
return prev && active.some(a => a.isPending) ? prev.setDisabled() : null;
@@ -1017,13 +1056,14 @@ const completionState = state.StateField.define({
view.EditorView.contentAttributes.from(f, state => state.attrs)
]
});
+const getCompletionTooltip = (state) => { var _a; return (_a = state.field(completionState, false)) === null || _a === void 0 ? void 0 : _a.tooltip; };
function applyCompletion(view, option) {
const apply = option.completion.apply || option.completion.label;
let result = view.state.field(completionState).active.find(a => a.source == option.source);
if (!(result instanceof ActiveResult))
return false;
if (typeof apply == "string")
- view.dispatch(Object.assign(Object.assign({}, insertCompletionText(view.state, apply, result.from, result.to)), { annotations: pickedCompletion.of(option.completion) }));
+ view.dispatch(Object.assign(Object.assign({}, insertCompletionText(view.state, apply, result.from, result.to, option.completion.extend)), { annotations: pickedCompletion.of(option.completion) }));
else
apply(view, option.completion, result.from, result.to);
return true;
@@ -1559,20 +1599,42 @@ interpreted as indicating a placeholder.
function snippet(template) {
let snippet = Snippet.parse(template);
return (editor, completion, from, to) => {
- let { text, ranges } = snippet.instantiate(editor.state, from);
- let { main } = editor.state.selection;
- let spec = {
- changes: { from, to: to == main.from ? main.to : to, insert: state.Text.of(text) },
- scrollIntoView: true,
- annotations: completion ? [pickedCompletion.of(completion), state.Transaction.userEvent.of("input.complete")] : undefined
- };
+ let { main } = editor.state.selection, fromOff = from - main.from, toOff = to - main.from;
+ let ranges = [];
+ let totalOffset = 0;
+ let spec = Object.assign(Object.assign({}, editor.state.changeByRange(range => {
+ if (range != main && from != to &&
+ editor.state.sliceDoc(range.from + fromOff, range.from + toOff) != editor.state.sliceDoc(from, to))
+ return { range };
+ let { text, ranges: fieldRanges } = snippet.instantiate(editor.state, range.from + fromOff);
+ let change = {
+ from: range.from + fromOff,
+ to: range.from + toOff,
+ insert: state.Text.of(text)
+ };
+ let originalTo = change.to;
+ let offset = change.insert.length + fromOff;
+ if (completion.extend) {
+ completion.extend(editor.state, change);
+ offset += originalTo - change.to;
+ }
+ for (const fieldRange of fieldRanges) {
+ ranges.push(new FieldRange(fieldRange.field, fieldRange.from + totalOffset, fieldRange.to + totalOffset));
+ }
+ totalOffset += offset;
+ return {
+ changes: change,
+ range: state.EditorSelection.cursor(change.from + change.insert.length)
+ };
+ })), { scrollIntoView: true, annotations: completion ? [pickedCompletion.of(completion), state.Transaction.userEvent.of("input.complete")] : undefined, effects: [] });
if (ranges.length)
spec.selection = fieldSelection(ranges, 0);
if (ranges.some(r => r.field > 0)) {
let active = new ActiveSnippet(ranges, 0);
- let effects = spec.effects = [setActive.of(active)];
- if (editor.state.field(snippetState, false) === undefined)
- effects.push(state.StateEffect.appendConfig.of([snippetState, addSnippetKeymap, snippetPointerHandler, baseTheme]));
+ spec.effects.push(setActive.of(active));
+ if (editor.state.field(snippetState, false) === undefined) {
+ spec.effects.push(state.StateEffect.appendConfig.of([snippetState, addSnippetKeymap, snippetPointerHandler, baseTheme]));
+ }
}
editor.dispatch(editor.state.update(spec));
};
@@ -1746,7 +1808,8 @@ const completeAnyWord = context => {
const defaults = {
brackets: ["(", "[", "{", "'", '"'],
before: ")]}:;>",
- stringPrefixes: []
+ stringPrefixes: [],
+ buildInsert: (state, range, open, close) => open + close,
};
const closeBracketEffect = state.StateEffect.define({
map(value, mapping) {
@@ -1854,8 +1917,8 @@ function insertBracket(state$1, bracket) {
for (let tok of tokens) {
let closed = closing(state.codePointAt(tok, 0));
if (bracket == tok)
- return closed == tok ? handleSame(state$1, tok, tokens.indexOf(tok + tok + tok) > -1, conf)
- : handleOpen(state$1, tok, closed, conf.before || defaults.before);
+ return closed == tok ? handleSame(state$1, tok, tokens.indexOf(tok + tok) > -1, tokens.indexOf(tok + tok + tok) > -1, conf)
+ : handleOpen(state$1, tok, closed, conf.before || defaults.before, conf);
if (bracket == closed && closedBracketAt(state$1, state$1.selection.main.from))
return handleClose(state$1, tok, closed);
}
@@ -1877,17 +1940,21 @@ function prevChar(doc, pos) {
let prev = doc.sliceString(pos - 2, pos);
return state.codePointSize(state.codePointAt(prev, 0)) == prev.length ? prev : prev.slice(1);
}
-function handleOpen(state$1, open, close, closeBefore) {
+function handleOpen(state$1, open, close, closeBefore, config) {
+ let buildInsert = config.buildInsert || defaults.buildInsert;
let dont = null, changes = state$1.changeByRange(range => {
+ var _a;
if (!range.empty)
return { changes: [{ insert: open, from: range.from }, { insert: close, from: range.to }],
effects: closeBracketEffect.of(range.to + open.length),
range: state.EditorSelection.range(range.anchor + open.length, range.head + open.length) };
let next = nextChar(state$1.doc, range.head);
- if (!next || /\s/.test(next) || closeBefore.indexOf(next) > -1)
- return { changes: { insert: open + close, from: range.head },
- effects: closeBracketEffect.of(range.head + open.length),
+ if (!next || /\s/.test(next) || closeBefore.indexOf(next) > -1) {
+ const insert = (_a = buildInsert(state$1, range, open, close)) !== null && _a !== void 0 ? _a : open + close;
+ return { changes: { insert, from: range.head },
+ effects: insert === open ? [] : closeBracketEffect.of(range.head + open.length),
range: state.EditorSelection.cursor(range.head + open.length) };
+ }
return { range: dont = range };
});
return dont ? null : state$1.update(changes, {
@@ -1909,18 +1976,36 @@ function handleClose(state$1, _open, close) {
}
// Handles cases where the open and close token are the same, and
// possibly triple quotes (as in `"""abc"""`-style quoting).
-function handleSame(state$1, token, allowTriple, config) {
+function handleSame(state$1, token, allowDouble, allowTriple, config) {
let stringPrefixes = config.stringPrefixes || defaults.stringPrefixes;
+ let buildInsert = config.buildInsert || defaults.buildInsert;
let dont = null, changes = state$1.changeByRange(range => {
+ var _a, _b, _c;
if (!range.empty)
return { changes: [{ insert: token, from: range.from }, { insert: token, from: range.to }],
effects: closeBracketEffect.of(range.to + token.length),
range: state.EditorSelection.range(range.anchor + token.length, range.head + token.length) };
let pos = range.head, next = nextChar(state$1.doc, pos), start;
- if (next == token) {
+ if (allowTriple && state$1.sliceDoc(pos - 2 * token.length, pos) == token + token &&
+ (start = canStartStringAt(state$1, pos - 2 * token.length, stringPrefixes)) > -1 &&
+ nodeStart(state$1, start)) {
+ return { changes: { insert: token + token + token + token, from: pos },
+ effects: closeBracketEffect.of(pos + token.length),
+ range: state.EditorSelection.cursor(pos + token.length) };
+ }
+ else if (allowDouble && state$1.sliceDoc(pos - token.length, pos) == token &&
+ (start = canStartStringAt(state$1, pos - token.length, stringPrefixes)) > -1 &&
+ nodeStart(state$1, start)) {
+ let insert = (_a = buildInsert(state$1, range, token, token)) !== null && _a !== void 0 ? _a : token + token;
+ return { changes: { insert, from: pos },
+ effects: insert === token ? [] : closeBracketEffect.of(pos + token.length),
+ range: state.EditorSelection.cursor(pos + token.length) };
+ }
+ else if (next == token) {
if (nodeStart(state$1, pos)) {
- return { changes: { insert: token + token, from: pos },
- effects: closeBracketEffect.of(pos + token.length),
+ let insert = (_b = buildInsert(state$1, range, token, token)) !== null && _b !== void 0 ? _b : token + token;
+ return { changes: { insert, from: pos },
+ effects: insert === token ? [] : closeBracketEffect.of(pos + token.length),
range: state.EditorSelection.cursor(pos + token.length) };
}
else if (closedBracketAt(state$1, pos)) {
@@ -1930,18 +2015,13 @@ function handleSame(state$1, token, allowTriple, config) {
range: state.EditorSelection.cursor(pos + content.length) };
}
}
- else if (allowTriple && state$1.sliceDoc(pos - 2 * token.length, pos) == token + token &&
- (start = canStartStringAt(state$1, pos - 2 * token.length, stringPrefixes)) > -1 &&
- nodeStart(state$1, start)) {
- return { changes: { insert: token + token + token + token, from: pos },
- effects: closeBracketEffect.of(pos + token.length),
- range: state.EditorSelection.cursor(pos + token.length) };
- }
else if (state$1.charCategorizer(pos)(next) != state.CharCategory.Word) {
- if (canStartStringAt(state$1, pos, stringPrefixes) > -1 && !probablyInString(state$1, pos, token, stringPrefixes))
- return { changes: { insert: token + token, from: pos },
- effects: closeBracketEffect.of(pos + token.length),
+ if (canStartStringAt(state$1, pos, stringPrefixes) > -1 && !probablyInString(state$1, pos, token, stringPrefixes)) {
+ const insert = (_c = buildInsert(state$1, range, token, token)) !== null && _c !== void 0 ? _c : token + token;
+ return { changes: { insert, from: pos },
+ effects: insert === token ? [] : closeBracketEffect.of(pos + token.length),
range: state.EditorSelection.cursor(pos + token.length) };
+ }
}
return { range: dont = range };
});
@@ -2086,6 +2166,7 @@ exports.completionKeymap = completionKeymap;
exports.completionStatus = completionStatus;
exports.currentCompletions = currentCompletions;
exports.deleteBracketPair = deleteBracketPair;
+exports.getCompletionTooltip = getCompletionTooltip;
exports.hasNextSnippetField = hasNextSnippetField;
exports.hasPrevSnippetField = hasPrevSnippetField;
exports.ifIn = ifIn;
@@ -2093,8 +2174,10 @@ exports.ifNotIn = ifNotIn;
exports.insertBracket = insertBracket;
exports.insertCompletionText = insertCompletionText;
exports.moveCompletionSelection = moveCompletionSelection;
+exports.nextChar = nextChar;
exports.nextSnippetField = nextSnippetField;
exports.pickedCompletion = pickedCompletion;
+exports.prevChar = prevChar;
exports.prevSnippetField = prevSnippetField;
exports.selectedCompletion = selectedCompletion;
exports.selectedCompletionIndex = selectedCompletionIndex;
diff --git a/dist/index.d.cts b/dist/index.d.cts
index b57b8f6..fce47ab 100644
--- a/dist/index.d.cts
+++ b/dist/index.d.cts
@@ -1,6 +1,6 @@
import * as _codemirror_state from '@codemirror/state';
-import { EditorState, ChangeDesc, TransactionSpec, Transaction, StateCommand, Facet, Extension, StateEffect } from '@codemirror/state';
-import { EditorView, Rect, KeyBinding, Command } from '@codemirror/view';
+import { EditorState, Text, ChangeDesc, TransactionSpec, StateCommand, Transaction, Facet, SelectionRange, Extension, StateEffect } from '@codemirror/state';
+import { EditorView, Rect, KeyBinding, Tooltip, Command } from '@codemirror/view';
import * as _lezer_common from '@lezer/common';
/**
@@ -73,6 +73,19 @@ interface Completion {
a `{name}` object.
*/
section?: string | CompletionSection;
+ /**
+ Can be used to alter the change created when the completion is applied
+ */
+ extend?: ExtendCompletion;
+ /**
+ If multiple sources return the same result, use this field to specifiy a
+ deduplication key as well as a priority. For each unique key, only the
+ completion with the highest priority will be shown.
+ */
+ deduplicate?: {
+ key: string;
+ priority: number;
+ };
}
/**
The type returned from
@@ -306,12 +319,17 @@ This annotation is added to transactions that are produced by
picking a completion.
*/
declare const pickedCompletion: _codemirror_state.AnnotationType<Completion>;
+type ExtendCompletion = (state: EditorState, change: {
+ from: number;
+ to: number;
+ insert: string | Text;
+}) => void;
/**
Helper function that returns a transaction spec which inserts a
completion's text in the main selection range, and any other
selection range that has the same text in front of it.
*/
-declare function insertCompletionText(state: EditorState, text: string, from: number, to: number): TransactionSpec;
+declare function insertCompletionText(state: EditorState, text: string | Text, from: number, to: number, extend?: ExtendCompletion): TransactionSpec;
interface CompletionConfig {
/**
@@ -441,6 +459,10 @@ interface CompletionConfig {
milliseconds.
*/
updateSyncTime?: number;
+ /**
+ overleaf: Move unfiltered results after the filtered ones
+ */
+ unfilteredResultsAtEnd?: boolean;
}
/**
@@ -514,6 +536,8 @@ applies the snippet.
*/
declare function snippetCompletion(template: string, completion: Completion): Completion;
+declare const getCompletionTooltip: (state: EditorState) => Tooltip | undefined | null;
+
/**
Returns a command that moves the completion selection forward or
backward by the given amount.
@@ -562,6 +586,11 @@ interface CloseBracketConfig {
these prefixes before the opening quote.
*/
stringPrefixes?: string[];
+ /**
+ An optional callback for overriding the content that's inserted
+ based on surrounding characters
+ */
+ buildInsert?: (state: EditorState, range: SelectionRange, open: string, close: string) => string;
}
/**
Extension to enable bracket-closing behavior. When a closeable
@@ -593,6 +622,8 @@ to programmatically insert brackets—the
take care of running this for user input.)
*/
declare function insertBracket(state: EditorState, bracket: string): Transaction | null;
+declare function nextChar(doc: Text, pos: number): string;
+declare function prevChar(doc: Text, pos: number): string;
/**
Returns an extension that enables autocompletion.
@@ -636,4 +667,5 @@ the currently selected completion.
*/
declare function setSelectedCompletion(index: number): StateEffect<unknown>;
-export { type CloseBracketConfig, type Completion, CompletionContext, type CompletionInfo, type CompletionResult, type CompletionSection, type CompletionSource, acceptCompletion, autocompletion, clearSnippet, closeBrackets, closeBracketsKeymap, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, deleteBracketPair, hasNextSnippetField, hasPrevSnippetField, ifIn, ifNotIn, insertBracket, insertCompletionText, moveCompletionSelection, nextSnippetField, pickedCompletion, prevSnippetField, selectedCompletion, selectedCompletionIndex, setSelectedCompletion, snippet, snippetCompletion, snippetKeymap, startCompletion };
+export { CompletionContext, acceptCompletion, autocompletion, clearSnippet, closeBrackets, closeBracketsKeymap, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, deleteBracketPair, getCompletionTooltip, hasNextSnippetField, hasPrevSnippetField, ifIn, ifNotIn, insertBracket, insertCompletionText, moveCompletionSelection, nextChar, nextSnippetField, pickedCompletion, prevChar, prevSnippetField, selectedCompletion, selectedCompletionIndex, setSelectedCompletion, snippet, snippetCompletion, snippetKeymap, startCompletion };
+export type { CloseBracketConfig, Completion, CompletionInfo, CompletionResult, CompletionSection, CompletionSource };
diff --git a/dist/index.d.ts b/dist/index.d.ts
index b57b8f6..fce47ab 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -1,6 +1,6 @@
import * as _codemirror_state from '@codemirror/state';
-import { EditorState, ChangeDesc, TransactionSpec, Transaction, StateCommand, Facet, Extension, StateEffect } from '@codemirror/state';
-import { EditorView, Rect, KeyBinding, Command } from '@codemirror/view';
+import { EditorState, Text, ChangeDesc, TransactionSpec, StateCommand, Transaction, Facet, SelectionRange, Extension, StateEffect } from '@codemirror/state';
+import { EditorView, Rect, KeyBinding, Tooltip, Command } from '@codemirror/view';
import * as _lezer_common from '@lezer/common';
/**
@@ -73,6 +73,19 @@ interface Completion {
a `{name}` object.
*/
section?: string | CompletionSection;
+ /**
+ Can be used to alter the change created when the completion is applied
+ */
+ extend?: ExtendCompletion;
+ /**
+ If multiple sources return the same result, use this field to specifiy a
+ deduplication key as well as a priority. For each unique key, only the
+ completion with the highest priority will be shown.
+ */
+ deduplicate?: {
+ key: string;
+ priority: number;
+ };
}
/**
The type returned from
@@ -306,12 +319,17 @@ This annotation is added to transactions that are produced by
picking a completion.
*/
declare const pickedCompletion: _codemirror_state.AnnotationType<Completion>;
+type ExtendCompletion = (state: EditorState, change: {
+ from: number;
+ to: number;
+ insert: string | Text;
+}) => void;
/**
Helper function that returns a transaction spec which inserts a
completion's text in the main selection range, and any other
selection range that has the same text in front of it.
*/
-declare function insertCompletionText(state: EditorState, text: string, from: number, to: number): TransactionSpec;
+declare function insertCompletionText(state: EditorState, text: string | Text, from: number, to: number, extend?: ExtendCompletion): TransactionSpec;
interface CompletionConfig {
/**
@@ -441,6 +459,10 @@ interface CompletionConfig {
milliseconds.
*/
updateSyncTime?: number;
+ /**
+ overleaf: Move unfiltered results after the filtered ones
+ */
+ unfilteredResultsAtEnd?: boolean;
}
/**
@@ -514,6 +536,8 @@ applies the snippet.
*/
declare function snippetCompletion(template: string, completion: Completion): Completion;
+declare const getCompletionTooltip: (state: EditorState) => Tooltip | undefined | null;
+
/**
Returns a command that moves the completion selection forward or
backward by the given amount.
@@ -562,6 +586,11 @@ interface CloseBracketConfig {
these prefixes before the opening quote.
*/
stringPrefixes?: string[];
+ /**
+ An optional callback for overriding the content that's inserted
+ based on surrounding characters
+ */
+ buildInsert?: (state: EditorState, range: SelectionRange, open: string, close: string) => string;
}
/**
Extension to enable bracket-closing behavior. When a closeable
@@ -593,6 +622,8 @@ to programmatically insert brackets—the
take care of running this for user input.)
*/
declare function insertBracket(state: EditorState, bracket: string): Transaction | null;
+declare function nextChar(doc: Text, pos: number): string;
+declare function prevChar(doc: Text, pos: number): string;
/**
Returns an extension that enables autocompletion.
@@ -636,4 +667,5 @@ the currently selected completion.
*/
declare function setSelectedCompletion(index: number): StateEffect<unknown>;
-export { type CloseBracketConfig, type Completion, CompletionContext, type CompletionInfo, type CompletionResult, type CompletionSection, type CompletionSource, acceptCompletion, autocompletion, clearSnippet, closeBrackets, closeBracketsKeymap, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, deleteBracketPair, hasNextSnippetField, hasPrevSnippetField, ifIn, ifNotIn, insertBracket, insertCompletionText, moveCompletionSelection, nextSnippetField, pickedCompletion, prevSnippetField, selectedCompletion, selectedCompletionIndex, setSelectedCompletion, snippet, snippetCompletion, snippetKeymap, startCompletion };
+export { CompletionContext, acceptCompletion, autocompletion, clearSnippet, closeBrackets, closeBracketsKeymap, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, deleteBracketPair, getCompletionTooltip, hasNextSnippetField, hasPrevSnippetField, ifIn, ifNotIn, insertBracket, insertCompletionText, moveCompletionSelection, nextChar, nextSnippetField, pickedCompletion, prevChar, prevSnippetField, selectedCompletion, selectedCompletionIndex, setSelectedCompletion, snippet, snippetCompletion, snippetKeymap, startCompletion };
+export type { CloseBracketConfig, Completion, CompletionInfo, CompletionResult, CompletionSection, CompletionSource };
diff --git a/dist/index.js b/dist/index.js
index 4729223..9361a53 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -1,4 +1,4 @@
-import { Annotation, StateEffect, EditorSelection, codePointAt, codePointSize, fromCodePoint, Facet, combineConfig, StateField, Prec, Text, Transaction, MapMode, RangeValue, RangeSet, CharCategory } from '@codemirror/state';
+import { Annotation, StateEffect, Text, EditorSelection, codePointAt, codePointSize, fromCodePoint, Facet, combineConfig, StateField, Prec, Transaction, MapMode, RangeValue, RangeSet, CharCategory } from '@codemirror/state';
import { Direction, logException, showTooltip, EditorView, ViewPlugin, getTooltip, Decoration, WidgetType, keymap } from '@codemirror/view';
import { syntaxTree, indentUnit } from '@codemirror/language';
@@ -185,16 +185,23 @@ Helper function that returns a transaction spec which inserts a
completion's text in the main selection range, and any other
selection range that has the same text in front of it.
*/
-function insertCompletionText(state, text, from, to) {
+function insertCompletionText(state, text, from, to, extend) {
let { main } = state.selection, fromOff = from - main.from, toOff = to - main.from;
return Object.assign(Object.assign({}, state.changeByRange(range => {
if (range != main && from != to &&
state.sliceDoc(range.from + fromOff, range.from + toOff) != state.sliceDoc(from, to))
return { range };
- let lines = state.toText(text);
+ let change = {
+ from: range.from + fromOff,
+ to: to == main.from ? range.to : range.from + toOff,
+ insert: text instanceof Text ? text : state.toText(text),
+ };
+ if (extend) {
+ extend(state, change);
+ }
return {
- changes: { from: range.from + fromOff, to: to == main.from ? range.to : range.from + toOff, insert: lines },
- range: EditorSelection.cursor(range.from + fromOff + lines.length)
+ changes: change,
+ range: EditorSelection.cursor(change.from + change.insert.length)
};
})), { scrollIntoView: true, userEvent: "input.complete" });
}
@@ -387,7 +394,9 @@ const completionConfig = /*@__PURE__*/Facet.define({
filterStrict: false,
compareCompletions: (a, b) => a.label.localeCompare(b.label),
interactionDelay: 75,
- updateSyncTime: 100
+ updateSyncTime: 100,
+ // overleaf: default to at top which is default CM6 behaviour
+ unfilteredResultsAtEnd: false
}, {
defaultKeymap: (a, b) => a && b,
closeOnBlur: (a, b) => a && b,
@@ -742,6 +751,7 @@ function score(option) {
(option.type ? 1 : 0);
}
function sortOptions(active, state) {
+ var _a;
let options = [];
let sections = null;
let addOption = (option) => {
@@ -761,7 +771,8 @@ function sortOptions(active, state) {
let getMatch = a.result.getMatch;
if (a.result.filter === false) {
for (let option of a.result.options) {
- addOption(new Option(option, a.source, getMatch ? getMatch(option) : [], 1e9 - options.length));
+ let defaultScore = conf.unfilteredResultsAtEnd ? -1e9 : 1e9;
+ addOption(new Option(option, a.source, getMatch ? getMatch(option) : [], defaultScore - options.length));
}
}
else {
@@ -788,15 +799,42 @@ function sortOptions(active, state) {
}
}
let result = [], prev = null;
+ const priorityIndices = new Map();
let compare = conf.compareCompletions;
for (let opt of options.sort((a, b) => (b.score - a.score) || compare(a.completion, b.completion))) {
+ // overleaf: Deduplicate results with dedup options
+ // The goal is to keep only the highest priority option, in the
+ // highest scoring position.
+ const key = (_a = opt.completion.deduplicate) === null || _a === void 0 ? void 0 : _a.key;
+ if (key) {
+ // Handle merging specifically for deduplicated items item
+ const currentOptionIndex = priorityIndices.get(key);
+ if (currentOptionIndex === undefined) {
+ priorityIndices.set(key, result.length);
+ result.push(opt);
+ prev = opt.completion;
+ }
+ else {
+ if (result[currentOptionIndex].completion.deduplicate.priority < opt.completion.deduplicate.priority) {
+ result[currentOptionIndex] = opt;
+ if (currentOptionIndex === result.length - 1) {
+ prev = opt.completion;
+ }
+ }
+ }
+ continue;
+ }
+ // overleaf: end
let cur = opt.completion;
- if (!prev || prev.label != cur.label || prev.detail != cur.detail ||
- (prev.type != null && cur.type != null && prev.type != cur.type) ||
- prev.apply != cur.apply || prev.boost != cur.boost)
+ if (!prev || prev.label != cur.label)
+ result.push(opt);
+ // overleaf: we're already handling deduplication, so skip extra merges
+ else if (prev.deduplicate)
result.push(opt);
else if (score(opt.completion) > score(prev))
result[result.length - 1] = opt;
+ else if (opt.completion.info)
+ result[result.length - 1] = opt;
prev = opt.completion;
}
return result;
@@ -815,8 +853,9 @@ class CompletionDialog {
: new CompletionDialog(this.options, makeAttrs(id, selected), this.tooltip, this.timestamp, selected, this.disabled);
}
static build(active, state, id, prev, conf, didSetActive) {
- if (prev && !didSetActive && active.some(s => s.isPending))
- return prev.setDisabled();
+ // Overleaf: avoid setting the previous completion state to disabled while completion sources are pending
+ // if (prev && !didSetActive && active.some(s => s.isPending))
+ // return prev.setDisabled()
let options = sortOptions(active, state);
if (!options.length)
return prev && active.some(a => a.isPending) ? prev.setDisabled() : null;
@@ -1015,13 +1054,14 @@ const completionState = /*@__PURE__*/StateField.define({
EditorView.contentAttributes.from(f, state => state.attrs)
]
});
+const getCompletionTooltip = (state) => { var _a; return (_a = state.field(completionState, false)) === null || _a === void 0 ? void 0 : _a.tooltip; };
function applyCompletion(view, option) {
const apply = option.completion.apply || option.completion.label;
let result = view.state.field(completionState).active.find(a => a.source == option.source);
if (!(result instanceof ActiveResult))
return false;
if (typeof apply == "string")
- view.dispatch(Object.assign(Object.assign({}, insertCompletionText(view.state, apply, result.from, result.to)), { annotations: pickedCompletion.of(option.completion) }));
+ view.dispatch(Object.assign(Object.assign({}, insertCompletionText(view.state, apply, result.from, result.to, option.completion.extend)), { annotations: pickedCompletion.of(option.completion) }));
else
apply(view, option.completion, result.from, result.to);
return true;
@@ -1557,20 +1597,42 @@ interpreted as indicating a placeholder.
function snippet(template) {
let snippet = Snippet.parse(template);
return (editor, completion, from, to) => {
- let { text, ranges } = snippet.instantiate(editor.state, from);
- let { main } = editor.state.selection;
- let spec = {
- changes: { from, to: to == main.from ? main.to : to, insert: Text.of(text) },
- scrollIntoView: true,
- annotations: completion ? [pickedCompletion.of(completion), Transaction.userEvent.of("input.complete")] : undefined
- };
+ let { main } = editor.state.selection, fromOff = from - main.from, toOff = to - main.from;
+ let ranges = [];
+ let totalOffset = 0;
+ let spec = Object.assign(Object.assign({}, editor.state.changeByRange(range => {
+ if (range != main && from != to &&
+ editor.state.sliceDoc(range.from + fromOff, range.from + toOff) != editor.state.sliceDoc(from, to))
+ return { range };
+ let { text, ranges: fieldRanges } = snippet.instantiate(editor.state, range.from + fromOff);
+ let change = {
+ from: range.from + fromOff,
+ to: range.from + toOff,
+ insert: Text.of(text)
+ };
+ let originalTo = change.to;
+ let offset = change.insert.length + fromOff;
+ if (completion.extend) {
+ completion.extend(editor.state, change);
+ offset += originalTo - change.to;
+ }
+ for (const fieldRange of fieldRanges) {
+ ranges.push(new FieldRange(fieldRange.field, fieldRange.from + totalOffset, fieldRange.to + totalOffset));
+ }
+ totalOffset += offset;
+ return {
+ changes: change,
+ range: EditorSelection.cursor(change.from + change.insert.length)
+ };
+ })), { scrollIntoView: true, annotations: completion ? [pickedCompletion.of(completion), Transaction.userEvent.of("input.complete")] : undefined, effects: [] });
if (ranges.length)
spec.selection = fieldSelection(ranges, 0);
if (ranges.some(r => r.field > 0)) {
let active = new ActiveSnippet(ranges, 0);
- let effects = spec.effects = [setActive.of(active)];
- if (editor.state.field(snippetState, false) === undefined)
- effects.push(StateEffect.appendConfig.of([snippetState, addSnippetKeymap, snippetPointerHandler, baseTheme]));
+ spec.effects.push(setActive.of(active));
+ if (editor.state.field(snippetState, false) === undefined) {
+ spec.effects.push(StateEffect.appendConfig.of([snippetState, addSnippetKeymap, snippetPointerHandler, baseTheme]));
+ }
}
editor.dispatch(editor.state.update(spec));
};
@@ -1744,7 +1806,8 @@ const completeAnyWord = context => {
const defaults = {
brackets: ["(", "[", "{", "'", '"'],
before: ")]}:;>",
- stringPrefixes: []
+ stringPrefixes: [],
+ buildInsert: (state, range, open, close) => open + close,
};
const closeBracketEffect = /*@__PURE__*/StateEffect.define({
map(value, mapping) {
@@ -1852,8 +1915,8 @@ function insertBracket(state, bracket) {
for (let tok of tokens) {
let closed = closing(codePointAt(tok, 0));
if (bracket == tok)
- return closed == tok ? handleSame(state, tok, tokens.indexOf(tok + tok + tok) > -1, conf)
- : handleOpen(state, tok, closed, conf.before || defaults.before);
+ return closed == tok ? handleSame(state, tok, tokens.indexOf(tok + tok) > -1, tokens.indexOf(tok + tok + tok) > -1, conf)
+ : handleOpen(state, tok, closed, conf.before || defaults.before, conf);
if (bracket == closed && closedBracketAt(state, state.selection.main.from))
return handleClose(state, tok, closed);
}
@@ -1875,17 +1938,21 @@ function prevChar(doc, pos) {
let prev = doc.sliceString(pos - 2, pos);
return codePointSize(codePointAt(prev, 0)) == prev.length ? prev : prev.slice(1);
}
-function handleOpen(state, open, close, closeBefore) {
+function handleOpen(state, open, close, closeBefore, config) {
+ let buildInsert = config.buildInsert || defaults.buildInsert;
let dont = null, changes = state.changeByRange(range => {
+ var _a;
if (!range.empty)
return { changes: [{ insert: open, from: range.from }, { insert: close, from: range.to }],
effects: closeBracketEffect.of(range.to + open.length),
range: EditorSelection.range(range.anchor + open.length, range.head + open.length) };
let next = nextChar(state.doc, range.head);
- if (!next || /\s/.test(next) || closeBefore.indexOf(next) > -1)
- return { changes: { insert: open + close, from: range.head },
- effects: closeBracketEffect.of(range.head + open.length),
+ if (!next || /\s/.test(next) || closeBefore.indexOf(next) > -1) {
+ const insert = (_a = buildInsert(state, range, open, close)) !== null && _a !== void 0 ? _a : open + close;
+ return { changes: { insert, from: range.head },
+ effects: insert === open ? [] : closeBracketEffect.of(range.head + open.length),
range: EditorSelection.cursor(range.head + open.length) };
+ }
return { range: dont = range };
});
return dont ? null : state.update(changes, {
@@ -1907,18 +1974,36 @@ function handleClose(state, _open, close) {
}
// Handles cases where the open and close token are the same, and
// possibly triple quotes (as in `"""abc"""`-style quoting).
-function handleSame(state, token, allowTriple, config) {
+function handleSame(state, token, allowDouble, allowTriple, config) {
let stringPrefixes = config.stringPrefixes || defaults.stringPrefixes;
+ let buildInsert = config.buildInsert || defaults.buildInsert;
let dont = null, changes = state.changeByRange(range => {
+ var _a, _b, _c;
if (!range.empty)
return { changes: [{ insert: token, from: range.from }, { insert: token, from: range.to }],
effects: closeBracketEffect.of(range.to + token.length),
range: EditorSelection.range(range.anchor + token.length, range.head + token.length) };
let pos = range.head, next = nextChar(state.doc, pos), start;
- if (next == token) {
+ if (allowTriple && state.sliceDoc(pos - 2 * token.length, pos) == token + token &&
+ (start = canStartStringAt(state, pos - 2 * token.length, stringPrefixes)) > -1 &&
+ nodeStart(state, start)) {
+ return { changes: { insert: token + token + token + token, from: pos },
+ effects: closeBracketEffect.of(pos + token.length),
+ range: EditorSelection.cursor(pos + token.length) };
+ }
+ else if (allowDouble && state.sliceDoc(pos - token.length, pos) == token &&
+ (start = canStartStringAt(state, pos - token.length, stringPrefixes)) > -1 &&
+ nodeStart(state, start)) {
+ let insert = (_a = buildInsert(state, range, token, token)) !== null && _a !== void 0 ? _a : token + token;
+ return { changes: { insert, from: pos },
+ effects: insert === token ? [] : closeBracketEffect.of(pos + token.length),
+ range: EditorSelection.cursor(pos + token.length) };
+ }
+ else if (next == token) {
if (nodeStart(state, pos)) {
- return { changes: { insert: token + token, from: pos },
- effects: closeBracketEffect.of(pos + token.length),
+ let insert = (_b = buildInsert(state, range, token, token)) !== null && _b !== void 0 ? _b : token + token;
+ return { changes: { insert, from: pos },
+ effects: insert === token ? [] : closeBracketEffect.of(pos + token.length),
range: EditorSelection.cursor(pos + token.length) };
}
else if (closedBracketAt(state, pos)) {
@@ -1928,18 +2013,13 @@ function handleSame(state, token, allowTriple, config) {
range: EditorSelection.cursor(pos + content.length) };
}
}
- else if (allowTriple && state.sliceDoc(pos - 2 * token.length, pos) == token + token &&
- (start = canStartStringAt(state, pos - 2 * token.length, stringPrefixes)) > -1 &&
- nodeStart(state, start)) {
- return { changes: { insert: token + token + token + token, from: pos },
- effects: closeBracketEffect.of(pos + token.length),
- range: EditorSelection.cursor(pos + token.length) };
- }
else if (state.charCategorizer(pos)(next) != CharCategory.Word) {
- if (canStartStringAt(state, pos, stringPrefixes) > -1 && !probablyInString(state, pos, token, stringPrefixes))
- return { changes: { insert: token + token, from: pos },
- effects: closeBracketEffect.of(pos + token.length),
+ if (canStartStringAt(state, pos, stringPrefixes) > -1 && !probablyInString(state, pos, token, stringPrefixes)) {
+ const insert = (_c = buildInsert(state, range, token, token)) !== null && _c !== void 0 ? _c : token + token;
+ return { changes: { insert, from: pos },
+ effects: insert === token ? [] : closeBracketEffect.of(pos + token.length),
range: EditorSelection.cursor(pos + token.length) };
+ }
}
return { range: dont = range };
});
@@ -2071,4 +2151,4 @@ function setSelectedCompletion(index) {
return setSelectedEffect.of(index);
}
-export { CompletionContext, acceptCompletion, autocompletion, clearSnippet, closeBrackets, closeBracketsKeymap, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, deleteBracketPair, hasNextSnippetField, hasPrevSnippetField, ifIn, ifNotIn, insertBracket, insertCompletionText, moveCompletionSelection, nextSnippetField, pickedCompletion, prevSnippetField, selectedCompletion, selectedCompletionIndex, setSelectedCompletion, snippet, snippetCompletion, snippetKeymap, startCompletion };
+export { CompletionContext, acceptCompletion, autocompletion, clearSnippet, closeBrackets, closeBracketsKeymap, closeCompletion, completeAnyWord, completeFromList, completionKeymap, completionStatus, currentCompletions, deleteBracketPair, getCompletionTooltip, hasNextSnippetField, hasPrevSnippetField, ifIn, ifNotIn, insertBracket, insertCompletionText, moveCompletionSelection, nextChar, nextSnippetField, pickedCompletion, prevChar, prevSnippetField, selectedCompletion, selectedCompletionIndex, setSelectedCompletion, snippet, snippetCompletion, snippetKeymap, startCompletion };
@@ -0,0 +1,381 @@
diff --git a/dist/index.cjs b/dist/index.cjs
index 46231ae..fb0f9aa 100644
--- a/dist/index.cjs
+++ b/dist/index.cjs
@@ -592,6 +592,7 @@ class SearchQuery {
this.valid = !!this.search && (!this.regexp || validRegExp(this.search));
this.unquoted = this.unquote(this.search);
this.wholeWord = !!config.wholeWord;
+ this.scope = config.scope;
}
/**
@internal
@@ -606,7 +607,7 @@ class SearchQuery {
eq(other) {
return this.search == other.search && this.replace == other.replace &&
this.caseSensitive == other.caseSensitive && this.regexp == other.regexp &&
- this.wholeWord == other.wholeWord;
+ this.wholeWord == other.wholeWord && this.scope == other.scope;
}
/**
@internal
@@ -631,7 +632,12 @@ class QueryType {
}
}
function stringCursor(spec, state, from, to) {
- return new SearchCursor(state.doc, spec.unquoted, from, to, spec.caseSensitive ? undefined : x => x.toLowerCase(), spec.wholeWord ? stringWordTest(state.doc, state.charCategorizer(state.selection.main.head)) : undefined);
+ const test = spec.wholeWord ? stringWordTest(state.doc, state.charCategorizer(state.selection.main.head)) : undefined;
+ const testWithinScope = (from, to, buffer, bufferPos) => {
+ return (!test || test(from, to, buffer, bufferPos))
+ && (!spec.scope || spec.scope.some(range => from >= range.from && from <= range.to && to >= range.from && to <= range.to));
+ };
+ return new SearchCursor(state.doc, spec.unquoted, from, to, spec.caseSensitive ? undefined : x => x.toLowerCase(), testWithinScope);
}
function stringWordTest(doc, categorizer) {
return (from, to, buf, bufPos) => {
@@ -695,9 +701,14 @@ class StringQuery extends QueryType {
}
}
function regexpCursor(spec, state, from, to) {
+ const test = spec.wholeWord ? regexpWordTest(state.charCategorizer(state.selection.main.head)) : undefined;
+ const testWithinScope = (from, to, match) => {
+ return (!test || test(from, to, match))
+ && (!spec.scope || spec.scope.some(range => from >= range.from && from <= range.to && to >= range.from && to <= range.to));
+ };
return new RegExpCursor(state.doc, spec.search, {
ignoreCase: !spec.caseSensitive,
- test: spec.wholeWord ? regexpWordTest(state.charCategorizer(state.selection.main.head)) : undefined
+ test: testWithinScope,
}, from, to);
}
function charBefore(str, index) {
@@ -737,10 +748,18 @@ class RegExpQuery extends QueryType {
this.prevMatchInRange(state, curTo, state.doc.length);
}
getReplacement(result) {
- return this.spec.unquote(this.spec.replace).replace(/\$([$&\d+])/g, (m, i) => i == "$" ? "$"
- : i == "&" ? result.match[0]
- : i != "0" && +i < result.match.length ? result.match[i]
- : m);
+ return this.spec.unquote(this.spec.replace).replace(/\$([$&]|\d+)/g, (m, i) => {
+ if (i == "&")
+ return result.match[0];
+ if (i == "$")
+ return "$";
+ for (let l = i.length; l > 0; l--) {
+ let n = +i.slice(0, l);
+ if (n > 0 && n < result.match.length)
+ return result.match[n] + i.slice(l);
+ }
+ return m;
+ });
}
matchAll(state, limit) {
let cursor = regexpCursor(this.spec, state, 0, state.doc.length), ranges = [];
@@ -1227,7 +1246,9 @@ const searchExtensions = [
exports.RegExpCursor = RegExpCursor;
exports.SearchCursor = SearchCursor;
exports.SearchQuery = SearchQuery;
+exports.StringQuery = StringQuery;
exports.closeSearchPanel = closeSearchPanel;
+exports.createSearchPanel = createSearchPanel;
exports.findNext = findNext;
exports.findPrevious = findPrevious;
exports.getSearchQuery = getSearchQuery;
@@ -1242,4 +1263,6 @@ exports.searchPanelOpen = searchPanelOpen;
exports.selectMatches = selectMatches;
exports.selectNextOccurrence = selectNextOccurrence;
exports.selectSelectionMatches = selectSelectionMatches;
+exports.selectWord = selectWord;
exports.setSearchQuery = setSearchQuery;
+exports.togglePanel = togglePanel;
diff --git a/dist/index.d.cts b/dist/index.d.cts
index 08f5696..663d192 100644
--- a/dist/index.d.cts
+++ b/dist/index.d.cts
@@ -1,6 +1,6 @@
import * as _codemirror_state from '@codemirror/state';
import { Text, Extension, StateCommand, EditorState, SelectionRange, StateEffect } from '@codemirror/state';
-import { Command, KeyBinding, EditorView, Panel } from '@codemirror/view';
+import { Command, EditorView, Panel, KeyBinding } from '@codemirror/view';
/**
A search cursor provides an iterator over text matches in a
@@ -161,6 +161,7 @@ the `"cm-selectionMatch"` class for the highlighting. When
itself will be highlighted with `"cm-selectionMatch-main"`.
*/
declare function highlightSelectionMatches(options?: HighlightOptions): Extension;
+declare const selectWord: StateCommand;
/**
Select next occurrence of the current selection. Expand selection
to the surrounding word when the selection is empty.
@@ -264,6 +265,13 @@ declare class SearchQuery {
*/
readonly wholeWord: boolean;
/**
+ When set, only include search matches within these ranges
+ */
+ readonly scope?: Readonly<{
+ from: number;
+ to: number;
+ }[]>;
+ /**
Create a query object.
*/
constructor(config: {
@@ -293,6 +301,13 @@ declare class SearchQuery {
Enable whole-word matching.
*/
wholeWord?: boolean;
+ /**
+ The ranges to match within
+ */
+ scope?: Readonly<{
+ from: number;
+ to: number;
+ }[]>;
});
/**
Compare this query to another query.
@@ -307,6 +322,34 @@ declare class SearchQuery {
to: number;
}>;
}
+type SearchResult = typeof SearchCursor.prototype.value;
+declare abstract class QueryType<Result extends SearchResult = SearchResult> {
+ readonly spec: SearchQuery;
+ constructor(spec: SearchQuery);
+ abstract nextMatch(state: EditorState, curFrom: number, curTo: number): Result | null;
+ abstract prevMatch(state: EditorState, curFrom: number, curTo: number): Result | null;
+ abstract getReplacement(result: Result): string;
+ abstract matchAll(state: EditorState, limit: number): readonly Result[] | null;
+ abstract highlight(state: EditorState, from: number, to: number, add: (from: number, to: number) => void): void;
+}
+declare class StringQuery extends QueryType<SearchResult> {
+ constructor(spec: SearchQuery);
+ nextMatch(state: EditorState, curFrom: number, curTo: number): {
+ from: number;
+ to: number;
+ } | null;
+ private prevMatchInRange;
+ prevMatch(state: EditorState, curFrom: number, curTo: number): {
+ from: number;
+ to: number;
+ } | null;
+ getReplacement(_result: SearchResult): string;
+ matchAll(state: EditorState, limit: number): {
+ from: number;
+ to: number;
+ }[] | null;
+ highlight(state: EditorState, from: number, to: number, add: (from: number, to: number) => void): void;
+}
/**
A state effect that updates the current search query. Note that
this only has an effect if the search state has been initialized
@@ -315,6 +358,7 @@ by running [`openSearchPanel`](https://codemirror.net/6/docs/ref/#search.openSea
once).
*/
declare const setSearchQuery: _codemirror_state.StateEffectType<SearchQuery>;
+declare const togglePanel: _codemirror_state.StateEffectType<boolean>;
/**
Get the current search query from an editor state.
*/
@@ -353,6 +397,7 @@ Replace all instances of the search query with the given
replacement.
*/
declare const replaceAll: Command;
+declare function createSearchPanel(view: EditorView): Panel;
/**
Make sure the search panel is open and focused.
*/
@@ -372,4 +417,4 @@ Default search-related key bindings.
*/
declare const searchKeymap: readonly KeyBinding[];
-export { RegExpCursor, SearchCursor, SearchQuery, closeSearchPanel, findNext, findPrevious, getSearchQuery, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, search, searchKeymap, searchPanelOpen, selectMatches, selectNextOccurrence, selectSelectionMatches, setSearchQuery };
+export { RegExpCursor, SearchCursor, SearchQuery, StringQuery, closeSearchPanel, createSearchPanel, findNext, findPrevious, getSearchQuery, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, search, searchKeymap, searchPanelOpen, selectMatches, selectNextOccurrence, selectSelectionMatches, selectWord, setSearchQuery, togglePanel };
diff --git a/dist/index.d.ts b/dist/index.d.ts
index 08f5696..663d192 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -1,6 +1,6 @@
import * as _codemirror_state from '@codemirror/state';
import { Text, Extension, StateCommand, EditorState, SelectionRange, StateEffect } from '@codemirror/state';
-import { Command, KeyBinding, EditorView, Panel } from '@codemirror/view';
+import { Command, EditorView, Panel, KeyBinding } from '@codemirror/view';
/**
A search cursor provides an iterator over text matches in a
@@ -161,6 +161,7 @@ the `"cm-selectionMatch"` class for the highlighting. When
itself will be highlighted with `"cm-selectionMatch-main"`.
*/
declare function highlightSelectionMatches(options?: HighlightOptions): Extension;
+declare const selectWord: StateCommand;
/**
Select next occurrence of the current selection. Expand selection
to the surrounding word when the selection is empty.
@@ -264,6 +265,13 @@ declare class SearchQuery {
*/
readonly wholeWord: boolean;
/**
+ When set, only include search matches within these ranges
+ */
+ readonly scope?: Readonly<{
+ from: number;
+ to: number;
+ }[]>;
+ /**
Create a query object.
*/
constructor(config: {
@@ -293,6 +301,13 @@ declare class SearchQuery {
Enable whole-word matching.
*/
wholeWord?: boolean;
+ /**
+ The ranges to match within
+ */
+ scope?: Readonly<{
+ from: number;
+ to: number;
+ }[]>;
});
/**
Compare this query to another query.
@@ -307,6 +322,34 @@ declare class SearchQuery {
to: number;
}>;
}
+type SearchResult = typeof SearchCursor.prototype.value;
+declare abstract class QueryType<Result extends SearchResult = SearchResult> {
+ readonly spec: SearchQuery;
+ constructor(spec: SearchQuery);
+ abstract nextMatch(state: EditorState, curFrom: number, curTo: number): Result | null;
+ abstract prevMatch(state: EditorState, curFrom: number, curTo: number): Result | null;
+ abstract getReplacement(result: Result): string;
+ abstract matchAll(state: EditorState, limit: number): readonly Result[] | null;
+ abstract highlight(state: EditorState, from: number, to: number, add: (from: number, to: number) => void): void;
+}
+declare class StringQuery extends QueryType<SearchResult> {
+ constructor(spec: SearchQuery);
+ nextMatch(state: EditorState, curFrom: number, curTo: number): {
+ from: number;
+ to: number;
+ } | null;
+ private prevMatchInRange;
+ prevMatch(state: EditorState, curFrom: number, curTo: number): {
+ from: number;
+ to: number;
+ } | null;
+ getReplacement(_result: SearchResult): string;
+ matchAll(state: EditorState, limit: number): {
+ from: number;
+ to: number;
+ }[] | null;
+ highlight(state: EditorState, from: number, to: number, add: (from: number, to: number) => void): void;
+}
/**
A state effect that updates the current search query. Note that
this only has an effect if the search state has been initialized
@@ -315,6 +358,7 @@ by running [`openSearchPanel`](https://codemirror.net/6/docs/ref/#search.openSea
once).
*/
declare const setSearchQuery: _codemirror_state.StateEffectType<SearchQuery>;
+declare const togglePanel: _codemirror_state.StateEffectType<boolean>;
/**
Get the current search query from an editor state.
*/
@@ -353,6 +397,7 @@ Replace all instances of the search query with the given
replacement.
*/
declare const replaceAll: Command;
+declare function createSearchPanel(view: EditorView): Panel;
/**
Make sure the search panel is open and focused.
*/
@@ -372,4 +417,4 @@ Default search-related key bindings.
*/
declare const searchKeymap: readonly KeyBinding[];
-export { RegExpCursor, SearchCursor, SearchQuery, closeSearchPanel, findNext, findPrevious, getSearchQuery, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, search, searchKeymap, searchPanelOpen, selectMatches, selectNextOccurrence, selectSelectionMatches, setSearchQuery };
+export { RegExpCursor, SearchCursor, SearchQuery, StringQuery, closeSearchPanel, createSearchPanel, findNext, findPrevious, getSearchQuery, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, search, searchKeymap, searchPanelOpen, selectMatches, selectNextOccurrence, selectSelectionMatches, selectWord, setSearchQuery, togglePanel };
diff --git a/dist/index.js b/dist/index.js
index 22172ef..08a9974 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -590,6 +590,7 @@ class SearchQuery {
this.valid = !!this.search && (!this.regexp || validRegExp(this.search));
this.unquoted = this.unquote(this.search);
this.wholeWord = !!config.wholeWord;
+ this.scope = config.scope;
}
/**
@internal
@@ -604,7 +605,7 @@ class SearchQuery {
eq(other) {
return this.search == other.search && this.replace == other.replace &&
this.caseSensitive == other.caseSensitive && this.regexp == other.regexp &&
- this.wholeWord == other.wholeWord;
+ this.wholeWord == other.wholeWord && this.scope == other.scope;
}
/**
@internal
@@ -629,7 +630,12 @@ class QueryType {
}
}
function stringCursor(spec, state, from, to) {
- return new SearchCursor(state.doc, spec.unquoted, from, to, spec.caseSensitive ? undefined : x => x.toLowerCase(), spec.wholeWord ? stringWordTest(state.doc, state.charCategorizer(state.selection.main.head)) : undefined);
+ const test = spec.wholeWord ? stringWordTest(state.doc, state.charCategorizer(state.selection.main.head)) : undefined;
+ const testWithinScope = (from, to, buffer, bufferPos) => {
+ return (!test || test(from, to, buffer, bufferPos))
+ && (!spec.scope || spec.scope.some(range => from >= range.from && from <= range.to && to >= range.from && to <= range.to));
+ };
+ return new SearchCursor(state.doc, spec.unquoted, from, to, spec.caseSensitive ? undefined : x => x.toLowerCase(), testWithinScope);
}
function stringWordTest(doc, categorizer) {
return (from, to, buf, bufPos) => {
@@ -693,9 +699,14 @@ class StringQuery extends QueryType {
}
}
function regexpCursor(spec, state, from, to) {
+ const test = spec.wholeWord ? regexpWordTest(state.charCategorizer(state.selection.main.head)) : undefined;
+ const testWithinScope = (from, to, match) => {
+ return (!test || test(from, to, match))
+ && (!spec.scope || spec.scope.some(range => from >= range.from && from <= range.to && to >= range.from && to <= range.to));
+ };
return new RegExpCursor(state.doc, spec.search, {
ignoreCase: !spec.caseSensitive,
- test: spec.wholeWord ? regexpWordTest(state.charCategorizer(state.selection.main.head)) : undefined
+ test: testWithinScope,
}, from, to);
}
function charBefore(str, index) {
@@ -735,10 +746,18 @@ class RegExpQuery extends QueryType {
this.prevMatchInRange(state, curTo, state.doc.length);
}
getReplacement(result) {
- return this.spec.unquote(this.spec.replace).replace(/\$([$&\d+])/g, (m, i) => i == "$" ? "$"
- : i == "&" ? result.match[0]
- : i != "0" && +i < result.match.length ? result.match[i]
- : m);
+ return this.spec.unquote(this.spec.replace).replace(/\$([$&]|\d+)/g, (m, i) => {
+ if (i == "&")
+ return result.match[0];
+ if (i == "$")
+ return "$";
+ for (let l = i.length; l > 0; l--) {
+ let n = +i.slice(0, l);
+ if (n > 0 && n < result.match.length)
+ return result.match[n] + i.slice(l);
+ }
+ return m;
+ });
}
matchAll(state, limit) {
let cursor = regexpCursor(this.spec, state, 0, state.doc.length), ranges = [];
@@ -1222,4 +1241,4 @@ const searchExtensions = [
baseTheme
];
-export { RegExpCursor, SearchCursor, SearchQuery, closeSearchPanel, findNext, findPrevious, getSearchQuery, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, search, searchKeymap, searchPanelOpen, selectMatches, selectNextOccurrence, selectSelectionMatches, setSearchQuery };
+export { RegExpCursor, SearchCursor, SearchQuery, StringQuery, closeSearchPanel, createSearchPanel, findNext, findPrevious, getSearchQuery, gotoLine, highlightSelectionMatches, openSearchPanel, replaceAll, replaceNext, search, searchKeymap, searchPanelOpen, selectMatches, selectNextOccurrence, selectSelectionMatches, selectWord, setSearchQuery, togglePanel };
@@ -1,7 +1,7 @@
diff --git a/node_modules/body-parser/lib/read.js b/node_modules/body-parser/lib/read.js
diff --git a/lib/read.js b/lib/read.js
index fce6283..6131c31 100644
--- a/node_modules/body-parser/lib/read.js
+++ b/node_modules/body-parser/lib/read.js
--- a/lib/read.js
+++ b/lib/read.js
@@ -18,7 +18,7 @@ var iconv = require('iconv-lite')
var onFinished = require('on-finished')
var unpipe = require('unpipe')
@@ -1,7 +1,7 @@
diff --git a/node_modules/cypress-multi-reporters/lib/MultiReporters.js b/node_modules/cypress-multi-reporters/lib/MultiReporters.js
diff --git a/lib/MultiReporters.js b/lib/MultiReporters.js
index 98dc4ef..b2a97bf 100644
--- a/node_modules/cypress-multi-reporters/lib/MultiReporters.js
+++ b/node_modules/cypress-multi-reporters/lib/MultiReporters.js
--- a/lib/MultiReporters.js
+++ b/lib/MultiReporters.js
@@ -160,7 +160,7 @@ MultiReporters.prototype.getCustomOptions = function (options) {
debug('options file (custom)', customOptionsFile);
@@ -1,7 +1,7 @@
diff --git a/node_modules/forwarded/index.js b/node_modules/forwarded/index.js
diff --git a/index.js b/index.js
index b2b6bdd..75e6254 100644
--- a/node_modules/forwarded/index.js
+++ b/node_modules/forwarded/index.js
--- a/index.js
+++ b/index.js
@@ -46,7 +46,7 @@ function forwarded (req) {
function getSocketAddr (req) {
return req.socket
@@ -1,7 +1,7 @@
diff --git a/node_modules/mocha-multi-reporters/lib/MultiReporters.js b/node_modules/mocha-multi-reporters/lib/MultiReporters.js
index d61e019..e7a9515 100644
--- a/node_modules/mocha-multi-reporters/lib/MultiReporters.js
+++ b/node_modules/mocha-multi-reporters/lib/MultiReporters.js
diff --git a/lib/MultiReporters.js b/lib/MultiReporters.js
index d61e019711d5ac7f82c0fb90548bb0eb41ebbb85..e7a9515e05287621301b831191884a84d72ad0a1 100644
--- a/lib/MultiReporters.js
+++ b/lib/MultiReporters.js
@@ -153,7 +153,7 @@ MultiReporters.prototype.getCustomOptions = function (options) {
debug('options file (custom)', customOptionsFile);
@@ -1,7 +1,7 @@
diff --git a/node_modules/multer/lib/make-middleware.js b/node_modules/multer/lib/make-middleware.js
diff --git a/lib/make-middleware.js b/lib/make-middleware.js
index ee50988..de77364 100644
--- a/node_modules/multer/lib/make-middleware.js
+++ b/node_modules/multer/lib/make-middleware.js
--- a/lib/make-middleware.js
+++ b/lib/make-middleware.js
@@ -164,7 +164,7 @@ function makeMiddleware (setup) {
if (fieldname == null) return abortWithCode('MISSING_FIELD_NAME')
@@ -1,7 +1,7 @@
diff --git a/node_modules/node-fetch/lib/index.js b/node_modules/node-fetch/lib/index.js
diff --git a/lib/index.js b/lib/index.js
index 567ff5d..8eb45f7 100644
--- a/node_modules/node-fetch/lib/index.js
+++ b/node_modules/node-fetch/lib/index.js
--- a/lib/index.js
+++ b/lib/index.js
@@ -545,8 +545,8 @@ function clone(instance) {
// tee instance body
p1 = new PassThrough();
@@ -1,7 +1,7 @@
diff --git a/node_modules/passport-oauth2/lib/utils.js b/node_modules/passport-oauth2/lib/utils.js
diff --git a/lib/utils.js b/lib/utils.js
index 486f9e1..4584507 100644
--- a/node_modules/passport-oauth2/lib/utils.js
+++ b/node_modules/passport-oauth2/lib/utils.js
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -24,7 +24,7 @@ exports.originalURL = function(req, options) {
var trustProxy = options.proxy;
@@ -1,7 +1,7 @@
diff --git a/node_modules/pdfjs-dist/build/pdf.worker.mjs b/node_modules/pdfjs-dist/build/pdf.worker.mjs
diff --git a/build/pdf.worker.mjs b/build/pdf.worker.mjs
index 6c5c6f1..bb6b7d1 100644
--- a/node_modules/pdfjs-dist/build/pdf.worker.mjs
+++ b/node_modules/pdfjs-dist/build/pdf.worker.mjs
--- a/build/pdf.worker.mjs
+++ b/build/pdf.worker.mjs
@@ -1830,7 +1830,7 @@ async function __wbg_init(module_or_path) {
}
}
File diff suppressed because it is too large Load Diff
@@ -1,12 +1,12 @@
diff --git a/node_modules/google-gax/node_modules/retry-request/index.js b/node_modules/google-gax/node_modules/retry-request/index.js
index 2fae107..5721c54 100644
--- a/node_modules/google-gax/node_modules/retry-request/index.js
+++ b/node_modules/google-gax/node_modules/retry-request/index.js
diff --git a/index.js b/index.js
index 2fae107d03b30aff9320d135ec79c049c51f298a..32ec707ddbf8937e3e130c3deac1060c308bf439 100644
--- a/index.js
+++ b/index.js
@@ -1,6 +1,6 @@
'use strict';
-const {PassThrough} = require('stream');
+const { PassThrough, pipeline } = require('stream');
+const {PassThrough, pipeline} = require('stream');
const extend = require('extend');
let debug = () => {};
@@ -1,12 +1,12 @@
diff --git a/node_modules/@google-cloud/logging/node_modules/retry-request/index.js b/node_modules/@google-cloud/logging/node_modules/retry-request/index.js
index 2fae107..5721c54 100644
--- a/node_modules/@google-cloud/logging/node_modules/retry-request/index.js
+++ b/node_modules/@google-cloud/logging/node_modules/retry-request/index.js
diff --git a/index.js b/index.js
index 298a351097d70a7fb005b6961f4d58247e391d8f..6a809ace0349d40cb2b6732ad66fd8ad208698ca 100644
--- a/index.js
+++ b/index.js
@@ -1,6 +1,6 @@
'use strict';
-const {PassThrough} = require('stream');
+const { PassThrough, pipeline } = require('stream');
+const {PassThrough, pipeline} = require('stream');
const extend = require('extend');
let debug = () => {};
@@ -0,0 +1,57 @@
diff --git a/lib/sandboxed_module.js b/lib/sandboxed_module.js
index 1cd6743..4718b97 100644
--- a/lib/sandboxed_module.js
+++ b/lib/sandboxed_module.js
@@ -4,7 +4,7 @@ var Module = require('module');
var fs = require('fs');
var vm = require('vm');
var path = require('path');
-var builtinModules = require('./builtin_modules.json');
+var builtinModules = Module.builtinModules || require('./builtin_modules.json');
var parent = module.parent;
var globalOptions = {};
var registeredBuiltInSourceTransformers = ['coffee'];
@@ -157,12 +157,20 @@ SandboxedModule.prototype._createRecursiveRequireProxy = function() {
var cache = Object.create(null);
var required = this._getRequires();
for (var key in required) {
- var injectedFilename = requireLike(this.filename).resolve(key);
- cache[injectedFilename] = required[key];
+ // Under Yarn PnP, resolution from a transitive dependency's context may fail
+ // for packages not declared in that dependency's package.json. Silently skip
+ // cache pre-population on failure; the mock will still be injected via the
+ // inject map in requireInterceptor or resolved via RecursiveRequireProxy fallback.
+ try {
+ var injectedFilename = requireLike(this.filename).resolve(key);
+ cache[injectedFilename] = required[key];
+ } catch (e) {}
}
cache[this.filename] = this.exports;
var globals = this.globals;
+ // Store the top-level module's filename for PnP fallback resolution
+ var topLevelFilename = this.filename;
var options;
if(!this._options.sourceTransformersSingleOnly && this._options.sourceTransformers){
options = {
@@ -208,8 +216,18 @@ SandboxedModule.prototype._createRecursiveRequireProxy = function() {
if (request in cache) return cache[request];
return require(request);
}
- // cached modules
- var requestedFilename = requireLike(this.filename).resolve(request);
+ // Resolve the requested module filename.
+ // Under Yarn PnP, packages can only resolve their declared dependencies.
+ // When sandboxed-module loads a transitive dependency, the resolution context
+ // may not have access to all needed packages. Fall back to resolving from
+ // the top-level module's context (the module under test).
+ var requestedFilename;
+ try {
+ requestedFilename = requireLike(this.filename).resolve(request);
+ } catch (e) {
+ if (this.filename === topLevelFilename) throw e;
+ requestedFilename = requireLike(topLevelFilename).resolve(request);
+ }
if (requestedFilename in cache) return cache[requestedFilename];
var sandboxedModule = createInnerSandboxedModule(requestedFilename)
return sandboxedModule.exports;
@@ -1,7 +1,7 @@
diff --git a/node_modules/send/index.js b/node_modules/send/index.js
diff --git a/index.js b/index.js
index 768f8ca..a882f4d 100644
--- a/node_modules/send/index.js
+++ b/node_modules/send/index.js
--- a/index.js
+++ b/index.js
@@ -788,29 +788,29 @@ SendStream.prototype.stream = function stream (path, options) {
// pipe
var stream = fs.createReadStream(path, options)
@@ -1,7 +1,7 @@
diff --git a/node_modules/teeny-request/build/src/index.js b/node_modules/teeny-request/build/src/index.js
diff --git a/build/src/index.js b/build/src/index.js
index a101736..a87f6b9 100644
--- a/node_modules/teeny-request/build/src/index.js
+++ b/node_modules/teeny-request/build/src/index.js
--- a/build/src/index.js
+++ b/build/src/index.js
@@ -130,6 +130,9 @@ function createMultipartStream(boundary, multipart) {
}
else {
@@ -1,7 +1,7 @@
diff --git a/node_modules/google-gax/node_modules/teeny-request/build/src/index.js b/node_modules/google-gax/node_modules/teeny-request/build/src/index.js
index af5d15e..2b63d0c 100644
--- a/node_modules/google-gax/node_modules/teeny-request/build/src/index.js
+++ b/node_modules/google-gax/node_modules/teeny-request/build/src/index.js
diff --git a/build/src/index.js b/build/src/index.js
index af5d15e260e2a47588c7c536447fe84bd3f86136..2b63d0c0b1eb6595c7a0bb314c1df792454c1a72 100644
--- a/build/src/index.js
+++ b/build/src/index.js
@@ -115,6 +115,9 @@ function createMultipartStream(boundary, multipart) {
}
else {
@@ -1,7 +1,7 @@
diff --git a/node_modules/thread-loader/dist/WorkerPool.js b/node_modules/thread-loader/dist/WorkerPool.js
index 4145779..f0ff068 100644
--- a/node_modules/thread-loader/dist/WorkerPool.js
+++ b/node_modules/thread-loader/dist/WorkerPool.js
diff --git a/dist/WorkerPool.js b/dist/WorkerPool.js
index 4145779f08eefafd0c18394806b6409c595ac5bb..aa16dd6a0f463804455164493a1e75e80cdf656a 100644
--- a/dist/WorkerPool.js
+++ b/dist/WorkerPool.js
@@ -258,6 +258,19 @@ class PoolWorker {
finalCallback();
break;
@@ -22,10 +22,10 @@ index 4145779..f0ff068 100644
case 'emitWarning':
{
const {
diff --git a/node_modules/thread-loader/dist/index.js b/node_modules/thread-loader/dist/index.js
index 75cd30f..d834af6 100644
--- a/node_modules/thread-loader/dist/index.js
+++ b/node_modules/thread-loader/dist/index.js
diff --git a/dist/index.js b/dist/index.js
index 75cd30fb63dc864057c1afc866f43fc7cc0a8020..d834af6ce1e2cd4473c4ae1b325d63eb1190c17e 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -43,6 +43,7 @@ function pitch() {
sourceMap: this.sourceMap,
emitError: this.emitError,
@@ -34,10 +34,10 @@ index 75cd30f..d834af6 100644
loadModule: this.loadModule,
resolve: this.resolve,
getResolve: this.getResolve,
diff --git a/node_modules/thread-loader/dist/worker.js b/node_modules/thread-loader/dist/worker.js
index 8e67959..aca94f1 100644
--- a/node_modules/thread-loader/dist/worker.js
+++ b/node_modules/thread-loader/dist/worker.js
diff --git a/dist/worker.js b/dist/worker.js
index 8e67959e4b7c9fd0db116509b1459636ba13a097..aca94f1442906baf179ada8e6a56501bbf71c480 100644
--- a/dist/worker.js
+++ b/dist/worker.js
@@ -90,6 +90,22 @@ function writeJson(data) {
writePipeWrite(lengthBuffer);
writePipeWrite(messageBuffer);
+25
View File
@@ -0,0 +1,25 @@
approvedGitRepositories:
- "**"
enableGlobalCache: false
enableScripts: true
enableTelemetry: false
nodeLinker: node-modules
npmMinimalAgeGate: 3d
supportedArchitectures:
cpu:
- current
- arm64
- x64
libc:
- current
- glibc
os:
- current
- darwin
- linux
+266 -50
View File
@@ -1,80 +1,296 @@
<h1 align="center">
<br>
<a href="https://www.overleaf.com"><img src="doc/logo.png" alt="Overleaf" width="300"></a>
</h1>
<h4 align="center">An open-source online real-time collaborative LaTeX editor.</h4>
<p align="center">
<a href="https://github.com/overleaf/overleaf/wiki">Wiki</a> •
<a href="https://www.overleaf.com/for/enterprises">Server Pro</a> •
<a href="#contributing">Contributing</a> •
<a href="https://mailchi.mp/overleaf.com/community-edition-and-server-pro">Mailing List</a> •
<a href="#authors">Authors</a> •
<a href="#license">License</a>
<img src="services/web/public/img/ol-brand/verso-logo.svg" alt="Verso" width="440">
</p>
<img src="doc/screenshot.png" alt="A screenshot of a project being edited in Overleaf Community Edition">
<p align="center">
Figure 1: A screenshot of a project being edited in Overleaf Community Edition.
</p>
**A collaborative, real-time editor for LaTeX, Quarto and Typst — self-hosted.**
## Community Edition
---
[Overleaf](https://www.overleaf.com) is an open-source online real-time collaborative LaTeX editor. We run a hosted version at [www.overleaf.com](https://www.overleaf.com), but you can also run your own local version, and contribute to the development of Overleaf.
## What is Verso?
Verso is a fork of [Overleaf](https://github.com/overleaf/overleaf) that extends its
collaborative editing infrastructure to support [Quarto](https://quarto.org) and
[Typst](https://typst.app) projects alongside LaTeX. Think of it as Overleaf, but
not limited to LaTeX.
### Verso vs Overleaf
[Overleaf](https://www.overleaf.com) is the gold standard for collaborative LaTeX
editing. Verso keeps everything that makes Overleaf great — real-time co-editing,
operational-transformation history, auth, project management, file storage — and
adds:
- **Quarto and Typst compilers** running alongside TeX Live, dispatched
automatically from the root file's extension (`.qmd` → Quarto, `.typ` → Typst,
`.tex``latexmk`).
- **Language-aware editor** for Quarto and Typst (syntax highlighting, completions,
document outline) — not just LaTeX.
- **Publish & share compiled output** (`/p/:token` with tiered access links) — a
feature absent from Overleaf Community Edition.
- **Lumière theme** — a redesigned project dashboard and editor chrome with a
card-based grid, thumbnails, and a teal gradient identity.
- **Full i18n** — French, German, Italian, and Spanish UI translations on top of
Overleaf's English base.
- Completely **free and self-hosted**; no Overleaf subscription required.
### Verso vs Quarto
[Quarto](https://quarto.org) is a command-line tool: you install it locally, write
`.qmd` files in any text editor, and run `quarto render` in a terminal. It is
excellent for solo authors with full control over their environment.
Verso wraps Quarto in a collaborative web editor:
- **No local install** — Quarto, Typst, TeX Live and Python run on the server.
- **Real-time collaboration** — multiple people edit the same `.qmd` simultaneously
with live cursors and conflict-free merging.
- **Not just Quarto** — LaTeX and Typst projects live in the same workspace, under
the same auth and history system.
- **Publish in one click** — RevealJS decks and PDFs are served at a stable link
without leaving the browser.
Verso is not a replacement for Quarto's CLI — it is a platform that makes Quarto
accessible as a shared, always-on service.
### Verso vs Typst.app
[Typst.app](https://typst.app) is a cloud-hosted web editor for Typst. It is
polished and fast, but it is a proprietary SaaS product and only supports Typst.
Verso differs in that:
- It is **self-hosted** and open-source (AGPL v3) — you control your data.
- It supports **three languages** (Typst, LaTeX, Quarto) in one instance.
- Real-time collaboration is powered by **operational transformation** (the same
engine as Overleaf), not CRDTs, which means it handles concurrent edits
gracefully for long documents.
- It ships with a full **project history** and version-restore workflow.
If you only need Typst and want a lighter, Typst-focused alternative, have a look
at **[Collabst](https://github.com/herluf-ba/collabst)** — an open-source,
self-hosted collaborative Typst editor that is independent of the Overleaf
codebase and shows a lot of promise.
---
## Features
- **Real-time collaboration** — multiple editors, live cursors, full project history and version restore.
- **Three compilers, auto-dispatched** by root file extension:
| Root file | Compiler | Typical output |
|-----------|----------|----------------|
| `.qmd` | Quarto | PDF (via Typst or LaTeX), HTML, or RevealJS |
| `.tex` | `latexmk` / TeX Live | PDF |
| `.typ` | Typst | PDF |
- **Language-aware editor for all three** — syntax highlighting, completions, and a document outline panel for LaTeX, Quarto and Typst.
- **Format badge** on the project dashboard; compiler dropdown greys out inapplicable engines.
- **Publish & share** — compile and snapshot to `/p/:token` with three independent access tiers (project members / any logged-in user / public). HTML/RevealJS decks are served live; PDFs are embedded inline. A **Present** toolbar button links directly to the published deck.
- **RevealJS thumbnails** — the first slide of a presentation is rendered as a preview card in the project list.
- **Quarto Python cells** — optional per-project virtual environment built from `requirements.txt`, so Python code chunks execute during render.
- **Visual formatting toolbar** — bold, italic, headings and inline code shortcuts for Quarto (`.qmd`) and Typst (`.typ`) files, in addition to Overleaf's existing LaTeX toolbar.
- **Lumière theme** — card-based project dashboard with PDF/slide thumbnails, a teal gradient identity, dark editor chrome, and an XS compact list view.
- **i18n** — French, German, Italian and Spanish UI translations.
- **Auto-compile** — preview refreshes automatically after you stop typing.
---
## Releases
### Alpha 1
The initial public release. Established Verso as an Overleaf fork with first-class
multi-language support:
- Quarto (`.qmd`) and Typst (`.typ`) compilers running alongside TeX Live,
dispatched automatically by root file extension — no per-project configuration.
- Language-aware editor for Quarto: Markdown highlighting, code-chunk completions
(`{python}`, `{r}`, `{julia}`, `{ojs}`…), callout and fenced-div completions,
cross-reference completions (`@fig-`, `@tbl-`, `@sec-`…).
- Language-aware editor for Typst: syntax highlighting and completions for
functions, imports, math and markup.
- Document outline panel for all three languages (LaTeX `\section`, Quarto `#`,
Typst `=`).
- Format badge on the project dashboard; compiler selector greys out inapplicable
engines for the current root file.
- Publish & share compiled output — HTML/RevealJS decks and PDFs hosted at
`/p/:token` with tiered access links (project / logged-in / public), each
independently resettable.
- Quarto Python code-cell execution via an optional per-project `requirements.txt`
virtual environment.
- Verso branding: name, logo and Kubernetes production deploy workflow.
### Alpha 2
Refinements to the Typst editor and the format badge system:
- **Quarto format sub-types** — the project badge now distinguishes *Quarto PDF*
from *Quarto Slides*, reading the frontmatter `format:` to pick the right label.
- **Python packages for collaborators** — Quarto Python package installation
extended to all users who have write access to the project, not only the owner.
- **Typst syntax highlighting overhaul** — complete grammar rewrite covering:
function calls and named argument keys, multi-line display math, `#{…}` code
blocks, content blocks, `show`-rule bodies, `let`-value bindings, and keyword
vs identifier disambiguation.
- **Typst visual formatting** — bold and italic toolbar buttons and keyboard
shortcuts (`Ctrl+B`, `Ctrl+I`), plus underline, small-caps and hyperlink
buttons, matching the Quarto and LaTeX toolbar experience.
### Alpha 3
- **Lumière theme** — redesigned project dashboard with a card grid, PDF/slide
thumbnails, parallax hover effects, a teal gradient identity and a dark editor
chrome. Includes an XS compact list view and a tile zoom slider.
- **Full i18n** — French, German, Italian and Spanish translations covering the
complete UI (login, dashboard, editor, settings, emails).
- **Visual editors for Quarto and Typst** — bold, italic, headings and inline code
shortcuts in the toolbar for `.qmd` and `.typ` files.
- **Top/bottom split view** — new editor layout that stacks the source editor
above the PDF preview vertically, in addition to the existing side-by-side mode.
- **Bidirectional format export** — LaTeX projects can be converted to Typst and
Typst projects to LaTeX via pandoc. Available from the File menu in the editor.
- **Mobile layout** — project dashboard, search bar, footer and editor all
adapted for phone screen sizes.
---
## Known issues
- **Large file upload timeouts** — uploads of large files on slow connections
can time out at the proxy layer. A streaming response fix is pending.
---
## Security model — trusted environments only
> [!CAUTION]
> Overleaf Community Edition is intended for use in environments where **all** users are trusted. Community Edition is **not** appropriate for scenarios where isolation of users is required due to Sandbox Compiles not being available. When not using Sandboxed Compiles, users have full read and write access to the `sharelatex` container resources (filesystem, network, environment variables) when running LaTeX compiles.
> Verso is designed for **closed groups of trusted users** (a lab, a class, a
> small team). All three compilers can execute arbitrary code on the server:
>
> - LaTeX with shell-escape enabled can run system commands.
> - Quarto Python cells execute Python code directly.
> - Typst's scripting layer is sandboxed by design, but runs server-side.
>
> There is **no per-project sandbox or resource isolation** beyond what the
> operating system provides. Exposing Verso to the public internet with open
> registration is not recommended. If you need to host a collaborative
> LaTeX editor for untrusted users or at scale, look at
> [Overleaf's non-Community offerings](https://www.overleaf.com/for/enterprises),
> which include proper sandboxing and enterprise access controls.
For more information on Sandbox Compiles check out our [documentation](https://docs.overleaf.com/on-premises/configuration/overleaf-toolkit/server-pro-only-configuration/sandboxed-compiles).
---
## Enterprise
## Quick start
If you want help installing and maintaining Overleaf in your lab or workplace, we offer an officially supported version called [Overleaf Server Pro](https://www.overleaf.com/for/enterprises). It also includes more features for security (SSO with LDAP or SAML), administration and collaboration (e.g. tracked changes). [Find out more!](https://www.overleaf.com/for/enterprises)
### With Docker
## Keeping up to date
```bash
docker run -d \
-p 80:80 \
-v ~/verso_data:/var/lib/overleaf \
--name verso \
registry.alocoq.fr/verso:latest
```
Sign up to the [mailing list](https://mailchi.mp/overleaf.com/community-edition-and-server-pro) to get updates on Overleaf releases and development.
Open `http://localhost`, then visit `/launchpad` on first run to create the admin
account.
## Installation
### Build from source
We have detailed installation instructions in the [Overleaf Toolkit](https://github.com/overleaf/toolkit/).
```bash
cd server-ce
make build-base # base OS image: system deps, Quarto, Typst, TeX Live
make build-community # application image: Node services + compiled frontend
```
## Upgrading
| File | Purpose |
|------|---------|
| `server-ce/Dockerfile-base` | Base image — system deps, Quarto (with Typst) and TeX Live |
| `server-ce/Dockerfile` | App image — Node services and the compiled React frontend |
If you are upgrading from a previous version of Overleaf, please see the [Release Notes section on the Wiki](https://github.com/overleaf/overleaf/wiki#release-notes) for all of the versions between your current version and the version you are upgrading to.
---
## Overleaf Docker Image
## Architecture
This repo contains two dockerfiles, [`Dockerfile-base`](server-ce/Dockerfile-base), which builds the
`sharelatex/sharelatex-base` image, and [`Dockerfile`](server-ce/Dockerfile) which builds the
`sharelatex/sharelatex` (or "community") image.
Verso is a microservices monorepo (Yarn workspaces). All services run inside a
single container managed by `runit`, with `nginx` as the front router.
The Base image generally contains the basic dependencies like `wget`, plus `texlive`.
We split this out because it's a pretty heavy set of
dependencies, and it's nice to not have to rebuild all of that every time.
```
browser ──→ nginx:80
├── / ──────────────────→ web:4000 (main app, React UI)
├── /socket.io ──────────→ real-time:3026 (WebSocket, OT engine)
├── /p/:token ───────────→ web (published output)
└── /project/*/output/* → clsi-nginx:8080 (compiled output files)
The `sharelatex/sharelatex` image extends the base image and adds the actual Overleaf code
and services.
web → document-updater → Redis pub/sub → real-time → browser
web → CLSI (quarto render / latexmk / typst) → output files → nginx → browser
```
Use `make build-base` and `make build-community` from `server-ce/` to build these images.
| Service | Role |
|---------|------|
| `web` | HTTP API, React frontend, auth, project & sharing management |
| `real-time` | WebSocket layer, live cursors and edit sync |
| `document-updater` | Operational transformation, Redis pub/sub |
| `clsi` | Compiler — runs `quarto render`, `latexmk` or `typst` and serves output |
| `docstore` | Document text storage (MongoDB) |
| `filestore` | Binary file storage (S3 or local) |
| `project-history` | Change history and version tracking |
We use the [Phusion base-image](https://github.com/phusion/baseimage-docker)
(which is extended by our `base` image) to provide us with a VM-like container
in which to run the Overleaf services. Baseimage uses the `runit` service
manager to manage services, and we add our init-scripts from the `server-ce/runit`
folder.
---
## Contributing
## Environment variables
Please see the [CONTRIBUTING](CONTRIBUTING.md) file for information on contributing to the development of Overleaf.
Verso inherits all of Overleaf's environment variables (prefixed `OVERLEAF_`).
The most commonly needed:
## Authors
| Variable | Default | Description |
|----------|---------|-------------|
| `OVERLEAF_APP_NAME` | `Verso` | Name shown in the UI |
| `OVERLEAF_NAV_TITLE` | — | Instance name/version shown in the top bar |
| `OVERLEAF_MONGO_URL` | `mongodb://mongo/sharelatex` | MongoDB connection string |
| `OVERLEAF_REDIS_HOST` | `localhost` | Redis host |
| `OVERLEAF_SITE_URL` | — | Public URL (used in emails and published links) |
| `OVERLEAF_SITE_LANGUAGE` | `en` | Default UI language (e.g. `fr`) |
| `OVERLEAF_ENABLE_PROJECT_PYTHON_VENV` | `false` | Allow Quarto Python cells to use a project `requirements.txt` |
| `OVERLEAF_ADMIN_EMAIL` | — | Email shown on the launchpad for the first admin account |
[The Overleaf Team](https://www.overleaf.com/about)
See the [Overleaf Server documentation](https://github.com/overleaf/overleaf/wiki)
for the full list.
---
## Relation to Overleaf
Verso is a fork of [Overleaf Community Edition](https://github.com/overleaf/overleaf).
Everything that Overleaf CE provides — real-time collaboration, operational-transformation
history, auth, project management, binary file storage — is inherited unchanged. The
Verso-specific additions are listed in the Features section and tracked across releases above.
Verso is not affiliated with Overleaf Ltd.
---
## Supporting the ecosystem
Verso is not accepting contributions or donations at this time. If you find it
useful and want to support the broader ecosystem it builds on:
- **Support Overleaf** — Verso is built on Overleaf's infrastructure. The best
way to support their work is to use or subscribe to
[Overleaf](https://www.overleaf.com) and encourage your institution to do the
same.
- **Support Typst** — [Typst GmbH](https://typst.app) is the company behind the
Typst compiler. Using Typst.app or sponsoring the
[Typst project on GitHub](https://github.com/typst/typst) helps sustain the
language itself.
- **Support RevealJS** — Verso uses [Reveal.js](https://revealjs.com) for
HTML presentations. Consider sponsoring the
[RevealJS project on GitHub](https://github.com/hakimel/reveal.js).
---
## License
The code in this repository is released under the GNU AFFERO GENERAL PUBLIC LICENSE, version 3. A copy can be found in the [`LICENSE`](LICENSE) file.
GNU Affero General Public License v3 — see [LICENSE](LICENSE).
Copyright (c) Overleaf, 2014-2025.
Copyright © Overleaf, 20142026 (original code).
Verso modifications © Aloïs Coquillard, 2026.
+50
View File
@@ -0,0 +1,50 @@
# Verso — Next Alpha Roadmap
Ideas and features deferred from the current alpha.
---
## Next alpha (post-current)
### Typst editing experience (inspired by Collabst)
- **typst.ts WASM preview** — Run the Typst compiler in the browser via
WebAssembly (typst.ts). This would give instant, sub-second preview
without a server round-trip, and would eliminate the entire class of
race conditions in the CLSI watcher (files written → typst compiles →
resolver missed). Could coexist with the CLSI watcher for PDF export
while using the WASM path for live preview.
- **Tinymist LSP integration** — Wire up
[Tinymist](https://github.com/Myriad-Dreamin/tinymist) (the Typst
language server) behind a WebSocket proxy. Would give Typst files
first-class autocomplete, hover docs, go-to-definition, and inline
error diagnostics — the main editing comfort gap vs. a native editor.
### Editor UX for non-LaTeX formats (.typ, .qmd, .md)
- **Visual/rich-text editing mode** — A toggle between raw source and a
rendered-in-place view for `.typ`, `.qmd`, and `.md` files (similar to
Overleaf's rich-text mode for LaTeX). Users who don't know Typst or
Markdown syntax should be able to edit content without seeing markup.
CodeMirror 6 already supports this pattern via a custom `NodeView` layer
or a separate Prosemirror bridge.
- **Toolbar / insertion shortcuts** — A formatting toolbar and keyboard
shortcuts for common operations, adapted per file type:
- **All formats**: bold, italic, underline, headings, bullet/numbered
lists, inline code, links.
- **Quarto / Markdown**: insert image, insert table, insert code block
with language tag.
- **Quarto RevealJS**: insert slide divider (`---`), insert speaker
notes (`::: notes`), insert columns layout, insert video embed
(using Quarto's `{{< video >}}` shortcode).
### AI writing assistant
- **In-editor AI assistant** — Inline writing help similar to what Overleaf
and CoCalc offer: suggest completions, rephrase selections, explain LaTeX
errors, and generate boilerplate (figures, tables, equations). Should work
across all three formats (`.tex`, `.typ`, `.qmd`). Backend would proxy
requests to a configurable model API (Claude, OpenAI-compatible) so
self-hosters can bring their own key.
-1
View File
@@ -59,7 +59,6 @@ each service:
| `web` | 9229 |
| `clsi` | 9230 |
| `chat` | 9231 |
| `contacts` | 9232 |
| `docstore` | 9233 |
| `document-updater` | 9234 |
| `filestore` | 9235 |
-1
View File
@@ -1,7 +1,6 @@
CHAT_HOST=chat
CLSI_HOST=clsi
DOWNLOAD_HOST=clsi-nginx
CONTACTS_HOST=contacts
DOCSTORE_HOST=docstore
DOCUMENT_UPDATER_HOST=document-updater
FILESTORE_HOST=filestore
-11
View File
@@ -21,17 +21,6 @@ services:
- ../services/chat/app.js:/overleaf/services/chat/app.js
- ../services/chat/config:/overleaf/services/chat/config
contacts:
command: ["node", "--watch", "app.js"]
environment:
- NODE_OPTIONS=--inspect=0.0.0.0:9229
ports:
- "127.0.0.1:9232:9229"
volumes:
- ../services/contacts/app:/overleaf/services/contacts/app
- ../services/contacts/app.js:/overleaf/services/contacts/app.js
- ../services/contacts/config:/overleaf/services/contacts/config
docstore:
command: ["node", "--watch", "app.js"]
environment:
-8
View File
@@ -45,13 +45,6 @@ services:
- ${PWD}/output:/output:ro
- ../services/clsi/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro
contacts:
build:
context: ..
dockerfile: services/contacts/Dockerfile
env_file:
- dev.env
docstore:
build:
context: ..
@@ -161,7 +154,6 @@ services:
- redis
- chat
- clsi
- contacts
- docstore
- document-updater
- filestore
+1 -1
View File
@@ -103,7 +103,7 @@ services:
mongo:
restart: always
image: mongo:6.0
image: mongo:8.0
container_name: mongo
command: "--replSet overleaf"
volumes:
+7 -1
View File
@@ -1,7 +1,13 @@
FROM cypress/included:13.13.2
FROM cypress/included:15.12.0
ARG USER_UID=1000
ARG USER_GID=1000
# Corepack setup, shared between all the images.
ENV PATH="/overleaf/node_modules/.bin:$PATH"
ENV COREPACK_HOME=/opt/corepack
RUN corepack enable && corepack install -g yarn@4.14.1
ENV COREPACK_ENABLE_NETWORK=0
WORKDIR /overleaf
RUN sed -i s/node:x:1000:/node:x:${USER_GID}:/ /etc/group \
+100
View File
@@ -0,0 +1,100 @@
# Design: per-project Python dependencies (cached virtualenv)
Status: **Phase 1 implemented** (gated behind `OVERLEAF_ENABLE_PROJECT_PYTHON_VENV`,
on in the deployment). Network egress policy and venv eviction (Phases 23)
remain. Captures the plan for letting Quarto `{python}` cells use libraries
beyond the curated base set.
## What ships in Phase 1
- A project root `requirements.vrf` is installed into a venv cached by its
sha256, created with `python3 -m venv --system-site-packages`; `QuartoRunner`
points Quarto at it via `QUARTO_PYTHON`. A per-hash `flock` serialises
concurrent builds; pip output is merged into `output.log`; on failure the
render falls back to the base interpreter (and the missing-package message
surfaces). Venvs live under `PYTHON_VENVS_DIR`
(default `/var/lib/overleaf/data/python-venvs`).
- Gated by `userCanInstallPython` (`PythonVenvGate.mjs`) to the project owner +
invited collaborators (any role) — never anonymous / link-sharing users —
threaded to CLSI as `allowPythonInstall` on the editor compile, presentation
export, and publish paths.
### Known Phase-1 limitations
- The first build of a heavy `requirements.vrf` runs within the compile
timeout; a very large install can be killed and retried next compile (the
venv is only marked complete on success).
- No egress restriction yet (Phase 2) — installs reach PyPI directly.
- No eviction yet (Phase 3) — venvs accumulate under `PYTHON_VENVS_DIR`.
## Background
Quarto executes `` ```{python} `` cells through a Jupyter kernel. The base image
([`server-ce/Dockerfile-base`](../server-ce/Dockerfile-base)) bundles a curated
scientific stack (numpy, pandas, scipy, matplotlib, seaborn, scikit-learn,
sympy, plotly, tabulate). Anything outside that set currently fails the render
with `ModuleNotFoundError`.
As a first step that already shipped, the Quarto log parser
([`quarto-log-parser.ts`](../services/web/frontend/js/ide/log-parser/quarto-log-parser.ts))
turns a missing-package traceback into an actionable message. This document is
the *next* step: letting a project declare and install its own dependencies.
**Key constraint:** the instance runs with anonymous read+write enabled
(`OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true`), so compiles can be
triggered by untrusted users. Installing arbitrary packages is therefore a
security decision, not just a convenience.
## Mechanism
1. **Declaration.** A standard `requirements.vrf` at the project root opts the
project in (familiar, Quarto-agnostic, supports version pinning).
2. **Keying.** CLSI hashes `sha256(requirements.vrf + python version)`. The hash
names a venv directory on a **persistent volume**, e.g.
`…/data/python-venvs/<hash>/`. Identical dependency sets share one venv across
projects and compiles.
3. **Build-if-missing.** `python3 -m venv --system-site-packages <dir>` (so the
bundled stack stays visible and only the *extra* deps are installed — smaller
and faster), then `<dir>/bin/pip install -r requirements.vrf`. Guard with a
per-hash `flock` so concurrent compiles don't build the same venv twice.
4. **Point Quarto at it.** Set `QUARTO_PYTHON=<dir>/bin/python3` in the render
environment (threaded web → CLSI exactly like `exportMode`). With
`--system-site-packages`, `ipykernel` from the base is importable, so the
kernel runs in that interpreter with base + project packages.
## Guard rails
- **Auth gating.** Only run the install path for **logged-in owner/collaborator**
compiles. Anonymous-link compiles use the plain base interpreter and never
trigger installs. Web decides and passes a boolean to CLSI; default-deny.
- **Network egress.** The compile environment must reach PyPI to install.
Restrict egress to PyPI / an internal mirror only (k8s NetworkPolicy + pip
`--index-url`), not arbitrary hosts.
- **Resource caps.** Install timeout, venv size cap, max package count; surface
overruns as a clear log error.
- **Trust boundary.** Even gated, a trusted user installing packages is
arbitrary code execution in the sandbox. Containment stays the CLSI container
+ resource limits + egress policy. This is owner-trust-level by design.
## Lifecycle
- **Eviction.** `touch` the venv on use; an LRU cleanup job prunes the oldest
venvs when the volume exceeds a size budget.
- **Failure UX.** pip errors flow into the log panel (reusing the friendly-error
pattern) showing pip's output.
## Rollout
- **Phase 1.** Detection + `flock` venv build + `QUARTO_PYTHON`, behind a
settings flag (default **off**), gated to logged-in owner, dev volume.
- **Phase 2.** Egress NetworkPolicy + index pinning + eviction job.
- **Phase 3.** Nicer pip-error surfacing + a small project-settings UI
affordance.
## Open decisions
- `requirements.vrf` vs a frontmatter field vs both?
- Shared global venv volume vs per-user namespacing (sharing is cheaper;
per-user is stricter isolation)?
- Allow native/compiled wheels (broader support) vs wheels-only/no-build
(tighter security)?
@@ -1,7 +1,7 @@
let reporterOptions = {}
if (process.env.CI) {
reporterOptions = {
reporter: '/overleaf/node_modules/mocha-multi-reporters',
reporter: require.resolve('mocha-multi-reporters'),
'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'],
}
}
@@ -5,5 +5,6 @@ access-token-encryptor
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/access-token-encryptor
--pipeline-owner=32
--public-repo=False
@@ -4,17 +4,17 @@
"description": "",
"main": "index.js",
"scripts": {
"test": "npm run lint && npm run types:check && npm run test:unit",
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test:ci": "npm run test:unit",
"test:ci": "yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"types:check": "tsc --noEmit"
},
"author": "",
"license": "AGPL-3.0-only",
"dependencies": {
"lodash": "^4.17.21"
"lodash": "^4.18.1"
},
"devDependencies": {
"chai": "^4.3.6",
+7
View File
@@ -1,4 +1,10 @@
const pkg = require('./package.json')
module.exports = {
meta: {
name: pkg.name,
version: pkg.version,
},
rules: {
'no-unnecessary-trans': require('./no-unnecessary-trans'),
'prefer-kebab-url': require('./prefer-kebab-url'),
@@ -8,5 +14,6 @@ module.exports = {
'require-vi-doMock-valid-path': require('./require-vi-doMock-valid-path'),
'require-loading-label': require('./require-loading-label'),
'require-cio-snake-case-properties': require('./require-cio-snake-case-properties'),
'no-throw-in-callback': require('./no-throw-in-callback'),
},
}
@@ -0,0 +1,52 @@
const CALLBACK_PARAM_NAMES = new Set(['cb', 'callback', 'done', 'next'])
function isCallbackParam(param) {
return (
param && param.type === 'Identifier' && CALLBACK_PARAM_NAMES.has(param.name)
)
}
module.exports = {
meta: {
type: 'error',
docs: {
description: 'Disallow throw statements inside callback-based functions',
},
messages: {
noThrowInCallback:
'Pass the error to the callback instead of throwing in callback-based code.',
},
},
create(context) {
// Stack tracks whether each enclosing function is a callback-style function.
// A callback-style function is non-async and has a last param named cb/callback/done/next.
const stack = []
function enterFunction(node) {
const params = node.params
const isCallback =
!node.async &&
params.length > 0 &&
isCallbackParam(params[params.length - 1])
stack.push(isCallback)
}
function exitFunction() {
stack.pop()
}
return {
FunctionDeclaration: enterFunction,
'FunctionDeclaration:exit': exitFunction,
FunctionExpression: enterFunction,
'FunctionExpression:exit': exitFunction,
ArrowFunctionExpression: enterFunction,
'ArrowFunctionExpression:exit': exitFunction,
ThrowStatement(node) {
if (stack[stack.length - 1]) {
context.report({ node, messageId: 'noThrowInCallback' })
}
},
}
},
}
+5 -3
View File
@@ -5,11 +5,13 @@
"license": "AGPL-3.0-only",
"main": "index.js",
"dependencies": {
"eslint": "^8.51.0",
"lodash": "^4.17.21"
"lodash": "^4.18.1"
},
"devDependencies": {
"@typescript-eslint/parser": "^8.50.0"
"@typescript-eslint/parser": "^8.59.4"
},
"peerDependencies": {
"eslint": "^10.4.0"
},
"scripts": {
"test": "node rules.test.js"
@@ -22,7 +22,7 @@ module.exports = {
},
},
create(context) {
const currentFilePath = context.getFilename()
const currentFilePath = context.filename
// ESLint can sometimes pass <text> or <input> for snippets not in a file
if (currentFilePath === '<text>' || currentFilePath === '<input>') {
return {}
@@ -81,9 +81,10 @@ module.exports = {
typeof firstArg.value !== 'string'
) {
if (firstArg.type === 'Identifier') {
const variable = context
.getScope()
.variables.find(v => v.name === firstArg.name)
const scope = context.sourceCode.getScope(node)
const variable = scope.variables.find(
v => v.name === firstArg.name
)
if (
variable &&
variable.defs.length > 0 &&
+69 -7
View File
@@ -1,4 +1,6 @@
const { RuleTester } = require('eslint')
const tsParser = require('@typescript-eslint/parser')
const noThrowInCallback = require('./no-throw-in-callback')
const preferKebabUrl = require('./prefer-kebab-url')
const noUnnecessaryTrans = require('./no-unnecessary-trans')
const shouldUnescapeTrans = require('./should-unescape-trans')
@@ -7,10 +9,10 @@ const viDoMockValidPath = require('./require-vi-doMock-valid-path')
const requireCioSnakeCaseProperties = require('./require-cio-snake-case-properties')
const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
languageOptions: {
parser: tsParser,
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
parserOptions: { ecmaFeatures: { jsx: true } },
},
})
@@ -32,19 +34,27 @@ ruleTester.run('prefer-kebab-url', preferKebabUrl, {
invalid: [
{
code: `app.get('/fooBar')`,
errors: [{ message: 'Route path should be in kebab-case.' }],
errors: [
{ message: 'Route path should be in kebab-case.', suggestions: 1 },
],
},
{
code: `app.get('/fooBar/:id')`,
errors: [{ message: 'Route path should be in kebab-case.' }],
errors: [
{ message: 'Route path should be in kebab-case.', suggestions: 1 },
],
},
{
code: `webRouter.get('/foo_bar/:id/FooBar/:name/fooBar')`,
errors: [{ message: 'Route path should be in kebab-case.' }],
errors: [
{ message: 'Route path should be in kebab-case.', suggestions: 1 },
],
},
{
code: `router.get(/^\\/downLoad\\/pro-ject\\/([^/]*)\\/OutPut\\/out-put\\.pdf$/)`,
errors: [{ message: 'Route path should be in kebab-case.' }],
errors: [
{ message: 'Route path should be in kebab-case.', suggestions: 1 },
],
},
],
})
@@ -152,6 +162,7 @@ ruleTester.run('domock-require-valid-path', viDoMockValidPath, {
{
message:
'The path "./require-vi-doMock-valid-path2" in vi.doMock() cannot be resolved relative to the current file.',
suggestions: [],
},
],
},
@@ -162,6 +173,7 @@ ruleTester.run('domock-require-valid-path', viDoMockValidPath, {
{
message:
'The first argument of vi.doMock() must be (or resolve to) a string literal representing a path.',
suggestions: [],
},
],
},
@@ -267,3 +279,53 @@ ruleTester.run(
],
}
)
const noThrowInCallbackMessage =
'Pass the error to the callback instead of throwing in callback-based code.'
ruleTester.run('no-throw-in-callback', noThrowInCallback, {
valid: [
// Calling the callback with an error is fine
{ code: `function foo(cb) { cb(new Error()) }` },
// async functions may throw (they return a rejected promise)
{ code: `async function foo(cb) { throw new Error() }` },
// Last param not a callback name — not a callback-style function
{ code: `function foo(data) { throw new Error() }` },
// No params at all
{ code: `function foo() { throw new Error() }` },
// throw inside a nested non-callback function is fine
{ code: `function foo(cb) { [1].map(function() { throw new Error() }) }` },
// throw inside a nested async arrow is fine
{ code: `function foo(cb) { [1].map(async () => { throw new Error() }) }` },
],
invalid: [
{
code: `function foo(cb) { throw new Error() }`,
errors: [{ message: noThrowInCallbackMessage }],
},
{
code: `function foo(callback) { throw new Error() }`,
errors: [{ message: noThrowInCallbackMessage }],
},
{
code: `function foo(done) { throw new Error() }`,
errors: [{ message: noThrowInCallbackMessage }],
},
{
code: `function foo(next) { throw new Error() }`,
errors: [{ message: noThrowInCallbackMessage }],
},
{
code: `function foo(data, cb) { throw new Error() }`,
errors: [{ message: noThrowInCallbackMessage }],
},
{
code: `const foo = (cb) => { throw new Error() }`,
errors: [{ message: noThrowInCallbackMessage }],
},
// throw in a nested callback-style function inside another callback function
{
code: `function foo(cb) { bar(function(done) { throw new Error() }) }`,
errors: [{ message: noThrowInCallbackMessage }],
},
],
})
+1 -1
View File
@@ -1,7 +1,7 @@
let reporterOptions = {}
if (process.env.CI) {
reporterOptions = {
reporter: '/overleaf/node_modules/mocha-multi-reporters',
reporter: require.resolve('mocha-multi-reporters'),
'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'],
}
}
+1
View File
@@ -5,5 +5,6 @@ fetch-utils
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/fetch-utils
--pipeline-owner=32
--public-repo=False
+4 -4
View File
@@ -4,10 +4,10 @@
"description": "utilities for node-fetch",
"main": "index.js",
"scripts": {
"test": "npm run lint && npm run types:check && npm run test:unit",
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test:ci": "npm run test:unit",
"test:ci": "yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"types:check": "tsc --noEmit"
},
@@ -26,8 +26,8 @@
"typescript": "^5.0.4"
},
"dependencies": {
"@overleaf/o-error": "*",
"lodash": "^4.17.21",
"@overleaf/o-error": "workspace:*",
"lodash": "^4.18.1",
"node-fetch": "^2.7.0"
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
let reporterOptions = {}
if (process.env.CI) {
reporterOptions = {
reporter: '/overleaf/node_modules/mocha-multi-reporters',
reporter: require.resolve('mocha-multi-reporters'),
'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'],
}
}
+1
View File
@@ -5,5 +5,6 @@ logger
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/logger
--pipeline-owner=32
--public-repo=False
+5 -5
View File
@@ -10,17 +10,17 @@
"license": "AGPL-3.0-only",
"version": "3.1.1",
"scripts": {
"test": "npm run lint && npm run types:check && npm run test:unit",
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test:ci": "npm run test:unit",
"test:ci": "yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"types:check": "tsc --noEmit"
},
"dependencies": {
"@google-cloud/logging-bunyan": "^5.1.0",
"@overleaf/fetch-utils": "*",
"@overleaf/o-error": "*",
"@overleaf/fetch-utils": "workspace:*",
"@overleaf/o-error": "workspace:*",
"bunyan": "^1.8.14"
},
"devDependencies": {
@@ -34,6 +34,6 @@
"typescript": "^5.0.4"
},
"peerDependencies": {
"@overleaf/metrics": "*"
"@overleaf/metrics": "workspace:*"
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
let reporterOptions = {}
if (process.env.CI) {
reporterOptions = {
reporter: '/overleaf/node_modules/mocha-multi-reporters',
reporter: require.resolve('mocha-multi-reporters'),
'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'],
}
}
+1
View File
@@ -5,5 +5,6 @@ metrics
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/metrics
--pipeline-owner=32
--public-repo=False
+12 -12
View File
@@ -8,14 +8,14 @@
},
"main": "index.js",
"dependencies": {
"@google-cloud/opentelemetry-cloud-trace-exporter": "^2.4.1",
"@google-cloud/profiler": "^6.0.3",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/auto-instrumentations-node": "^0.70.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.212.0",
"@opentelemetry/resources": "^1.30.1",
"@opentelemetry/sdk-node": "^0.212.0",
"@opentelemetry/semantic-conventions": "^1.39.0",
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",
"@google-cloud/profiler": "^6.0.4",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/auto-instrumentations-node": "^0.76.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.218.0",
"@opentelemetry/resources": "^2.7.1",
"@opentelemetry/sdk-node": "^0.218.0",
"@opentelemetry/semantic-conventions": "^1.41.1",
"compression": "^1.7.4",
"prom-client": "^14.1.1",
"yn": "^3.1.1"
@@ -34,12 +34,12 @@
"lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"test:acceptance": "mocha --recursive --exit --grep=$MOCHA_GREP test/acceptance",
"test": "npm run lint && npm run types:check && npm run test:unit",
"test:ci": "npm run test:unit",
"test:acceptance": "mocha --recursive --exit --grep=${MOCHA_GREP:-} test/acceptance",
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"test:ci": "yarn run test:unit",
"types:check": "tsc --noEmit"
},
"peerDependencies": {
"@overleaf/logger": "*"
"@overleaf/logger": "workspace:*"
}
}
+1
View File
@@ -5,6 +5,7 @@ mongo-utils
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/mongo-utils
--pipeline-owner=32
--public-repo=False
--tsconfig-no-implicit-any=True
+2 -2
View File
@@ -4,11 +4,11 @@
"description": "utilities to help working with mongo",
"main": "index.js",
"scripts": {
"test": "npm run lint && npm run types:check && npm run test:unit",
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test:ci": "npm run test:unit",
"test:ci": "yarn run test:unit",
"types:check": "tsc --noEmit"
},
"author": "Overleaf (https://www.overleaf.com)",
+1 -1
View File
@@ -1,7 +1,7 @@
let reporterOptions = {}
if (process.env.CI) {
reporterOptions = {
reporter: '/overleaf/node_modules/mocha-multi-reporters',
reporter: require.resolve('mocha-multi-reporters'),
'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'],
}
}
+1
View File
@@ -5,5 +5,6 @@ o-error
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/o-error
--pipeline-owner=32
--public-repo=False
+3 -3
View File
@@ -17,11 +17,11 @@
"index.cjs"
],
"scripts": {
"build": "npm run --silent test",
"build": "yarn run --silent test",
"lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test": "npm run lint && npm run types:check && npm run test:unit",
"test:ci": "npm run test:unit",
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"test:ci": "yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"types:check": "tsc --noEmit"
},
+1 -1
View File
@@ -1,7 +1,7 @@
let reporterOptions = {}
if (process.env.CI) {
reporterOptions = {
reporter: '/overleaf/node_modules/mocha-multi-reporters',
reporter: require.resolve('mocha-multi-reporters'),
'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'],
}
}
+1 -1
View File
@@ -304,7 +304,7 @@ In order to prevent accidental deletion from outside this mechanism, an event-ba
Contributions should pass lint, formatting and unit test checks. To run these, use
```
npm run test
yarn run test
```
There are no acceptance tests in this module, but https://github.com/overleaf/filestore/ contains a comprehensive set of acceptance tests that use this module. These should also pass, with the changes.
@@ -5,6 +5,7 @@ object-persistor
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/object-persistor
--pipeline-owner=32
--public-repo=False
--tsconfig-no-implicit-any=True
+6 -7
View File
@@ -4,11 +4,11 @@
"description": "Module for storing objects in multiple backends, with fallback on 404 to assist migration between them",
"main": "index.js",
"scripts": {
"test": "npm run lint && npm run types:check && npm run test:unit",
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test:ci": "npm run test:unit",
"test:ci": "yarn run test:unit",
"types:check": "tsc --noEmit"
},
"repository": {
@@ -23,10 +23,10 @@
"@aws-sdk/node-http-handler": "^3.374.0",
"@aws-sdk/s3-request-presigner": "^3.994.0",
"@google-cloud/storage": "^7.19.0",
"@overleaf/logger": "*",
"@overleaf/metrics": "*",
"@overleaf/o-error": "*",
"@overleaf/stream-utils": "*",
"@overleaf/logger": "workspace:*",
"@overleaf/metrics": "workspace:*",
"@overleaf/o-error": "workspace:*",
"@overleaf/stream-utils": "workspace:*",
"fast-crc32c": "overleaf/node-fast-crc32c#aae6b2a4c7a7a159395df9cc6c38dfde702d6f51",
"glob": "^12.0.0",
"range-parser": "^1.2.1",
@@ -38,7 +38,6 @@
"mocha": "^11.1.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"mock-fs": "^5.2.0",
"mongodb": "6.12.0",
"sandboxed-module": "^2.0.4",
"sinon": "^9.2.4",
@@ -3,9 +3,12 @@ const fs = require('node:fs')
const fsPromises = require('node:fs/promises')
const { glob } = require('glob')
const Path = require('node:path')
const { promisify } = require('node:util')
const { PassThrough } = require('node:stream')
const { pipeline } = require('node:stream/promises')
const openCb = promisify(fs.open)
const AbstractPersistor = require('./AbstractPersistor')
const { ReadError, WriteError, NotImplementedError } = require('./Errors')
const PersistorHelper = require('./PersistorHelper')
@@ -85,8 +88,9 @@ module.exports = class FSPersistor extends AbstractPersistor {
})
const fsPath = this._getFsPath(location, name, opts.useSubdirectories)
let fd
try {
opts.fd = await fsPromises.open(fsPath, 'r')
fd = await openCb(fsPath, 'r')
} catch (err) {
throw PersistorHelper.wrapError(
err,
@@ -96,7 +100,7 @@ module.exports = class FSPersistor extends AbstractPersistor {
)
}
const stream = fs.createReadStream(null, opts)
const stream = fs.createReadStream(null, { ...opts, fd })
// Return a PassThrough stream with a minimal interface. It will buffer until the caller starts reading. It will emit errors from the source stream (Stream.pipeline passes errors along).
const pass = new PassThrough()
pipeline(stream, observer, pass).catch(() => {})
@@ -1,6 +1,6 @@
const crypto = require('node:crypto')
const os = require('node:os')
const { expect } = require('chai')
const mockFs = require('mock-fs')
const fs = require('node:fs')
const fsPromises = require('node:fs/promises')
const Path = require('node:path')
@@ -10,22 +10,59 @@ const Errors = require('../../src/Errors')
const MODULE_PATH = '../../src/FSPersistor.js'
function createTree(base, tree) {
fs.mkdirSync(base, { recursive: true })
for (const [name, content] of Object.entries(tree)) {
const fullPath = Path.join(base, name)
if (Buffer.isBuffer(content) || typeof content === 'string') {
fs.writeFileSync(fullPath, content)
} else if (content && typeof content.symlink === 'string') {
fs.symlinkSync(content.symlink, fullPath)
} else {
createTree(fullPath, content)
}
}
}
describe('FSPersistorTests', function () {
const localFiles = {
'/uploads/info.txt': Buffer.from('This information is critical', {
const fileContents = {
'info.txt': Buffer.from('This information is critical', {
encoding: 'utf-8',
}),
'/uploads/other.txt': Buffer.from('Some other content', {
'other.txt': Buffer.from('Some other content', {
encoding: 'utf-8',
}),
}
const location = '/bucket'
let tmpDir
let location
let notADirPath
const files = {
wombat: 'animals/wombat.tex',
giraffe: 'animals/giraffe.tex',
potato: 'vegetables/potato.tex',
}
beforeEach(function () {
tmpDir = fs.mkdtempSync(Path.join(os.tmpdir(), 'fs-persistor-test-'))
createTree(tmpDir, {
uploads: {
'info.txt': fileContents['info.txt'],
'other.txt': fileContents['other.txt'],
},
'not-a-dir':
'This regular file is meant to prevent using this path as a directory',
directory: {
subdirectory: {},
},
})
notADirPath = Path.join(tmpDir, 'not-a-dir')
location = Path.join(tmpDir, 'bucket')
})
afterEach(function () {
fs.rmSync(tmpDir, { recursive: true })
})
const scenarios = [
{
description: 'default settings',
@@ -54,31 +91,26 @@ describe('FSPersistorTests', function () {
persistor = new FSPersistor(scenario.settings)
})
beforeEach(function () {
mockFs({
...localFiles,
'/not-a-dir':
'This regular file is meant to prevent using this path as a directory',
'/directory/subdirectory': {},
})
})
afterEach(function () {
mockFs.restore()
})
describe('sendFile', function () {
it('should copy the file', async function () {
await persistor.sendFile(location, files.wombat, '/uploads/info.txt')
await persistor.sendFile(
location,
files.wombat,
Path.join(tmpDir, 'uploads', 'info.txt')
)
const contents = await fsPromises.readFile(
scenario.fsPath(files.wombat)
)
expect(contents.equals(localFiles['/uploads/info.txt'])).to.be.true
expect(contents.equals(fileContents['info.txt'])).to.be.true
})
it('should return an error if the file cannot be stored', async function () {
await expect(
persistor.sendFile('/not-a-dir', files.wombat, '/uploads/info.txt')
persistor.sendFile(
notADirPath,
files.wombat,
Path.join(tmpDir, 'uploads', 'info.txt')
)
).to.be.rejectedWith(Errors.WriteError)
})
})
@@ -88,7 +120,9 @@ describe('FSPersistorTests', function () {
describe("when the file doesn't exist", function () {
beforeEach(function () {
stream = fs.createReadStream('/uploads/info.txt')
stream = fs.createReadStream(
Path.join(tmpDir, 'uploads', 'info.txt')
)
})
it('should write the stream to disk', async function () {
@@ -96,7 +130,7 @@ describe('FSPersistorTests', function () {
const contents = await fsPromises.readFile(
scenario.fsPath(files.wombat)
)
expect(contents.equals(localFiles['/uploads/info.txt'])).to.be.true
expect(contents.equals(fileContents['info.txt'])).to.be.true
})
it('should delete the temporary file', async function () {
@@ -109,7 +143,7 @@ describe('FSPersistorTests', function () {
describe('on error', function () {
beforeEach(async function () {
await expect(
persistor.sendStream('/not-a-dir', files.wombat, stream)
persistor.sendStream(notADirPath, files.wombat, stream)
).to.be.rejectedWith(Errors.WriteError)
})
@@ -129,13 +163,12 @@ describe('FSPersistorTests', function () {
describe('when the md5 hash matches', function () {
it('should write the stream to disk', async function () {
await persistor.sendStream(location, files.wombat, stream, {
sourceMd5: md5(localFiles['/uploads/info.txt']),
sourceMd5: md5(fileContents['info.txt']),
})
const contents = await fsPromises.readFile(
scenario.fsPath(files.wombat)
)
expect(contents.equals(localFiles['/uploads/info.txt'])).to.be
.true
expect(contents.equals(fileContents['info.txt'])).to.be.true
})
})
@@ -169,9 +202,11 @@ describe('FSPersistorTests', function () {
await persistor.sendFile(
location,
files.wombat,
'/uploads/info.txt'
Path.join(tmpDir, 'uploads', 'info.txt')
)
stream = fs.createReadStream(
Path.join(tmpDir, 'uploads', 'other.txt')
)
stream = fs.createReadStream('/uploads/other.txt')
})
it('should write the stream to disk', async function () {
@@ -179,7 +214,7 @@ describe('FSPersistorTests', function () {
const contents = await fsPromises.readFile(
scenario.fsPath(files.wombat)
)
expect(contents.equals(localFiles['/uploads/other.txt'])).to.be.true
expect(contents.equals(fileContents['other.txt'])).to.be.true
})
it('should delete the temporary file', async function () {
@@ -192,7 +227,7 @@ describe('FSPersistorTests', function () {
describe('on error', function () {
beforeEach(async function () {
await expect(
persistor.sendStream('/not-a-dir', files.wombat, stream)
persistor.sendStream(notADirPath, files.wombat, stream)
).to.be.rejectedWith(Errors.WriteError)
})
@@ -200,8 +235,7 @@ describe('FSPersistorTests', function () {
const contents = await fsPromises.readFile(
scenario.fsPath(files.wombat)
)
expect(contents.equals(localFiles['/uploads/info.txt'])).to.be
.true
expect(contents.equals(fileContents['info.txt'])).to.be.true
})
it('should delete the temporary file', async function () {
@@ -215,13 +249,12 @@ describe('FSPersistorTests', function () {
describe('when the md5 hash matches', function () {
it('should write the stream to disk', async function () {
await persistor.sendStream(location, files.wombat, stream, {
sourceMd5: md5(localFiles['/uploads/other.txt']),
sourceMd5: md5(fileContents['other.txt']),
})
const contents = await fsPromises.readFile(
scenario.fsPath(files.wombat)
)
expect(contents.equals(localFiles['/uploads/other.txt'])).to.be
.true
expect(contents.equals(fileContents['other.txt'])).to.be.true
})
})
@@ -238,8 +271,7 @@ describe('FSPersistorTests', function () {
const contents = await fsPromises.readFile(
scenario.fsPath(files.wombat)
)
expect(contents.equals(localFiles['/uploads/info.txt'])).to.be
.true
expect(contents.equals(fileContents['info.txt'])).to.be.true
})
it('should delete the temporary file', async function () {
@@ -254,13 +286,17 @@ describe('FSPersistorTests', function () {
describe('getObjectStream', function () {
beforeEach(async function () {
await persistor.sendFile(location, files.wombat, '/uploads/info.txt')
await persistor.sendFile(
location,
files.wombat,
Path.join(tmpDir, 'uploads', 'info.txt')
)
})
it('should return a string with the object contents', async function () {
const stream = await persistor.getObjectStream(location, files.wombat)
const contents = await streamToBuffer(stream)
expect(contents.equals(localFiles['/uploads/info.txt'])).to.be.true
expect(contents.equals(fileContents['info.txt'])).to.be.true
})
it('should support ranges', async function () {
@@ -274,8 +310,8 @@ describe('FSPersistorTests', function () {
)
const contents = await streamToBuffer(stream)
// end is inclusive in ranges, but exclusive in slice()
expect(contents.equals(localFiles['/uploads/info.txt'].slice(5, 17)))
.to.be.true
expect(contents.equals(fileContents['info.txt'].slice(5, 17))).to.be
.true
})
it('should give a NotFoundError if the file does not exist', async function () {
@@ -287,13 +323,17 @@ describe('FSPersistorTests', function () {
describe('getObjectSize', function () {
beforeEach(async function () {
await persistor.sendFile(location, files.wombat, '/uploads/info.txt')
await persistor.sendFile(
location,
files.wombat,
Path.join(tmpDir, 'uploads', 'info.txt')
)
})
it('should return the file size', async function () {
expect(
await persistor.getObjectSize(location, files.wombat)
).to.equal(localFiles['/uploads/info.txt'].length)
).to.equal(fileContents['info.txt'].length)
})
it('should throw a NotFoundError if the file does not exist', async function () {
@@ -305,7 +345,11 @@ describe('FSPersistorTests', function () {
describe('copyObject', function () {
beforeEach(async function () {
await persistor.sendFile(location, files.wombat, '/uploads/info.txt')
await persistor.sendFile(
location,
files.wombat,
Path.join(tmpDir, 'uploads', 'info.txt')
)
})
it('Should copy the file to the new location', async function () {
@@ -313,13 +357,17 @@ describe('FSPersistorTests', function () {
const contents = await fsPromises.readFile(
scenario.fsPath(files.potato)
)
expect(contents.equals(localFiles['/uploads/info.txt'])).to.be.true
expect(contents.equals(fileContents['info.txt'])).to.be.true
})
})
describe('deleteObject', function () {
beforeEach(async function () {
await persistor.sendFile(location, files.wombat, '/uploads/info.txt')
await persistor.sendFile(
location,
files.wombat,
Path.join(tmpDir, 'uploads', 'info.txt')
)
await fsPromises.access(scenario.fsPath(files.wombat))
})
@@ -337,7 +385,11 @@ describe('FSPersistorTests', function () {
describe('deleteDirectory', function () {
beforeEach(async function () {
for (const file of Object.values(files)) {
await persistor.sendFile(location, file, '/uploads/info.txt')
await persistor.sendFile(
location,
file,
Path.join(tmpDir, 'uploads', 'info.txt')
)
await fsPromises.access(scenario.fsPath(file))
}
})
@@ -365,7 +417,11 @@ describe('FSPersistorTests', function () {
describe('checkIfObjectExists', function () {
beforeEach(async function () {
await persistor.sendFile(location, files.wombat, '/uploads/info.txt')
await persistor.sendFile(
location,
files.wombat,
Path.join(tmpDir, 'uploads', 'info.txt')
)
})
it('should return true for existing files', async function () {
@@ -384,13 +440,17 @@ describe('FSPersistorTests', function () {
describe('directorySize', function () {
beforeEach(async function () {
for (const file of Object.values(files)) {
await persistor.sendFile(location, file, '/uploads/info.txt')
await persistor.sendFile(
location,
file,
Path.join(tmpDir, 'uploads', 'info.txt')
)
}
})
it('should sum directory files size', async function () {
expect(await persistor.directorySize(location, 'animals')).to.equal(
2 * localFiles['/uploads/info.txt'].length
2 * fileContents['info.txt'].length
)
})
@@ -404,7 +464,11 @@ describe('FSPersistorTests', function () {
describe('listDirectoryKeys', function () {
beforeEach(async function () {
for (const file of Object.values(files)) {
await persistor.sendFile(location, file, '/uploads/info.txt')
await persistor.sendFile(
location,
file,
Path.join(tmpDir, 'uploads', 'info.txt')
)
}
})
@@ -427,7 +491,11 @@ describe('FSPersistorTests', function () {
describe('listDirectoryStats', function () {
beforeEach(async function () {
for (const file of Object.values(files)) {
await persistor.sendFile(location, file, '/uploads/info.txt')
await persistor.sendFile(
location,
file,
Path.join(tmpDir, 'uploads', 'info.txt')
)
}
})
@@ -438,7 +506,7 @@ describe('FSPersistorTests', function () {
expect(keys).to.include(scenario.fsPath(files.wombat))
expect(keys).to.include(scenario.fsPath(files.giraffe))
for (const stat of stats) {
expect(stat.size).to.equal(localFiles['/uploads/info.txt'].length)
expect(stat.size).to.equal(fileContents['info.txt'].length)
}
})
+1 -1
View File
@@ -1,7 +1,7 @@
let reporterOptions = {}
if (process.env.CI) {
reporterOptions = {
reporter: '/overleaf/node_modules/mocha-multi-reporters',
reporter: require.resolve('mocha-multi-reporters'),
'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'],
}
}
@@ -5,5 +5,6 @@ overleaf-editor-core
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=overleaf-editor-core
--pipeline-owner=44
--public-repo=False
@@ -4,11 +4,13 @@
const _ = require('lodash')
const assert = require('check-types').assert
const OError = require('@overleaf/o-error')
const Blob = require('../blob')
const FileData = require('./')
const EagerStringFileData = require('./string_file_data')
const EditOperation = require('../operation/edit_operation')
const EditOperationBuilder = require('../operation/edit_operation_builder')
const TextOperation = require('../operation/text_operation')
/**
* @import { BlobStore, ReadonlyBlobStore, RangesBlob, RawHashFileData, RawLazyStringFileData } from '../types'
@@ -152,7 +154,25 @@ class LazyStringFileData extends FileData {
ranges?.comments,
ranges?.trackedChanges
)
applyOperations(this.operations, file)
try {
applyOperations(this.operations, file)
} catch (err) {
const firstOp = this.operations[0]
const firstOpBaseLength =
firstOp instanceof TextOperation ? firstOp.baseLength : undefined
throw OError.tag(err, 'failed to apply operations in toEager', {
blobHash: this.hash,
blobContentLength: content.length,
metadataStringLength: this.stringLength,
totalOperations: this.operations.length,
firstOpBaseLength,
contentMatchesMetadata: content.length === this.stringLength,
contentMatchesFirstOp:
typeof firstOpBaseLength === 'number'
? content.length === firstOpBaseLength
: undefined,
})
}
return file
}
@@ -172,7 +192,18 @@ class LazyStringFileData extends FileData {
* @param {EditOperation} operation
*/
edit(operation) {
this.stringLength = operation.applyToLength(this.stringLength)
try {
this.stringLength = operation.applyToLength(this.stringLength)
} catch (err) {
const baseLength =
operation instanceof TextOperation ? operation.baseLength : undefined
throw OError.tag(err, 'failed to apply operation length in edit', {
blobHash: this.hash,
metadataStringLength: this.stringLength,
operationBaseLength: baseLength,
totalExistingOperations: this.operations.length,
})
}
this.operations.push(operation)
}
@@ -205,7 +236,17 @@ class LazyStringFileData extends FileData {
* @returns {void}
*/
function applyOperations(operations, file) {
_.each(operations, operation => operation.apply(file))
for (let i = 0; i < operations.length; i++) {
try {
operations[i].apply(file)
} catch (err) {
throw OError.tag(err, 'operation failed during applyOperations', {
operationIndex: i,
totalOperations: operations.length,
currentContentLength: file.getStringLength(),
})
}
}
}
module.exports = LazyStringFileData
+4 -4
View File
@@ -4,10 +4,10 @@
"description": "Library shared between the editor server and clients.",
"main": "index.js",
"scripts": {
"test": "npm run lint && npm run types:check && npm run test:unit",
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test:ci": "npm run test:unit",
"test:ci": "yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"types:check": "tsc --noEmit"
},
@@ -25,9 +25,9 @@
"typescript": "^5.0.4"
},
"dependencies": {
"@overleaf/o-error": "*",
"@overleaf/o-error": "workspace:*",
"check-types": "^5.1.0",
"lodash": "^4.17.19",
"lodash": "^4.18.1",
"p-map": "^4.0.0",
"path-browserify": "^1.0.1"
}
@@ -4,6 +4,7 @@
const _ = require('lodash')
const { expect } = require('chai')
const sinon = require('sinon')
const OError = require('@overleaf/o-error')
const ot = require('../..')
const File = ot.File
@@ -202,4 +203,85 @@ describe('LazyStringFileData', function () {
expect(fileData.hash).to.equal(stored.hash)
expect(fileData.operations).to.deep.equal([])
})
describe('error annotation', function () {
it('annotates errors in toEager with blob and operation metadata', async function () {
const testHash = this.fileHash
// Create a file with content length 19 ('the quick brown fox')
// then queue an operation that expects a different base length
const fileData = new LazyStringFileData(testHash, undefined, 19)
// This operation expects base length 999, which won't match the blob
const badOp = new TextOperation()
badOp.retain(999)
fileData.operations.push(badOp)
// Manually set stringLength to match the op's baseLength: pushing directly
// to operations bypasses edit(), which is what normally updates stringLength
fileData.stringLength = 999
try {
await fileData.toEager(this.blobStore)
expect.fail('should have thrown')
} catch (err) {
const info = OError.getFullInfo(err)
expect(info).to.have.property('blobHash', testHash)
expect(info).to.have.property('blobContentLength', 19)
expect(info).to.have.property('metadataStringLength', 999)
expect(info).to.have.property('totalOperations', 1)
expect(info).to.have.property('operationIndex', 0)
expect(info).to.have.property('currentContentLength', 19)
expect(info).to.have.property('firstOpBaseLength', 999)
expect(info).to.have.property('contentMatchesMetadata', false)
expect(info).to.have.property('contentMatchesFirstOp', false)
}
})
it('annotates errors in edit with operation and metadata context', function () {
const testHash = this.fileHash
const fileData = new LazyStringFileData(testHash, undefined, 19)
// Queue one valid operation first so totalExistingOperations > 0
fileData.edit(new TextOperation().retain(19).insert('!'))
// This operation expects base length 999, mismatching stringLength of 20
const badOp = new TextOperation()
badOp.retain(999)
try {
fileData.edit(badOp)
expect.fail('should have thrown')
} catch (err) {
const info = OError.getFullInfo(err)
expect(info).to.have.property('blobHash', testHash)
expect(info).to.have.property('metadataStringLength', 20)
expect(info).to.have.property('operationBaseLength', 999)
expect(info).to.have.property('totalExistingOperations', 1)
}
})
it('annotates errors in applyOperations with the failing operation index', async function () {
const testHash = this.fileHash
// Content is 'the quick brown fox' (length 19)
const fileData = new LazyStringFileData(testHash, undefined, 19)
// First op is valid: insert at end
const goodOp = new TextOperation().retain(19).insert('!')
// Second op expects length 999 — will fail
const badOp = new TextOperation()
badOp.retain(999)
fileData.operations.push(goodOp)
fileData.operations.push(badOp)
fileData.stringLength = 999
try {
await fileData.toEager(this.blobStore)
expect.fail('should have thrown')
} catch (err) {
const info = OError.getFullInfo(err)
// The second operation (index 1) should be the one that fails
expect(info).to.have.property('operationIndex', 1)
expect(info).to.have.property('totalOperations', 2)
expect(info).to.have.property('currentContentLength', 20)
// toEager also tags with blob metadata
expect(info).to.have.property('blobHash', testHash)
expect(info).to.have.property('blobContentLength', 19)
expect(info).to.have.property('metadataStringLength', 999)
}
})
})
})
@@ -17,6 +17,10 @@ const { RetainOp, InsertOp, RemoveOp } = require('../../lib/operation/scan_op')
const TrackingProps = require('../../lib/file_data/tracking_props')
const ClearTrackingProps = require('../../lib/file_data/clear_tracking_props')
function fuzzingErrorMessage(obj) {
return `Failed randomized test with input: ${JSON.stringify(obj)}`
}
describe('TextOperation', function () {
const numTrials = 500
@@ -145,18 +149,12 @@ describe('TextOperation', function () {
const str = random.string(50)
const comments = random.comments(6)
const o = randomOperation(str, comments.ids)
try {
expect(str.length).to.equal(o.baseLength)
const file = new StringFileData(str, comments.comments)
o.apply(file)
const result = file.getContent()
expect(result.length).to.equal(o.targetLength)
} catch (err) {
if (err instanceof Error) {
err.message = `Failing inputs:\n str: ${JSON.stringify(str)}\n comments: ${JSON.stringify(comments)}\n o: ${JSON.stringify(o.toJSON())}\n\n${err.message}`
}
throw err
}
const fuzzingError = fuzzingErrorMessage({ str, comments, o: o.toJSON() })
expect(str.length).to.equal(o.baseLength, fuzzingError)
const file = new StringFileData(str, comments.comments)
o.apply(file)
const result = file.getContent()
expect(result.length).to.equal(o.targetLength, fuzzingError)
})
)
@@ -166,15 +164,11 @@ describe('TextOperation', function () {
const doc = random.string(50)
const comments = random.comments(2)
const operation = randomOperation(doc, comments.ids)
try {
const roundTripOperation = TextOperation.fromJSON(operation.toJSON())
expect(operation.equals(roundTripOperation)).to.be.true
} catch (err) {
if (err instanceof Error) {
err.message = `Failing inputs:\n doc: ${JSON.stringify(doc)}\n comments: ${JSON.stringify(comments)}\n operation: ${JSON.stringify(operation.toJSON())}\n\n${err.message}`
}
throw err
}
const roundTripOperation = TextOperation.fromJSON(operation.toJSON())
expect(operation.equals(roundTripOperation)).to.equal(
true,
fuzzingErrorMessage({ operation })
)
})
)
@@ -227,22 +221,20 @@ describe('TextOperation', function () {
const str = random.string(50)
const comments = random.comments(6)
const o = randomOperation(str, comments.ids)
try {
const originalFile = new StringFileData(str, comments.comments)
const p = o.invert(originalFile)
expect(o.baseLength).to.equal(p.targetLength)
expect(o.targetLength).to.equal(p.baseLength)
const file = new StringFileData(str, comments.comments)
o.apply(file)
p.apply(file)
const result = file.toRaw()
expect(result).to.deep.equal(originalFile.toRaw())
} catch (err) {
if (err instanceof Error) {
err.message = `Failing inputs:\n str: ${JSON.stringify(str)}\n comments: ${JSON.stringify(comments)}\n o: ${JSON.stringify(o.toJSON())}\n\n${err.message}`
}
throw err
}
const originalFile = new StringFileData(str, comments.comments)
const p = o.invert(originalFile)
const fuzzingError = fuzzingErrorMessage({
str,
comments,
o: o.toJSON(),
})
expect(o.baseLength).to.equal(p.targetLength, fuzzingError)
expect(o.targetLength).to.equal(p.baseLength, fuzzingError)
const file = new StringFileData(str, comments.comments)
o.apply(file)
p.apply(file)
const result = file.toRaw()
expect(result).to.deep.equal(originalFile.toRaw(), fuzzingError)
})
)
@@ -394,26 +386,33 @@ describe('TextOperation', function () {
const str = random.string(20)
const comments = random.comments(6)
const a = randomOperation(str, comments.ids)
const fuzzingError = fuzzingErrorMessage({
str,
comments,
a: a.toJSON(),
})
const file = new StringFileData(str, comments.comments)
a.apply(file)
const afterA = file.toRaw()
expect(afterA.content.length).to.equal(a.targetLength, fuzzingError)
const b = randomOperation(afterA.content, comments.ids)
try {
expect(afterA.content.length).to.equal(a.targetLength)
b.apply(file)
const afterB = file.toRaw()
expect(afterB.content.length).to.equal(b.targetLength)
const ab = a.compose(b)
expect(ab.targetLength).to.equal(b.targetLength)
ab.apply(new StringFileData(str, comments.comments))
const afterAB = file.toRaw()
expect(afterAB).to.deep.equal(afterB)
} catch (err) {
if (err instanceof Error) {
err.message = `Failing inputs:\n str: ${JSON.stringify(str)}\n comments: ${JSON.stringify(comments)}\n a: ${JSON.stringify(a.toJSON())}\n b: ${JSON.stringify(b.toJSON())}\n\n${err.message}`
}
throw err
}
const fuzzingErrorWithB = fuzzingErrorMessage({
str,
comments,
a: a.toJSON(),
b: b.toJSON(),
})
b.apply(file)
const afterB = file.toRaw()
expect(afterB.content.length).to.equal(
b.targetLength,
fuzzingErrorWithB
)
const ab = a.compose(b)
expect(ab.targetLength).to.equal(b.targetLength, fuzzingErrorWithB)
ab.apply(new StringFileData(str, comments.comments))
const afterAB = file.toRaw()
expect(afterAB).to.deep.equal(afterB, fuzzingErrorWithB)
})
)
@@ -622,24 +621,23 @@ describe('TextOperation', function () {
const comments = random.comments(6)
const a = randomOperation(str, comments.ids)
const b = randomOperation(str, comments.ids)
try {
const primes = TextOperation.transform(a, b)
const aPrime = primes[0]
const bPrime = primes[1]
const abPrime = a.compose(bPrime)
const baPrime = b.compose(aPrime)
const abFile = new StringFileData(str, comments.comments)
const baFile = new StringFileData(str, comments.comments)
abPrime.apply(abFile)
baPrime.apply(baFile)
expect(abPrime.equals(baPrime)).to.be.true
expect(abFile.toRaw()).to.deep.equal(baFile.toRaw())
} catch (err) {
if (err instanceof Error) {
err.message = `Failing inputs:\n str: ${JSON.stringify(str)}\n comments: ${JSON.stringify(comments)}\n a: ${JSON.stringify(a.toJSON())}\n b: ${JSON.stringify(b.toJSON())}\n\n${err.message}`
}
throw err
}
const primes = TextOperation.transform(a, b)
const aPrime = primes[0]
const bPrime = primes[1]
const abPrime = a.compose(bPrime)
const baPrime = b.compose(aPrime)
const abFile = new StringFileData(str, comments.comments)
const baFile = new StringFileData(str, comments.comments)
abPrime.apply(abFile)
baPrime.apply(baFile)
const fuzzingError = fuzzingErrorMessage({
str,
comments,
a: a.toJSON(),
b: b.toJSON(),
})
expect(abPrime.equals(baPrime)).to.be.equal(true, fuzzingError)
expect(abFile.toRaw()).to.deep.equal(baFile.toRaw(), fuzzingError)
})
)
+1 -1
View File
@@ -1,7 +1,7 @@
let reporterOptions = {}
if (process.env.CI) {
reporterOptions = {
reporter: '/overleaf/node_modules/mocha-multi-reporters',
reporter: require.resolve('mocha-multi-reporters'),
'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'],
}
}
+1
View File
@@ -5,5 +5,6 @@ promise-utils
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/promise-utils
--pipeline-owner=32
--public-repo=False
+2 -2
View File
@@ -4,11 +4,11 @@
"description": "utilities to help working with promises",
"main": "index.js",
"scripts": {
"test": "npm run lint && npm run types:check && npm run test:unit",
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test:ci": "npm run test:unit",
"test:ci": "yarn run test:unit",
"types:check": "tsc --noEmit"
},
"author": "Overleaf (https://www.overleaf.com)",
+1 -1
View File
@@ -1,7 +1,7 @@
let reporterOptions = {}
if (process.env.CI) {
reporterOptions = {
reporter: '/overleaf/node_modules/mocha-multi-reporters',
reporter: require.resolve('mocha-multi-reporters'),
'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'],
}
}
+1
View File
@@ -5,5 +5,6 @@ ranges-tracker
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/ranges-tracker
--pipeline-owner=44
--public-repo=False
+2 -2
View File
@@ -11,8 +11,8 @@
"scripts": {
"lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test": "npm run lint && npm run types:check && npm run test:unit",
"test:ci": "npm run test:unit",
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"test:ci": "yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"types:check": "tsc --noEmit"
},
+1 -1
View File
@@ -1,7 +1,7 @@
let reporterOptions = {}
if (process.env.CI) {
reporterOptions = {
reporter: '/overleaf/node_modules/mocha-multi-reporters',
reporter: require.resolve('mocha-multi-reporters'),
'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'],
}
}
+1
View File
@@ -5,5 +5,6 @@ redis-wrapper
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/redis-wrapper
--pipeline-owner=32
--public-repo=False
+7 -7
View File
@@ -15,23 +15,23 @@
"scripts": {
"lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test": "npm run lint && npm run types:check && npm run test:unit",
"test:ci": "npm run test:unit",
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"test:ci": "yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"types:check": "tsc --noEmit"
},
"peerDependencies": {
"@overleaf/logger": "*",
"@overleaf/metrics": "*",
"@overleaf/o-error": "*"
"@overleaf/logger": "workspace:*",
"@overleaf/metrics": "workspace:*",
"@overleaf/o-error": "workspace:*"
},
"dependencies": {
"async": "^3.2.5",
"ioredis": "~4.27.1"
},
"devDependencies": {
"@overleaf/logger": "*",
"@overleaf/o-error": "*",
"@overleaf/logger": "workspace:*",
"@overleaf/o-error": "workspace:*",
"chai": "^4.3.6",
"mocha": "^11.1.0",
"mocha-junit-reporter": "^2.2.1",
+1
View File
@@ -5,5 +5,6 @@ settings
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/settings
--pipeline-owner=32
--public-repo=False
+2 -2
View File
@@ -7,8 +7,8 @@
"scripts": {
"lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test": "npm run lint && npm run types:check && npm run test:unit",
"test:ci": "npm run test:unit",
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"test:ci": "yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"types:check": "tsc --noEmit"
},
+1 -1
View File
@@ -1,7 +1,7 @@
let reporterOptions = {}
if (process.env.CI) {
reporterOptions = {
reporter: '/overleaf/node_modules/mocha-multi-reporters',
reporter: require.resolve('mocha-multi-reporters'),
'reporter-options': ['configFile=./test/mocha-multi-reporters.cjs'],
}
}
+1
View File
@@ -5,5 +5,6 @@ stream-utils
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/stream-utils
--pipeline-owner=32
--public-repo=False
+2 -2
View File
@@ -4,11 +4,11 @@
"description": "stream handling utilities",
"main": "index.js",
"scripts": {
"test": "npm run lint && npm run types:check && npm run test:unit",
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}",
"lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test:ci": "npm run test:unit",
"test:ci": "yarn run test:unit",
"types:check": "tsc --noEmit"
},
"author": "Overleaf (https://www.overleaf.com)",
+26 -2
View File
@@ -1,5 +1,29 @@
// @ts-check
const OError = require('@overleaf/o-error')
class ParamsError extends OError {}
/**
* @typedef {import('zod').ZodError} ZodError
*/
module.exports = { ParamsError }
class InvalidRequestError extends OError {
/**
* @param {ZodError} zodError
*/
constructor(zodError) {
super('Invalid request', {}, zodError)
this.zodError = zodError
}
}
class InvalidParamsError extends OError {
/**
* @param {ZodError} zodError
*/
constructor(zodError) {
super('Invalid request parameters', {}, zodError)
this.zodError = zodError
}
}
module.exports = { InvalidParamsError, InvalidRequestError }
@@ -5,6 +5,7 @@ validation-tools
--esmock-loader=False
--is-library=True
--node-version=24.14.1
--package-name=@overleaf/validation-tools
--public-repo=False
--test-acceptance-vitest=True
--test-unit-vitest=True
@@ -1,15 +1,20 @@
const { isZodErrorLike, fromError } = require('zod-validation-error')
const { fromError } = require('zod-validation-error')
const { InvalidParamsError, InvalidRequestError } = require('./Errors')
function createHandleValidationError(statusCode = 400) {
return [
(err, req, res, next) => {
if (!isZodErrorLike(err)) {
return next(err)
}
res.status(statusCode).json({ ...fromError(err), statusCode })
},
]
return (err, req, res, next) => {
if (err instanceof InvalidParamsError) {
res
.status(404)
.json({ error: fromError(err.zodError).toString(), statusCode: 404 })
} else if (err instanceof InvalidRequestError) {
res
.status(statusCode)
.json({ error: fromError(err.zodError).toString(), statusCode })
} else {
next(err)
}
}
}
const handleValidationError = createHandleValidationError(400)
+3 -2
View File
@@ -1,4 +1,4 @@
const { ParamsError } = require('./Errors')
const { InvalidParamsError, InvalidRequestError } = require('./Errors')
const { z } = require('zod')
const { zz } = require('./zodHelpers')
const { parseReq } = require('./parseReq')
@@ -15,5 +15,6 @@ module.exports = {
parseReq,
handleValidationError,
createHandleValidationError,
ParamsError,
InvalidRequestError,
InvalidParamsError,
}
+4 -4
View File
@@ -10,21 +10,21 @@
"license": "AGPL-3.0-only",
"version": "1.0.0",
"scripts": {
"test": "npm run lint && npm run types:check && npm run test:unit",
"test": "yarn run lint && yarn run types:check && yarn run test:unit",
"lint": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --ext .cjs,.js,.jsx,.mjs,.ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --cache --cache-location ../../node_modules/.cache/eslint/ --fix --ext .cjs,.js,.jsx,.mjs,.ts .",
"test:ci": "npm run test:unit",
"test:ci": "yarn run test:unit",
"test:unit": "vitest --config vitest.config.ts",
"types:check": "tsc --noEmit"
},
"dependencies": {
"@overleaf/o-error": "*",
"@overleaf/o-error": "workspace:*",
"mongodb": "^6.12.0",
"zod": "^4.1.8",
"zod-validation-error": "^4.0.1"
},
"devDependencies": {
"typescript": "^5.0.4",
"vitest": "^4.0.0"
"vitest": "4.1.5"
}
}
+3 -3
View File
@@ -1,5 +1,5 @@
// @ts-check
const { ParamsError } = require('./Errors')
const { InvalidRequestError, InvalidParamsError } = require('./Errors')
/**
* @typedef {import('zod').ZodType} ZodType
@@ -25,9 +25,9 @@ function parseReq(req, schema) {
return parsed.data
} else if (parsed.error.issues.some(issue => issue.path[0] === 'params')) {
// Parts of the URL path failed to validate; throw a specific error
throw new ParamsError('Invalid params').withCause(parsed.error)
throw new InvalidParamsError(parsed.error)
} else {
throw parsed.error
throw new InvalidRequestError(parsed.error)
}
}
@@ -54,7 +54,7 @@ describe('parseReq', () => {
}),
})
)
).toThrowError(expect.objectContaining({ name: 'ParamsError' }))
).toThrowError(expect.objectContaining({ name: 'InvalidParamsError' }))
})
it('should throw an error containing issues if the schema is invalid', () => {
@@ -75,9 +75,11 @@ describe('parseReq', () => {
)
).toThrowError(
expect.objectContaining({
issues: expect.arrayContaining([
expect.objectContaining({ path: ['body', 'name'] }),
]),
zodError: expect.objectContaining({
issues: expect.arrayContaining([
expect.objectContaining({ path: ['body', 'name'] }),
]),
}),
})
)
})
@@ -217,4 +217,121 @@ describe('zodHelpers', () => {
])
})
})
describe('buildId', () => {
it('fails to parse when provided with an invalid buildId', () => {
const parsed = zz.buildId().safeParse('aa')
expect(parsed.success).toBe(false)
expect(parsed.error?.issues).toHaveLength(1)
expect(parsed.error?.issues).toMatchObject([
expect.objectContaining({
message: 'invalid buildId',
}),
])
})
it('parses successfully when provided with a valid buildId', () => {
const parsed = zz.buildId().safeParse('19d6c341530-878fff6cdab7fb0c')
expect(parsed.success).toBe(true)
expect(parsed.data).toBe('19d6c341530-878fff6cdab7fb0c')
})
it('fails to parse when provided with an editorBuildId', () => {
const parsed = zz
.buildId()
.safeParse(
'03b1d773-6203-4669-b365-6a0aa5625878-19d6c341530-878fff6cdab7fb0c'
)
expect(parsed.success).toBe(false)
expect(parsed.error?.issues).toHaveLength(1)
expect(parsed.error?.issues).toMatchObject([
expect.objectContaining({
message: 'invalid buildId',
}),
])
})
})
describe('editorBuildId', () => {
it('fails to parse when provided with an invalid buildId', () => {
const parsed = zz.editorBuildId().safeParse('aa')
expect(parsed.success).toBe(false)
expect(parsed.error?.issues).toHaveLength(1)
expect(parsed.error?.issues).toMatchObject([
expect.objectContaining({
message: 'invalid editorId-buildId',
}),
])
})
it('fails to parse when provided with a buildId', () => {
const parsed = zz
.editorBuildId()
.safeParse('19d6c341530-878fff6cdab7fb0c')
expect(parsed.success).toBe(false)
expect(parsed.error?.issues).toHaveLength(1)
expect(parsed.error?.issues).toMatchObject([
expect.objectContaining({
message: 'invalid editorId-buildId',
}),
])
})
it('parses successfully when provided with a valid editorId-buildId', () => {
const parsed = zz
.editorBuildId()
.safeParse(
'03b1d773-6203-4669-b365-6a0aa5625878-19d6c341530-878fff6cdab7fb0c'
)
expect(parsed.success).toBe(true)
expect(parsed.data).toBe(
'03b1d773-6203-4669-b365-6a0aa5625878-19d6c341530-878fff6cdab7fb0c'
)
})
})
describe('filepath', () => {
it('fails to parse with empty input', () => {
const parsed = zz.filepath().safeParse('')
expect(parsed.success).toBe(false)
expect(parsed.error?.issues).toHaveLength(1)
expect(parsed.error?.issues).toMatchObject([
expect.objectContaining({
message: 'path is empty',
}),
])
})
it('fails to parse with absolute path', () => {
const parsed = zz.filepath().safeParse('/output.pdf')
expect(parsed.success).toBe(false)
expect(parsed.error?.issues).toHaveLength(1)
expect(parsed.error?.issues).toMatchObject([
expect.objectContaining({
message: 'path is absolute',
}),
])
})
it('fails to parse when provided with path traversal', () => {
const parsed = zz.filepath().safeParse('../output.pdf')
expect(parsed.success).toBe(false)
expect(parsed.error?.issues).toHaveLength(1)
expect(parsed.error?.issues).toMatchObject([
expect.objectContaining({
message: 'path traversal detected',
}),
])
})
it('parses successfully when provided a valid path', () => {
const parsed = zz.filepath().safeParse('output.pdf')
expect(parsed.success).toBe(true)
expect(parsed.data).toBe('output.pdf')
})
it('parses successfully when provided a valid nested path', () => {
const parsed = zz.filepath().safeParse('foo/output.pdf')
expect(parsed.success).toBe(true)
expect(parsed.data).toBe('foo/output.pdf')
})
})
})
+8
View File
@@ -0,0 +1,8 @@
function asZodError(...def) {
return {
name: 'ZodError',
_zod: { def },
}
}
module.exports = { asZodError }
+25
View File
@@ -33,6 +33,31 @@ const zz = {
datetimeNullable: options => datetimeSchema({ ...options, allowNull: true }),
datetimeNullish: options =>
datetimeSchema({ ...options, allowNull: true, allowUndefined: true }),
buildId: () =>
z.string().regex(/^[0-9a-f]+-[0-9a-f]+$/, { message: 'invalid buildId' }),
editorBuildId: () =>
z.string().regex(/^[a-f0-9-]{36}-[0-9a-f]+-[0-9a-f]+$/, {
message: 'invalid editorId-buildId',
}),
clsiServerId: () =>
z.string().regex(/^[a-z0-9-]+$/, { message: 'invalid clsiServerId' }),
compileBackendClass: () =>
z
.string()
.regex(/^[a-z0-9-]+$/, { message: 'invalid compileBackendClass' }),
compileGroup: () =>
z.enum(['alpha', 'gvisor', 'standard', 'priority'], {
message: 'invalid compileGroup',
}),
submissionId: () => z.string().regex(/^[a-zA-Z0-9_-]+$/),
filepath: () =>
z
.string()
.nonempty({ message: 'path is empty' })
.refine(s => !s.startsWith('/'), { message: 'path is absolute' })
.refine(s => !s.split('/').includes('..'), {
message: 'path traversal detected',
}),
}
module.exports = { zz }
-55115
View File
File diff suppressed because it is too large Load Diff
+191 -37
View File
@@ -1,52 +1,209 @@
{
"name": "overleaf",
"private": true,
"dependencies": {
"patch-package": "^8.0.0"
},
"packageManager": "yarn@4.14.1",
"devDependencies": {
"@eslint/compat": "^2.1.0",
"@eslint/js": "^10.0.1",
"@overleaf/eslint-plugin": "workspace:*",
"@prettier/plugin-pug": "^3.4.0",
"@types/chai": "^4.3.0",
"@types/chai-as-promised": "^7.1.8",
"@types/mocha": "^10.0.6",
"@types/multer": "^2.1.0",
"@typescript-eslint/eslint-plugin": "8.50.0",
"@typescript-eslint/parser": "^8.50.0",
"@typescript-eslint/eslint-plugin": "^8.59.4",
"@typescript-eslint/parser": "^8.59.4",
"@vitest/eslint-plugin": "^1.5.0",
"eslint": "^8.15.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-chai-expect": "^3.0.0",
"eslint-plugin-chai-friendly": "^0.7.2",
"eslint-plugin-cypress": "^2.15.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-mocha": "^10.1.0",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-promise": "^6.0.0",
"eslint": "^10.4.0",
"eslint-config-prettier": "^10.0.1",
"eslint-formatter-unix": "^8.40.0",
"eslint-plugin-chai-expect": "^4.0.0",
"eslint-plugin-chai-friendly": "^1.1.0",
"eslint-plugin-cypress": "^4.1.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-mocha": "^11.0.0",
"eslint-plugin-n": "^18.0.0",
"eslint-plugin-promise": "^7.2.1",
"eslint-plugin-unicorn": "^56.0.0",
"globals": "^17.6.0",
"prettier": "3.7.4",
"prettier-plugin-groovy": "0.2.1",
"typescript": "^5.9.3"
},
"engines": {
"npm": "11.11.0"
"node": ">=20.19.0"
},
"overrides": {
"request@2.88.2": {
"tough-cookie": "5.1.2",
"form-data": "2.5.5",
"qs": "6.14.1"
},
"cypress@13.13.2": {
"@cypress/request@3.0.9": {
"qs": "6.14.1"
}
},
"resolutions": {
"@xmldom/xmldom": "0.8.13",
"argparse/underscore": "1.13.8",
"east/underscore": "1.13.8",
"referer-parser/js-yaml": "^4.1.1",
"sandboxed-module": "patch:sandboxed-module@npm%3A2.0.4#~/.yarn/patches/sandboxed-module-npm-2.0.4-f8b45aacc9.patch",
"request/tough-cookie": "5.1.2",
"request/form-data": "2.5.5",
"request/qs": "6.14.1",
"@cypress/request/qs": "6.14.1",
"@opentelemetry/api": "1.9.0",
"mocha@^11.1.0": {
"serialize-javascript": "7.0.5"
}
"mocha/serialize-javascript": "7.0.5",
"pprof/protobufjs": "7.5.5",
"@google-cloud/profiler/protobufjs": "7.5.5",
"mocha-multi-reporters": "patch:mocha-multi-reporters@npm%3A1.5.1#~/.yarn/patches/mocha-multi-reporters-npm-1.5.1-0a1088aed5.patch",
"pdfjs-dist": "patch:pdfjs-dist@npm%3A5.1.91#~/.yarn/patches/pdfjs-dist-npm-5.1.91.patch",
"referer-parser": "patch:referer-parser@npm%3A0.0.3#~/.yarn/patches/referer-parser-npm-0.0.3.patch",
"sass": "1.77.1",
"@codemirror/autocomplete": "patch:@codemirror/autocomplete@npm%3A6.18.4#~/.yarn/patches/@codemirror-autocomplete-npm-6.18.4.patch",
"@codemirror/commands": "6.10.1",
"@codemirror/language": "6.12.1",
"@codemirror/lint": "6.9.2",
"@codemirror/search": "patch:@codemirror/search@npm%3A6.5.8#~/.yarn/patches/@codemirror-search-npm-6.5.8.patch",
"@codemirror/state": "6.5.4",
"@codemirror/view": "6.38.6",
"@lezer/common": "1.5.0",
"@lezer/highlight": "1.2.3",
"@lezer/lr": "1.4.7",
"@types/react": "18.3.28",
"@types/react-dom": "18.3.7",
"@vitest/expect": "4.1.5",
"@vitest/mocker": "4.1.5",
"@vitest/pretty-format": "4.1.5",
"@vitest/spy": "4.1.5",
"@vitest/utils": "4.1.5",
"cheerio": "1.0.0-rc.10",
"react": "18.3.1",
"react-dom": "18.3.1",
"sinon-chai": "3.7.0",
"i18next-scanner/i18next": "23.16.8",
"downshift": "9.0.9",
"body-parser@npm:1.20.4": "patch:body-parser@npm%3A1.20.4#~/.yarn/patches/body-parser-npm-1.20.4.patch",
"cypress-multi-reporters": "patch:cypress-multi-reporters@npm%3A2.0.5#~/.yarn/patches/cypress-multi-reporters-npm-2.0.5.patch",
"forwarded@npm:0.2.0": "patch:forwarded@npm%3A0.2.0#~/.yarn/patches/forwarded-npm-0.2.0.patch",
"multer@npm:2.1.1": "patch:multer@npm%3A2.1.1#~/.yarn/patches/multer-npm-2.1.1.patch",
"node-fetch": "patch:node-fetch@npm%3A2.7.0#~/.yarn/patches/node-fetch-npm-2.7.0.patch",
"passport-oauth2": "patch:passport-oauth2@npm%3A1.6.1#~/.yarn/patches/passport-oauth2-npm-1.6.1.patch",
"send": "patch:send@npm%3A0.19.0#~/.yarn/patches/send-npm-0.19.0.patch",
"serve-static": "1.16.2",
"@uppy/xhr-upload": "3.6.0",
"recurly": "4.12.0",
"mongoose": "8.22.1",
"pg": "8.7.1",
"pg-query-stream": "4.7.1",
"@aws-sdk/client-s3": "3.994.0",
"@aws-sdk/client-ses": "3.994.0",
"@aws-sdk/s3-request-presigner": "3.994.0",
"contentful": "10.8.5",
"@contentful/rich-text-html-renderer": "16.0.2",
"@contentful/rich-text-types": "16.0.2",
"i18next": "23.10.0",
"sanitize-html": "2.17.4",
"lodash": "4.18.1",
"express-session": "1.17.2",
"ioredis": "4.27.11",
"knip": "5.64.1",
"eslint-plugin-testing-library": "7.5.3",
"chart.js": "4.0.1",
"@customerio/cdp-analytics-node": "0.3.9",
"@google-cloud/bigquery": "8.1.1",
"moment": "2.29.4",
"sequelize-cli": "6.6.0",
"async": "3.2.5",
"dockerode": "4.0.9",
"tar-fs": "3.1.1",
"cluster-key-slot": "1.1.0",
"@octokit/request": "9.2.2",
"randomstring": "1.2.2",
"vite": "7.3.1",
"isomorphic-git": "1.33.1",
"http-status": "1.5.0",
"knex": "2.4.0",
"utf-8-validate": "5.0.8",
"samlp": "7.0.2",
"compression": "1.7.4",
"cookie-parser": "1.4.6",
"react-cookie": "7.2.0",
"react-dropzone": "14.2.3",
"@babel/core": "7.28.5",
"@babel/preset-env": "7.28.5",
"@babel/register": "7.28.3",
"@vitejs/plugin-react": "4.4.1",
"cssnano": "7.1.4",
"mini-css-extract-plugin": "2.7.6",
"nodemon": "3.0.1",
"postcss": "8.5.8",
"postcss-reporter": "7.0.5",
"zod": "4.1.11",
"zod-validation-error": "4.0.1",
"simple-oauth2": "5.0.0",
"@types/simple-oauth2": "5.0.7",
"@node-oauth/oauth2-server": "5.3.0",
"@phosphor-icons/react": "2.1.7",
"@slack/webhook": "7.0.2",
"@stripe/react-stripe-js": "3.9.0",
"@stripe/stripe-js": "7.7.0",
"cache-flow": "1.9.0",
"focus-trap-react": "11.0.4",
"i18next-http-middleware": "3.5.0",
"jose": "4.15.5",
"nodemailer": "8.0.5",
"on-headers": "1.0.2",
"pug": "3.0.3",
"rate-limiter-flexible": "2.4.1",
"react-hook-form": "7.71.1",
"stripe": "18.4.0",
"@babel/plugin-proposal-decorators": "7.28.0",
"@floating-ui/react": "0.27.16",
"@juggle/resize-observer": "3.3.1",
"@storybook/addon-a11y": "10.3.5",
"@storybook/addon-essentials": "10.3.5",
"@storybook/addon-interactions": "10.3.5",
"@storybook/addon-links": "10.3.5",
"@storybook/cli": "10.3.5",
"@storybook/react": "10.3.5",
"@storybook/react-webpack5": "10.3.5",
"@storybook/theming": "10.3.5",
"@streamdown/cjk": "1.0.2",
"@testing-library/dom": "10.4.0",
"@testing-library/user-event": "14.5.2",
"@types/express": "4.17.23",
"@types/recurly__recurly-js": "4.38.0",
"@types/sanitize-html": "2.16.0",
"@uppy/dashboard": "3.7.1",
"@uppy/drag-drop": "3.0.3",
"@uppy/file-input": "3.0.4",
"@uppy/progress-bar": "3.0.4",
"@uppy/react": "3.2.1",
"autoprefixer": "10.4.16",
"babel-plugin-module-resolver": "5.0.2",
"backbone": "1.6.0",
"dompurify": "3.4.0",
"eventsource-client": "1.1.4",
"fake-indexeddb": "6.0.0",
"formik": "2.2.9",
"katex": "0.16.28",
"match-sorter": "6.3.1",
"micromark": "4.0.0",
"pirates": "4.0.6",
"qrcode": "1.5.0",
"react-chartjs-2": "5.0.1",
"react-i18next": "13.3.1",
"react-resizable-panels": "2.1.1",
"rehype-harden": "1.1.7",
"scroll-into-view-if-needed": "2.2.28",
"storybook": "10.3.5",
"streamdown": "2.2.0",
"tailwindcss": "3.4.17",
"thread-loader": "patch:thread-loader@npm%3A4.0.2#~/.yarn/patches/thread-loader-npm-4.0.2-dab5735f54.patch",
"unist-util-visit": "5.0.0",
"use-stick-to-bottom": "1.1.1",
"zustand": "5.0.8",
"retry-request@npm:^8.0.0": "patch:retry-request@npm%3A8.0.2#~/.yarn/patches/retry-request-npm-8.0.2-448ad084c8.patch",
"retry-request@npm:^7.0.0": "patch:retry-request@npm%3A7.0.2#~/.yarn/patches/retry-request-npm-7.0.2-a41087680c.patch",
"teeny-request@npm:^10.0.0": "patch:teeny-request@npm%3A10.1.0#~/.yarn/patches/teeny-request-npm-10.1.0.patch",
"teeny-request@npm:^9.0.0": "patch:teeny-request@npm%3A9.0.0#~/.yarn/patches/teeny-request-npm-9.0.0-4d571e3c55.patch",
"@babel/plugin-transform-modules-systemjs": "7.29.4",
"fast-xml-builder": "1.1.7",
"ip-address": "10.1.1",
"systeminformation": "5.31.6",
"fast-uri": "3.1.2"
},
"scripts": {
"format": "prettier --cache --cache-location ./node_modules/.cache/prettier/.prettier-cache --check",
@@ -57,11 +214,10 @@
"format:pug:fix": "prettier --cache --cache-location ./node_modules/.cache/prettier/.prettier-cache --write --check '**/*.pug'",
"format:jenkins": "prettier --cache --cache-location ./node_modules/.cache/prettier/.prettier-cache --check '**/Jenkinsfile'",
"format:jenkins:fix": "prettier --cache --cache-location ./node_modules/.cache/prettier/.prettier-cache --write --check '**/Jenkinsfile'",
"format:monorepo-check": "prettier --cache --cache-location ./node_modules/.cache/prettier/.prettier-cache --check '**/Jenkinsfile' '**/*.md' '**/docker-compose.yml' '**/docker-compose.*.yml'",
"format:monorepo-check:fix": "prettier --cache --cache-location ./node_modules/.cache/prettier/.prettier-cache --write --check '**/Jenkinsfile' '**/*.md' '**/docker-compose.yml' '**/docker-compose.*.yml'",
"format:monorepo-check": "prettier --cache --cache-location ./node_modules/.cache/prettier/.prettier-cache --check '**/Jenkinsfile' '**/*.md' '**/docker-compose.yml' '**/docker-compose.*.yml' '.agents/**/*.js' '.agents/**/*.md'",
"format:monorepo-check:fix": "prettier --cache --cache-location ./node_modules/.cache/prettier/.prettier-cache --write --check '**/Jenkinsfile' '**/*.md' '**/docker-compose.yml' '**/docker-compose.*.yml' '.agents/**/*.js' '.agents/**/*.md'",
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint/ --max-warnings 0 --format unix .",
"lint:fix": "eslint --cache --cache-location ./node_modules/.cache/eslint/ --fix .",
"postinstall": "patch-package"
"lint:fix": "eslint --cache --cache-location ./node_modules/.cache/eslint/ --fix ."
},
"workspaces": [
"jobs/mirror-documentation",
@@ -72,7 +228,6 @@
"services/clsi",
"services/clsi-cache",
"services/clsi-perf",
"services/contacts",
"services/docstore",
"services/document-updater",
"services/filestore",
@@ -87,7 +242,6 @@
"services/templates",
"services/third-party-datastore",
"services/third-party-references",
"services/tpdsworker",
"services/web",
"tools/dependency-management",
"tools/npm-overrides-helper",
@@ -1,58 +0,0 @@
diff --git a/node_modules/@google-cloud/logging/node_modules/teeny-request/build/src/index.js b/node_modules/@google-cloud/logging/node_modules/teeny-request/build/src/index.js
index af5d15e..2b63d0c 100644
--- a/node_modules/@google-cloud/logging/node_modules/teeny-request/build/src/index.js
+++ b/node_modules/@google-cloud/logging/node_modules/teeny-request/build/src/index.js
@@ -115,6 +115,9 @@ function createMultipartStream(boundary, multipart) {
}
else {
part.body.pipe(stream, { end: false });
+ part.body.on('error', (err) => {
+ stream.destroy(err);
+ });
part.body.on('end', () => {
stream.write('\r\n');
stream.write(finale);
@@ -168,25 +171,27 @@ function teenyRequest(reqOpts, callback) {
// Stream mode
const requestStream = streamEvents(new stream_1.PassThrough());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- let responseStream;
- requestStream.once('reading', () => {
- if (responseStream) {
- (0, stream_1.pipeline)(responseStream, requestStream, () => { });
- }
- else {
- requestStream.once('response', () => {
- (0, stream_1.pipeline)(responseStream, requestStream, () => { });
- });
- }
- });
+ // let responseStream;
+ // requestStream.once('reading', () => {
+ // if (responseStream) {
+ // (0, stream_1.pipeline)(responseStream, requestStream, () => { });
+ // }
+ // else {
+ // requestStream.once('response', () => {
+ // (0, stream_1.pipeline)(responseStream, requestStream, () => { });
+ // });
+ // }
+ // });
+
+
options.compress = false;
teenyRequest.stats.requestStarting();
(0, node_fetch_1.default)(uri, options).then(res => {
- teenyRequest.stats.requestFinished();
- responseStream = res.body;
- responseStream.on('error', (err) => {
- requestStream.emit('error', err);
- });
+ teenyRequest.stats.requestFinished(); stream_1.pipeline(res.body, requestStream, () => {});
+ // responseStream = res.body;
+ // responseStream.on('error', (err) => {
+ // requestStream.emit('error', err);
+ // });
const response = fetchToRequestResponse(options, res);
requestStream.emit('response', response);
}, err => {
@@ -1,30 +0,0 @@
diff --git a/node_modules/@google-cloud/logging-min/node_modules/retry-request/index.js b/node_modules/@google-cloud/logging-min/node_modules/retry-request/index.js
index 2fae107..5721c54 100644
--- a/node_modules/@google-cloud/logging-min/node_modules/retry-request/index.js
+++ b/node_modules/@google-cloud/logging-min/node_modules/retry-request/index.js
@@ -1,6 +1,6 @@
'use strict';
-const {PassThrough} = require('stream');
+const { PassThrough, pipeline } = require('stream');
const extend = require('extend');
let debug = () => {};
@@ -185,7 +185,7 @@ function retryRequest(requestOpts, opts, callback) {
.on('complete', (...params) => handleFinish(params))
.on('finish', (...params) => handleFinish(params));
- requestStream.pipe(delayStream);
+ pipeline(requestStream, delayStream, () => {});
} else {
activeRequest = opts.request(requestOpts, onResponse);
}
@@ -251,7 +251,7 @@ function retryRequest(requestOpts, opts, callback) {
// No more attempts need to be made, just continue on.
if (streamMode) {
retryStream.emit('response', response);
- delayStream.pipe(retryStream);
+ pipeline(delayStream, retryStream, () => {});
requestStream.on('error', err => {
retryStream.destroy(err);
});
@@ -1,58 +0,0 @@
diff --git a/node_modules/@google-cloud/logging-min/node_modules/teeny-request/build/src/index.js b/node_modules/@google-cloud/logging-min/node_modules/teeny-request/build/src/index.js
index af5d15e..2b63d0c 100644
--- a/node_modules/@google-cloud/logging-min/node_modules/teeny-request/build/src/index.js
+++ b/node_modules/@google-cloud/logging-min/node_modules/teeny-request/build/src/index.js
@@ -115,6 +115,9 @@ function createMultipartStream(boundary, multipart) {
}
else {
part.body.pipe(stream, { end: false });
+ part.body.on('error', (err) => {
+ stream.destroy(err);
+ });
part.body.on('end', () => {
stream.write('\r\n');
stream.write(finale);
@@ -168,25 +171,27 @@ function teenyRequest(reqOpts, callback) {
// Stream mode
const requestStream = streamEvents(new stream_1.PassThrough());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- let responseStream;
- requestStream.once('reading', () => {
- if (responseStream) {
- (0, stream_1.pipeline)(responseStream, requestStream, () => { });
- }
- else {
- requestStream.once('response', () => {
- (0, stream_1.pipeline)(responseStream, requestStream, () => { });
- });
- }
- });
+ // let responseStream;
+ // requestStream.once('reading', () => {
+ // if (responseStream) {
+ // (0, stream_1.pipeline)(responseStream, requestStream, () => { });
+ // }
+ // else {
+ // requestStream.once('response', () => {
+ // (0, stream_1.pipeline)(responseStream, requestStream, () => { });
+ // });
+ // }
+ // });
+
+
options.compress = false;
teenyRequest.stats.requestStarting();
(0, node_fetch_1.default)(uri, options).then(res => {
- teenyRequest.stats.requestFinished();
- responseStream = res.body;
- responseStream.on('error', (err) => {
- requestStream.emit('error', err);
- });
+ teenyRequest.stats.requestFinished(); stream_1.pipeline(res.body, requestStream, () => {});
+ // responseStream = res.body;
+ // responseStream.on('error', (err) => {
+ // requestStream.emit('error', err);
+ // });
const response = fetchToRequestResponse(options, res);
requestStream.emit('response', response);
}, err => {
@@ -1,30 +0,0 @@
diff --git a/node_modules/@google-cloud/profiler/node_modules/retry-request/index.js b/node_modules/@google-cloud/profiler/node_modules/retry-request/index.js
index 2fae107..5721c54 100644
--- a/node_modules/@google-cloud/profiler/node_modules/retry-request/index.js
+++ b/node_modules/@google-cloud/profiler/node_modules/retry-request/index.js
@@ -1,6 +1,6 @@
'use strict';
-const {PassThrough} = require('stream');
+const { PassThrough, pipeline } = require('stream');
const extend = require('extend');
let debug = () => {};
@@ -185,7 +185,7 @@ function retryRequest(requestOpts, opts, callback) {
.on('complete', (...params) => handleFinish(params))
.on('finish', (...params) => handleFinish(params));
- requestStream.pipe(delayStream);
+ pipeline(requestStream, delayStream, () => {});
} else {
activeRequest = opts.request(requestOpts, onResponse);
}
@@ -251,7 +251,7 @@ function retryRequest(requestOpts, opts, callback) {
// No more attempts need to be made, just continue on.
if (streamMode) {
retryStream.emit('response', response);
- delayStream.pipe(retryStream);
+ pipeline(delayStream, retryStream, () => {});
requestStream.on('error', err => {
retryStream.destroy(err);
});
@@ -1,58 +0,0 @@
diff --git a/node_modules/@google-cloud/profiler/node_modules/teeny-request/build/src/index.js b/node_modules/@google-cloud/profiler/node_modules/teeny-request/build/src/index.js
index af5d15e..2b63d0c 100644
--- a/node_modules/@google-cloud/profiler/node_modules/teeny-request/build/src/index.js
+++ b/node_modules/@google-cloud/profiler/node_modules/teeny-request/build/src/index.js
@@ -115,6 +115,9 @@ function createMultipartStream(boundary, multipart) {
}
else {
part.body.pipe(stream, { end: false });
+ part.body.on('error', (err) => {
+ stream.destroy(err);
+ });
part.body.on('end', () => {
stream.write('\r\n');
stream.write(finale);
@@ -168,25 +171,27 @@ function teenyRequest(reqOpts, callback) {
// Stream mode
const requestStream = streamEvents(new stream_1.PassThrough());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- let responseStream;
- requestStream.once('reading', () => {
- if (responseStream) {
- (0, stream_1.pipeline)(responseStream, requestStream, () => { });
- }
- else {
- requestStream.once('response', () => {
- (0, stream_1.pipeline)(responseStream, requestStream, () => { });
- });
- }
- });
+ // let responseStream;
+ // requestStream.once('reading', () => {
+ // if (responseStream) {
+ // (0, stream_1.pipeline)(responseStream, requestStream, () => { });
+ // }
+ // else {
+ // requestStream.once('response', () => {
+ // (0, stream_1.pipeline)(responseStream, requestStream, () => { });
+ // });
+ // }
+ // });
+
+
options.compress = false;
teenyRequest.stats.requestStarting();
(0, node_fetch_1.default)(uri, options).then(res => {
- teenyRequest.stats.requestFinished();
- responseStream = res.body;
- responseStream.on('error', (err) => {
- requestStream.emit('error', err);
- });
+ teenyRequest.stats.requestFinished(); stream_1.pipeline(res.body, requestStream, () => {});
+ // responseStream = res.body;
+ // responseStream.on('error', (err) => {
+ // requestStream.emit('error', err);
+ // });
const response = fetchToRequestResponse(options, res);
requestStream.emit('response', response);
}, err => {
@@ -1,30 +0,0 @@
diff --git a/node_modules/@google-cloud/storage/node_modules/retry-request/index.js b/node_modules/@google-cloud/storage/node_modules/retry-request/index.js
index 2fae107..5721c54 100644
--- a/node_modules/@google-cloud/storage/node_modules/retry-request/index.js
+++ b/node_modules/@google-cloud/storage/node_modules/retry-request/index.js
@@ -1,6 +1,6 @@
'use strict';
-const {PassThrough} = require('stream');
+const { PassThrough, pipeline } = require('stream');
const extend = require('extend');
let debug = () => {};
@@ -185,7 +185,7 @@ function retryRequest(requestOpts, opts, callback) {
.on('complete', (...params) => handleFinish(params))
.on('finish', (...params) => handleFinish(params));
- requestStream.pipe(delayStream);
+ pipeline(requestStream, delayStream, () => {});
} else {
activeRequest = opts.request(requestOpts, onResponse);
}
@@ -251,7 +251,7 @@ function retryRequest(requestOpts, opts, callback) {
// No more attempts need to be made, just continue on.
if (streamMode) {
retryStream.emit('response', response);
- delayStream.pipe(retryStream);
+ pipeline(delayStream, retryStream, () => {});
requestStream.on('error', err => {
retryStream.destroy(err);
});
@@ -1,58 +0,0 @@
diff --git a/node_modules/@google-cloud/storage/node_modules/teeny-request/build/src/index.js b/node_modules/@google-cloud/storage/node_modules/teeny-request/build/src/index.js
index af5d15e..2b63d0c 100644
--- a/node_modules/@google-cloud/storage/node_modules/teeny-request/build/src/index.js
+++ b/node_modules/@google-cloud/storage/node_modules/teeny-request/build/src/index.js
@@ -115,6 +115,9 @@ function createMultipartStream(boundary, multipart) {
}
else {
part.body.pipe(stream, { end: false });
+ part.body.on('error', (err) => {
+ stream.destroy(err);
+ });
part.body.on('end', () => {
stream.write('\r\n');
stream.write(finale);
@@ -168,25 +171,27 @@ function teenyRequest(reqOpts, callback) {
// Stream mode
const requestStream = streamEvents(new stream_1.PassThrough());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- let responseStream;
- requestStream.once('reading', () => {
- if (responseStream) {
- (0, stream_1.pipeline)(responseStream, requestStream, () => { });
- }
- else {
- requestStream.once('response', () => {
- (0, stream_1.pipeline)(responseStream, requestStream, () => { });
- });
- }
- });
+ // let responseStream;
+ // requestStream.once('reading', () => {
+ // if (responseStream) {
+ // (0, stream_1.pipeline)(responseStream, requestStream, () => { });
+ // }
+ // else {
+ // requestStream.once('response', () => {
+ // (0, stream_1.pipeline)(responseStream, requestStream, () => { });
+ // });
+ // }
+ // });
+
+
options.compress = false;
teenyRequest.stats.requestStarting();
(0, node_fetch_1.default)(uri, options).then(res => {
- teenyRequest.stats.requestFinished();
- responseStream = res.body;
- responseStream.on('error', (err) => {
- requestStream.emit('error', err);
- });
+ teenyRequest.stats.requestFinished(); stream_1.pipeline(res.body, requestStream, () => {});
+ // responseStream = res.body;
+ // responseStream.on('error', (err) => {
+ // requestStream.emit('error', err);
+ // });
const response = fetchToRequestResponse(options, res);
requestStream.emit('response', response);
}, err => {
-1
View File
@@ -1 +0,0 @@
The patches in this folder are applied by `patch-package` to dependencies, particularly those which need changes that are difficult to apply upstream.
-22
View File
@@ -1,22 +0,0 @@
diff --git a/node_modules/retry-request/index.js b/node_modules/retry-request/index.js
index 298a351..7ec1ef8 100644
--- a/node_modules/retry-request/index.js
+++ b/node_modules/retry-request/index.js
@@ -185,7 +185,7 @@ function retryRequest(requestOpts, opts, callback) {
.on('complete', (...params) => handleFinish(params))
.on('finish', (...params) => handleFinish(params));
- requestStream.pipe(delayStream);
+ pipeline(requestStream, delayStream, () => {});
} else {
activeRequest = opts.request(requestOpts, onResponse);
}
@@ -251,7 +251,7 @@ function retryRequest(requestOpts, opts, callback) {
// No more attempts need to be made, just continue on.
if (streamMode) {
retryStream.emit('response', response);
- delayStream.pipe(retryStream);
+ pipeline(delayStream, retryStream, () => {});
requestStream.on('error', err => {
retryStream.destroy(err);
});

Some files were not shown because too many files have changed in this diff Show More