Fix heading bleeding and smooth_pdf_transition translation
Build and Deploy Verso / deploy (push) Successful in 13m35s

Two fixes:

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
claude
2026-06-08 14:22:16 +00:00
parent f1abcaa4ce
commit a5ca432396
5 changed files with 81 additions and 46 deletions
+19
View File
@@ -20,3 +20,22 @@ Ideas and features deferred from the current alpha.
language server) behind a WebSocket proxy. Would give Typst files
first-class autocomplete, hover docs, go-to-definition, and inline
error diagnostics — the main editing comfort gap vs. a native editor.
### Editor UX for non-LaTeX formats (.typ, .qmd, .md)
- **Visual/rich-text editing mode** — A toggle between raw source and a
rendered-in-place view for `.typ`, `.qmd`, and `.md` files (similar to
Overleaf's rich-text mode for LaTeX). Users who don't know Typst or
Markdown syntax should be able to edit content without seeing markup.
CodeMirror 6 already supports this pattern via a custom `NodeView` layer
or a separate Prosemirror bridge.
- **Toolbar / insertion shortcuts** — A formatting toolbar and keyboard
shortcuts for common operations, adapted per file type:
- **All formats**: bold, italic, underline, headings, bullet/numbered
lists, inline code, links.
- **Quarto / Markdown**: insert image, insert table, insert code block
with language tag.
- **Quarto RevealJS**: insert slide divider (`---`), insert speaker
notes (`::: notes`), insert columns layout, insert video embed
(using Quarto's `{{< video >}}` shortcode).
@@ -1883,6 +1883,7 @@
"size": "",
"skip": "",
"skip_welcome_animation": "",
"smooth_pdf_transition": "",
"some_dois_could_not_be_resolved": "",
"something_not_right": "",
"something_went_wrong": "",
@@ -5,8 +5,17 @@ import {
foldInside,
HighlightStyle,
syntaxHighlighting,
syntaxTree,
} from '@codemirror/language'
import { styleTags, tags as t } from '@lezer/highlight'
import { RangeSetBuilder } from '@codemirror/state'
import {
Decoration,
DecorationSet,
EditorView,
ViewPlugin,
ViewUpdate,
} from '@codemirror/view'
import { parser } from '../../lezer-typst/typst.mjs'
import { typstCompletions } from './complete'
import { typstDocumentOutline } from './document-outline'
@@ -30,8 +39,9 @@ export const TypstLanguage = LRLanguage.define({
CodeArgs: foldInside,
}),
styleTags({
// Headings (HeadingTitle is an external token, one terminal per line)
'HeadingMark HeadingTitle': t.heading,
// HeadingMark covers the "=+ " prefix; heading line content is handled
// by the headingLinePlugin ViewPlugin below (not a grammar token).
HeadingMark: t.heading,
// Comments
'LineComment LineCommentContent': t.comment,
@@ -100,10 +110,50 @@ const typstHighlightStyle = HighlightStyle.define([
{ tag: t.emphasis, fontStyle: 'italic' },
])
// Heading title decoration: the grammar only has a HeadingMark token (the "=+ "
// prefix). This plugin walks the syntax tree, finds each HeadingMark, and
// extends a heading-style decoration to the end of the line so the title text
// gets the same blue + bold treatment without needing a HeadingTitle token
// (which had LALR state-merging issues causing body text to bleed into heading
// style in the previous implementation).
const headingTitleMark = Decoration.mark({
class: 'tok-heading',
attributes: { style: 'font-weight:bold' },
})
function buildHeadingDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>()
syntaxTree(view.state).cursor().iterate(node => {
if (node.name === 'HeadingMark') {
const line = view.state.doc.lineAt(node.from)
if (node.to < line.to) {
builder.add(node.to, line.to, headingTitleMark)
}
}
})
return builder.finish()
}
const headingLinePlugin = ViewPlugin.fromClass(
class {
decorations: DecorationSet
constructor(view: EditorView) {
this.decorations = buildHeadingDecorations(view)
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = buildHeadingDecorations(update.view)
}
}
},
{ decorations: v => v.decorations }
)
export const typst = () => {
return new LanguageSupport(TypstLanguage, [
TypstLanguage.data.of({ autocomplete: typstCompletions }),
typstDocumentOutline,
syntaxHighlighting(typstHighlightStyle),
headingLinePlugin,
])
}
@@ -3,7 +3,6 @@
import { ExternalTokenizer } from '@lezer/lr'
import {
HeadingMark,
HeadingTitle,
RawBlockOpen,
RawBlockBody,
RawBlockClose,
@@ -51,35 +50,6 @@ export const headingTokenizer = new ExternalTokenizer(
{ contextual: false }
)
// ── headingTitleTokenizer ────────────────────────────────────────────────
// Emits HeadingTitle — everything from the current position to end-of-line.
// Using an external token (terminal) instead of grammar sub-items avoids
// LALR(1) conflicts: any token shared between headingTitleItem and document-
// level item causes the automaton to merge the two contexts.
//
// contextual: true — the tokenizer checks stack.canShift(HeadingTitle) before
// doing anything, so it ONLY fires in the LR state reached after HeadingMark.
// With contextual: false, Lezer calls the tokenizer speculatively at any
// position where HeadingTitle is *conceivably* valid (due to LALR state
// merging), which can cause whole lines of body text to be consumed as
// HeadingTitle tokens and styled as headings.
export const headingTitleTokenizer = new ExternalTokenizer(
(input, stack) => {
if (!stack.canShift(HeadingTitle)) return
let hasContent = false
while (input.next !== -1 && input.next !== NEWLINE) {
// Stop before a line comment (//) or block comment (/*) so that
// '= Heading // note' leaves the comment for the LineComment rule.
if (input.next === SLASH &&
(input.peek(1) === SLASH || input.peek(1) === STAR)) break
input.advance()
hasContent = true
}
if (hasContent) input.acceptToken(HeadingTitle)
},
{ contextual: true }
)
// ── rawTokenizer ────────────────────────────────────────────────────────
// Handles all three raw-block tokens (contextual: uses stack.canShift).
//
@@ -1,11 +1,13 @@
// typst.grammar — Lezer LR grammar for the Typst typesetting language.
// Covers markup mode (top-level), code mode (#expr) and math mode ($...$).
// External tokenizers handle constructs requiring context-sensitive lexing:
// headingTokenizer — start-of-line detection for heading markers
// headingTokenizer — start-of-line detection for heading markers (=+ )
// rawTokenizer — triple-backtick raw block open/body/close
// rawInlineTokenizer — single-backtick raw inline content
// codeBlockTokenizer — brace-depth tracking inside #{ ... }
// blockCommentTokenizer — depth-tracked nested /* ... */ comments
// Note: heading title text is NOT a grammar token. The ViewPlugin in
// languages/typst/index.ts decorates heading lines via the syntax tree.
@top Document { item* }
@@ -30,12 +32,9 @@ item {
// ── Headings ──────────────────────────────────────────────────────────────
// HeadingMark is produced by an external tokenizer that enforces the
// start-of-line constraint and captures the "=+" prefix + trailing space.
Heading { HeadingMark HeadingTitle? }
// HeadingTitle is an external token (terminal) that reads everything to EOL.
// Using an external token is the only reliable fix: any token that can start
// both a headingTitleItem and a document-level item causes an LALR(1) conflict
// after headingTitleItem+ because the automaton merges the two contexts.
// External tokens are context-isolated by the LR state machine — no merging.
// The heading title (text after the marker) is NOT a grammar token; a
// ViewPlugin in index.ts decorates the rest of the line via the syntax tree.
Heading { HeadingMark }
// ── Comments ──────────────────────────────────────────────────────────────
LineComment { "//" LineCommentContent? }
@@ -129,10 +128,6 @@ Escape { "\\" EscapeChar }
HeadingMark
}
@external tokens headingTitleTokenizer from "./tokens.mjs" {
HeadingTitle
}
@external tokens rawTokenizer from "./tokens.mjs" {
RawBlockOpen,
RawBlockBody,