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>
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>
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>
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>
- 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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
- 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>
'\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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
* 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
* [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
* [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
* [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
* 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
* 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
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>
- 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>
- 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>
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>
- 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>
- 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>
- 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>
- 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>
* 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
- 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>
- 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>
- 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>
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>
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>
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>
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>
#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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
* [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
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
* [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
* [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
* 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
* 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
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>
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>
- 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>
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>
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>
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>
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>
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>
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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
--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>
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>
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>
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>
- 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>
- 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>
<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 Quarto, LaTeX and Typst — documents and presentations.**
## Community Edition
Verso is a fork of [Overleaf](https://github.com/overleaf/overleaf) that adds
first-class [Quarto](https://quarto.org) and [Typst](https://typst.app) support
alongside Overleaf's LaTeX toolchain. It keeps Overleaf's real-time
collaboration infrastructure and runs **three compilers side by side**, chosen
automatically from the root file's extension:
[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.
| Root file | Compiler | Typical output |
|-----------|----------|----------------|
| `.qmd` | Quarto | PDF (via Typst or LaTeX), or an HTML/RevealJS deck |
| `.tex` | `latexmk` / TeX Live | PDF |
| `.typ` | Typst | PDF |
> [!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.
All three coexist on one server; no per-project configuration is required to
pick the engine.
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
## Features
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)
- **Real-time collaboration** — multiple people editing the same file at once,
powered by Overleaf's operational-transformation engine, with live cursors
and full project history.
- **Three compilers, auto-dispatched** — Quarto, LaTeX and Typst projects live
side by side; the runner is selected from the root file's extension.
- *Typst (`.typ`)* — syntax highlighting and completions for the common
functions and markup (`#import`, `#let`, `#set`, `#show`, `#figure`,
`#table`, `#cite`, …).
- **Document outline** — section headings are extracted into the sidebar
outline panel for LaTeX, Quarto (`#`, `##`, …) and Typst (`=`, `==`, …).
- **Format at a glance** — the project dashboard shows a per-project format
badge (Quarto / Typst / LaTeX), and the compiler dropdown greys out engines
that don't apply to the current root file.
- **Publish & share compiled output** — publish the compiled result as a
standalone page at `/p/:token`, with three independent access tiers (project
members / any logged-in user / public). Works for both HTML/RevealJS decks
(served live) and PDFs (embedded inline). HTML decks also get a one-click
**Present** button in the toolbar.
- **Quarto Python cells** — optional per-project virtual environment built from
the project's `requirements.txt`, so Python code chunks run during render
(gated to the project owner and invited collaborators).
- **Auto-compile** — the preview refreshes automatically shortly after you stop
typing.
## Keeping up to date
## Output formats
Sign up to the [mailing list](https://mailchi.mp/overleaf.com/community-edition-and-server-pro) to get updates on Overleaf releases and development.
In the YAML frontmatter of a `.qmd` file:
## Installation
```yaml
format: typst # → PDF preview, rendered via Typst (no LaTeX required)
format: pdf # → PDF preview, rendered via LaTeX
format: revealjs # → interactive HTML slideshow preview
format: html # → a static HTML page
```
We have detailed installation instructions in the [Overleaf Toolkit](https://github.com/overleaf/toolkit/).
Typst ships inside Quarto, so `format: typst` needs no separate installation.
## Upgrading
> **Note on display math**: keep `$$ … $$` blocks on a single line. Multi-line
> display-math blocks can trigger YAML parse errors in some Quarto versions.
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.
## Quick start
## Overleaf Docker Image
### With Docker
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.
```bash
docker run -d \
-p 80:80 \
-v ~/verso_data:/var/lib/overleaf \
--name verso \
registry.alocoq.fr/verso:latest
```
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.
Open `http://localhost` in your browser, then visit `/launchpad` on first run to
create the admin account.
The `sharelatex/sharelatex` image extends the base image and adds the actual Overleaf code
and services.
### Build from source
Use `make build-base` and `make build-community` from `server-ce/` to build these images.
```bash
# Build the base image (system deps + Quarto + TeX Live)
cd server-ce
make build-base
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.
# Build the application image
make build-community
```
| File | Purpose |
|------|---------|
| `server-ce/Dockerfile-base` | Base OS image — system deps, Quarto (with Typst) and a TeX Live (`latexmk`) toolchain |
| `server-ce/Dockerfile` | Application image — Node services and the compiled frontend |
## Architecture
Verso is a microservices monorepo (Yarn workspaces). All services run inside a
single container managed by `runit`, with `nginx` as the front router.
// 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).
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.