After a successful compile, web service calls a new CLSI endpoint
(GET /project/:id/user/:uid/build/:bid/thumbnail) which runs pdftocairo
page-1 to a 190px-wide JPEG using the existing thumbnail preset. The
JPEG is stored in Redis (90-day TTL, overwritten on next compile) by
the new ThumbnailManager.
GET /project/:Project_id/thumbnail serves the cached JPEG to authenticated
users, returning 404 when no thumbnail exists. Project cards in the
Lumière grid show the image overlaying the coloured gradient tile; if
the image 404s (project never compiled or cache expired) the onerror
handler hides it and the gradient + initial letter shows through.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Trim SVG viewBox from 760 to 590 (content ends ~x=570; the blank
right whitespace was making the wordmark look left-biased). Remove the
scale(1.2) transform from the sidebar logo — the negative-margin
container already fills the sidebar width. Change login logo max-width
to be CSS-controlled only (removed inline 480px override).
Footer: switch to `background` shorthand !important so the dark-theme
`var(--footer-background)` shorthand can't compete; deepen the teal to
#c8e4de so the Lumière colour is clearly visible. Add a
`body:has(.login-page) .fat-footer` rule so the login-page footer gets
the same treatment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restructures ProjectCard so the card is a <div> container instead of <a>
(buttons cannot be nested inside anchor elements). A .lumiere-card-link
anchor wraps the thumb+body area; a .lumiere-card-actions strip sits below
it and fades in on hover.
Buttons added (reusing the same tooltip components as the classic table):
- Copy project (opens CloneProjectModal)
- Download project zip
- Compile & download PDF
- Archive project (skipped when already archived)
- Trash project (skipped when already trashed)
Action icons are coloured $lum-text-muted at rest and shift to $lum-teal
on hover, matching the Lumière palette.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Login: centre logo (display:block + margin:auto, max-width 300px) and
increase h1 from 1.4rem to 1.75rem
- Sidebar logo: switch from width:120%/margin-left:-10% to transform:scale(1.2)
so the image scales symmetrically from centre and isn't cut on the left
- Footer: use !important on background-color/color to beat the dark-theme
selector's higher specificity (:root [data-theme] = 0,4,0 vs our 0,3,0)
- Notifications: replace near-transparent rgba backgrounds with solid
opaque colours so the teal page gradient can't bleed through; make the
CTA button neutral slate-grey (not teal) with border-radius:8px
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Code/Visual editor toggle now uses a teal sliding indicator (::before
pseudo-element) that glides between tabs via translateX instead of the
plain background-color crossfade. Container and labels get border-radius:
10px/7px to match the rest of the Lumière toolbar's rounded-square style.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Logo (all themes): scale the Verso wordmark to 120% width, centered and
clipped to the sidebar column — the word mark visually fills the full
sidebar width. Uses overflow:hidden + width:120% !important + margin-left:-10%
to override the existing inline width style.
- Compile button (Lumière): replace the all-corners border-radius:7px on the
split button group with corner-specific rules — .compile-button gets 7px on
the left side only, .compile-dropdown-toggle gets 7px on the right side only,
so the shared inner edge stays flat as expected for a joined split button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Warning notification: switch from teal to amber (#b45309) so it reads as
a genuine warning and doesn't blend into the teal UI chrome
- Notification CTA button (.btn-secondary): style with teal tint so the
'Send confirmation code' button matches the Lumière theme
- Footer: override dark footer on .project-list-lumiere with a light teal
background (#edf7f5), dark text, teal section headings — selector has same
specificity as the default-theme dark rule but appears later in the cascade
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each project card now has a checkbox (top-left corner, semi-transparent by
default, fully opaque on hover). When any card is selected a selection bar
slides in above the grid showing: select-all checkbox, count, the existing
bulk-action toolbar (archive, trash, tags, delete), and a deselect-all button.
:has(input:checked) keeps all checkboxes visible once a selection is active.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Settings modal: teal accent stripe on header, teal gradient nav background,
teal active-tab highlight, teal section titles, teal focus rings on form
controls — scoped via :has(.ide-settings-modal-body) so other modals are
unaffected
- Login page: grainy teal gradient background, white rounded-square card with
teal/blue accent stripe, teal labels, focus rings, primary button — always
applied since users haven't set a theme yet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Override Bootstrap orange/yellow warning and generic blue info colors
with the Lumière teal palette. Warning banners now use a soft teal
tint instead of orange; info banners use the Lumière blue. Both types
get 10px border-radius and a subtle shadow to match the card style.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove font-size: 0.8rem from Admin navbar button (was shrinking text)
- Add border-radius: 7px to .toolbar-pdf .btn so the Recompile, Logs and
Download buttons in the PDF panel get the Lumière rounded-square shape
- Add border-radius to compile-button-group .btn to cover the dropdown
arrow toggle next to the Recompile button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- logged-in-items: pass showThemeToggle to AccountMenuItems so the theme
switcher is accessible from the top-right navbar (was lost when the
sidebar account icons were removed); AccountMenuItems already gates on
hasOverallThemes so it's a no-op on non-themed pages
- project-list-lumiere: restyle Account + Admin navbar buttons — rounded
square (8px) instead of pill, teal resting tint on Account, subtle
teal border on hover; matches Lumière design language
- ide-lumiere: extend rounded-square styling to all toolbar action buttons
(Share, Present, History, Layout, File/Edit/Help menu buttons) via
.ide-redesign-toolbar-actions and .ide-redesign-toolbar-menu selectors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces "Overleaf subscription"/"Overleaf Commons"/"Overleaf premium features"
with Verso equivalents in the institution subscription and commons upgrade
notification strings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- project-list-ds-nav.scss: remove display:none for .nav-item-account on
desktop — it was hidden because the sidebar handled it, but now the sidebar
no longer has the account icon so this made it invisible everywhere
- logged-in-items / nav-dropdown-menu: show User icon alongside 'Account'
text in navbar dropdown so it's recognisable as an account button
- Lumière: remove border-top from .ds-nav-verso-logo (was doubling up with
.ds-nav-sidebar-lower border)
- Logo hover: drop scale transform in both themes, use filter:brightness only
- Gradient: drop background-attachment:fixed (unreliable in scroll containers);
switch to circle gradients at 0.60/0.45 opacity; base colour #e8f5f2
- Editor ide-lumiere: rounded square (7px) on .ol-cm-toolbar-button with teal
hover/active states to match the Lumière design language
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- en.json: replace 'Overleaf' with 'Verso' in 6 user-visible strings
(email_already_registered, add_manager_user_not_found, compile_timeout,
download_metadata, to_confirm_email address, welcome_opening_workspace)
- groups-and-enterprise-banner: use dynamic appName instead of hard-coded
'Overleaf'
- SidebarLowerSection: add showAccountIcons prop (default true); set false
in project dashboard sidebar — account menu is already in the top-right
navbar, so the bottom-left duplicate is removed for all themes
- ds-nav-verso-logo: replace opacity-fade hover with scale+brightness
transform so logo is fully visible at rest
- Lumière: scope new-project-dropdown sidebar padding to avoid misaligning
the button when it appears next to the search bar in the header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove overflow:hidden from toolbar — it was clipping dropdown menus
- Increase SVG noise opacity 0.06→0.12 and gradient orb opacity for more
visible texture on the dashboard background
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Lumière container now carries project-ds-nav-page and
project-list-wrapper so the sidebar picks up all its existing styles.
The grey-rectangle button issue and broken sidebar layout were caused
by those expected parent classes being absent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bold (Ctrl+B) and italic (Ctrl+I) now unwrap when the cursor is already
inside a Strong/Emphasis node. Added #underline[…] and #smallcaps[…]
wrap commands (toolbar only) and #link("")[…] with Ctrl+K shortcut that
places the cursor in the URL field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New theme with gradient document cards, serif title typography and a
light airy palette. Set as the default for new users. Existing users
keep their current theme; all users can switch via the theme toggle
(new sparkle icon). Classic Dark / Classic Light are renamed accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OVERLEAF_LATEX_SHELL_ESCAPE=true was added to the prod workflow but
missed in the test workflow, so the svg package still failed on
test.alocoq.fr despite inkscape being installed in the image.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
inkscape's apt dependencies include python3-numpy, which pip can't
uninstall (no RECORD file). Moving inkscape to its own RUN layer after
the pip installs avoids the conflict: pip numpy lands in /usr/local/lib
first, then apt installs its numpy into /usr/lib alongside it, and
Python resolves /usr/local/lib first at import time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
inkscape pulls in python3-numpy 1.26.4 via apt; pip can't uninstall apt
packages (no RECORD file). --ignore-installed makes pip install its own
copy into /usr/local/lib without touching the apt version; /usr/local/lib
takes import precedence so runtime code gets the pip-managed numpy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The LaTeX svg package converts .svg files to PDF at compile time by
shelling out to Inkscape (requires --shell-escape). Without Inkscape in
the image and the flag enabled, compilation fails with "Did you run the
export with Inkscape?".
- Dockerfile-base: add inkscape to the apt install block
- settings.js: expose OVERLEAF_LATEX_SHELL_ESCAPE env var → clsi.latexShellEscape
- LatexRunner.js: pass -shell-escape to latexmk when the setting is on
- deploy-verso-prod.yml: set OVERLEAF_LATEX_SHELL_ESCAPE=true (trusted-user instance)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>