Add a Typst compiler alongside Quarto and LaTeX
Build and Deploy Verso / deploy (push) Successful in 7m37s
Build and Deploy Verso / deploy (push) Successful in 7m37s
A project whose root file is a .typ file now compiles straight to PDF with Typst, as a third engine beside Quarto (.qmd) and latexmk (.tex). Dispatch stays purely extension-based. CLSI: - New TypstRunner.js: runs `quarto typst compile <main>.typ output.pdf` (reuses the Typst bundled in Quarto, so no extra binary / Docker change). stderr is merged into output.log. - CompileManager: _isTypstFile + a TypstRunner branch in _getRunner, and TypstRunner added to the isRunning check and stopCompile kill list. - RequestParser: 'typst' added to VALID_COMPILERS. web: - settings.defaults: 'typ' added to validRootDocExtensions and the text extensions (so .typ opens in the editor); 'typst' added to safeCompilers. - output-files: the Quarto/Typst log parser (which already understands Typst `error:`/`warning:` + `┌─ file:line:col` diagnostics) now also handles .typ compiles, so their errors/warnings populate the log tabs. Polish: - New-project menu: "Blank Typst project" + "Example Typst project" in both the main and welcome dropdowns, backed by createBasicProject/createExampleProject flavour 'typst', a new mainbasic.typ template and an example-project-typst presentation (math, an image, a table, lists). - Compiler dropdown gains a "Typst" option (cosmetic; dispatch is by extension). README updated: three compilers side by side, with a Writing-a-Typst-document section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,14 +2,14 @@
|
||||
|
||||
**A collaborative real-time editor for Quarto and LaTeX presentations and documents.**
|
||||
|
||||
Verso is a fork of [Overleaf](https://github.com/overleaf/overleaf) that adds first-class [Quarto](https://quarto.org) support alongside Overleaf's existing LaTeX toolchain. It keeps Overleaf's real-time collaboration infrastructure and runs **two compilers side by side**: `.qmd` files are built with Quarto (PDF via Typst, or HTML via RevealJS), while `.tex` files still compile with `latexmk`/TeX Live. The engine is selected automatically from the root file's extension, so Quarto and LaTeX projects coexist.
|
||||
Verso is a fork of [Overleaf](https://github.com/overleaf/overleaf) that adds first-class [Quarto](https://quarto.org) support alongside Overleaf's existing 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: `.qmd` builds with Quarto (PDF via Typst, or HTML via RevealJS), `.tex` with `latexmk`/TeX Live, and `.typ` straight through [Typst](https://typst.app). All three coexist on one server.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Real-time collaboration** — multiple users editing the same `.qmd` file simultaneously, powered by Overleaf's operational-transformation engine
|
||||
- **Dual compiler** — `.qmd` files build with Quarto; `.tex` files build with `latexmk`/TeX Live. The runner is chosen by the root file's extension, so LaTeX and Quarto projects live side by side
|
||||
- **Three compilers** — `.qmd` builds with Quarto, `.tex` with `latexmk`/TeX Live, and `.typ` with Typst. The runner is chosen by the root file's extension, so Quarto, LaTeX and Typst projects live side by side
|
||||
- **Two output formats** — set `format: typst` in the YAML frontmatter for a PDF preview, or `format: revealjs` for an interactive HTML presentation rendered in an iframe
|
||||
- **Document outline** — headings (`#`, `##`, `###`) are extracted and shown in the sidebar outline panel
|
||||
- **Math, code, tables** — standard Quarto/Pandoc Markdown features all work
|
||||
@@ -70,7 +70,7 @@ browser ──→ nginx:80
|
||||
└── /project/*/output/* → clsi-nginx:8080 (compiled output files)
|
||||
|
||||
web → document-updater → Redis pub/sub → real-time → browser
|
||||
web → CLSI (quarto render / latexmk) → output files → nginx → browser
|
||||
web → CLSI (quarto render / latexmk / typst) → output files → nginx → browser
|
||||
```
|
||||
|
||||
Key services:
|
||||
@@ -80,7 +80,7 @@ Key services:
|
||||
| `web` | HTTP API, React frontend, auth, project management |
|
||||
| `real-time` | WebSocket layer, live cursor and edit sync |
|
||||
| `document-updater` | Operational transformation, Redis pub/sub |
|
||||
| `clsi` | Compiler — runs `quarto render` (`.qmd`) or `latexmk` (`.tex`) and serves output |
|
||||
| `clsi` | Compiler — runs `quarto render` (`.qmd`), `latexmk` (`.tex`) or `typst` (`.typ`) and serves output |
|
||||
| `docstore` | Document text storage (MongoDB) |
|
||||
| `filestore` | Binary file storage (S3 or local) |
|
||||
| `project-history` | Change history and version tracking |
|
||||
@@ -123,6 +123,14 @@ point; **Blank LaTeX project** gives you an empty `main.tex`.
|
||||
> extra packages may not build out of the box yet — see `server-ce/Dockerfile-base`
|
||||
> for how to switch to a fuller TeX Live scheme.
|
||||
|
||||
## Writing a Typst document
|
||||
|
||||
A project whose root file is a `.typ` file compiles directly to PDF with
|
||||
[Typst](https://typst.app) — fast, modern markup with a real scripting
|
||||
language. No extra install is needed: Verso drives the Typst that ships inside
|
||||
Quarto (`quarto typst compile`). Use the **Blank Typst project** or **Example
|
||||
Typst project** entries in the *New project* menu to get started.
|
||||
|
||||
## Environment variables
|
||||
|
||||
Verso inherits all of Overleaf's environment variables (prefixed `OVERLEAF_`).
|
||||
|
||||
@@ -7,6 +7,7 @@ import OError from '@overleaf/o-error'
|
||||
import ResourceWriter from './ResourceWriter.js'
|
||||
import QuartoRunner from './QuartoRunner.js'
|
||||
import LatexRunner from './LatexRunner.js'
|
||||
import TypstRunner from './TypstRunner.js'
|
||||
import OutputFileFinder from './OutputFileFinder.js'
|
||||
import OutputCacheManager from './OutputCacheManager.js'
|
||||
import ClsiMetrics from './Metrics.js'
|
||||
@@ -47,9 +48,22 @@ function _isQuartoFile(rootResourcePath) {
|
||||
return /\.(qmd|md|rmd)$/i.test(rootResourcePath || '')
|
||||
}
|
||||
|
||||
// A bare Typst source (.typ) compiles straight to PDF with the Typst that
|
||||
// ships inside Quarto (see TypstRunner), separate from the Quarto pipeline.
|
||||
function _isTypstFile(rootResourcePath) {
|
||||
return /\.typ$/i.test(rootResourcePath || '')
|
||||
}
|
||||
|
||||
// Return a runner with a uniform { run, isRunning, kill } interface so the
|
||||
// rest of CompileManager doesn't need to know which engine is in use.
|
||||
function _getRunner(rootResourcePath) {
|
||||
if (_isTypstFile(rootResourcePath)) {
|
||||
return {
|
||||
run: (name, opts) => TypstRunner.promises.runTypst(name, opts),
|
||||
isRunning: name => TypstRunner.isRunning(name),
|
||||
kill: name => TypstRunner.promises.killTypst(name),
|
||||
}
|
||||
}
|
||||
if (_isQuartoFile(rootResourcePath)) {
|
||||
return {
|
||||
run: (name, opts) => QuartoRunner.promises.runQuarto(name, opts),
|
||||
@@ -351,7 +365,9 @@ async function stopCompile(projectId, userId) {
|
||||
// stopCompile has no root path, so check both runners — only one can be
|
||||
// active for a given compileName at a time.
|
||||
const isRunning =
|
||||
QuartoRunner.isRunning(compileName) || LatexRunner.isRunning(compileName)
|
||||
QuartoRunner.isRunning(compileName) ||
|
||||
LatexRunner.isRunning(compileName) ||
|
||||
TypstRunner.isRunning(compileName)
|
||||
const lock = LockManager.getExistingLock(getCompileDir(projectId, userId))
|
||||
let lockReleased
|
||||
if (lock) {
|
||||
@@ -363,6 +379,7 @@ async function stopCompile(projectId, userId) {
|
||||
}
|
||||
await QuartoRunner.promises.killQuarto(compileName)
|
||||
await LatexRunner.promises.killLatex(compileName)
|
||||
await TypstRunner.promises.killTypst(compileName)
|
||||
await lockReleased
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,14 @@ import { promisify } from 'node:util'
|
||||
import settings from '@overleaf/settings'
|
||||
import OutputCacheManager from './OutputCacheManager.js'
|
||||
|
||||
const VALID_COMPILERS = ['quarto', 'pdflatex', 'latex', 'xelatex', 'lualatex']
|
||||
const VALID_COMPILERS = [
|
||||
'quarto',
|
||||
'typst',
|
||||
'pdflatex',
|
||||
'latex',
|
||||
'xelatex',
|
||||
'lualatex',
|
||||
]
|
||||
const MAX_TIMEOUT = 600
|
||||
const EDITOR_ID_REGEX = /^[a-f0-9-]{36}$/ // UUID
|
||||
const HISTORY_ID_REGEX = /^([0-9a-f]{24}|[1-9][0-9]{0,9})$/ // mongo id or postgres id
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import Path from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import logger from '@overleaf/logger'
|
||||
import CommandRunner from './CommandRunner.js'
|
||||
import fs from 'node:fs'
|
||||
|
||||
// Maps currently-running Typst jobs: compileName → PID (or docker container id)
|
||||
const ProcessTable = {}
|
||||
|
||||
// Compiles a standalone Typst document (.typ) straight to output.pdf. We reuse
|
||||
// the Typst that ships inside Quarto via `quarto typst compile`, so there is no
|
||||
// extra binary to install. This is deliberately the simplest of the three
|
||||
// runners: Typst only ever produces a PDF, so there is no format detection,
|
||||
// no HTML asset directory and no extension merging (cf. QuartoRunner).
|
||||
function runTypst(compileName, options, callback) {
|
||||
const { directory, mainFile, image, environment, compileGroup } = options
|
||||
const timeout = options.timeout || 60000
|
||||
|
||||
logger.debug(
|
||||
{ directory, timeout, mainFile, compileGroup },
|
||||
'starting typst compile'
|
||||
)
|
||||
|
||||
const command = _buildTypstCommand(mainFile)
|
||||
|
||||
ProcessTable[compileName] = CommandRunner.run(
|
||||
compileName,
|
||||
command,
|
||||
directory,
|
||||
image,
|
||||
timeout,
|
||||
environment || {},
|
||||
compileGroup,
|
||||
null,
|
||||
function (error, output) {
|
||||
delete ProcessTable[compileName]
|
||||
|
||||
// Propagate real process-level errors (killed, timed out) but NOT
|
||||
// ordinary non-zero exit codes from Typst itself. A compile failure
|
||||
// (exit code 1) is not a server error — the absence of output.pdf is
|
||||
// enough for CompileController to return 'failure'.
|
||||
if (error && (error.terminated || error.timedout)) {
|
||||
return callback(error)
|
||||
}
|
||||
|
||||
// On exit-code-1 errors LocalCommandRunner attaches stdout to the error
|
||||
// object; merge it so _writeLogOutput can persist it.
|
||||
const combined = output || (error ? { stdout: error.stdout || '' } : null)
|
||||
_writeLogOutput(compileName, directory, combined, () =>
|
||||
callback(null, combined)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function _buildTypstCommand(mainFile) {
|
||||
// Run through a POSIX shell so stderr (where Typst writes its diagnostics)
|
||||
// is merged into stdout (2>&1). LocalCommandRunner replaces $COMPILE_DIR
|
||||
// before the shell sees it; the output path is relative because the shell
|
||||
// CWD is already the compile directory.
|
||||
const inputPath = `$COMPILE_DIR/${mainFile}`
|
||||
const cmd = `quarto typst compile ${inputPath} output.pdf 2>&1`
|
||||
return ['/bin/sh', '-c', cmd]
|
||||
}
|
||||
|
||||
function _writeLogOutput(compileName, directory, output, callback) {
|
||||
const content = (output && output.stdout) || ''
|
||||
if (!content) return callback()
|
||||
// Write to output.log so the PDF-preview log panel picks it up. Typst's
|
||||
// `error:`/`warning:` + `┌─ file:line:col` diagnostics are understood by the
|
||||
// Quarto/Typst log parser on the web side.
|
||||
const logFile = Path.join(directory, 'output.log')
|
||||
fs.unlink(logFile, () => {
|
||||
fs.writeFile(logFile, content, { flag: 'wx' }, err => {
|
||||
if (err) {
|
||||
logger.error({ err, compileName, logFile }, 'error writing typst log')
|
||||
}
|
||||
callback()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function isRunning(compileName) {
|
||||
return ProcessTable[compileName] != null
|
||||
}
|
||||
|
||||
function killTypst(compileName, callback) {
|
||||
logger.debug({ compileName }, 'killing running typst compile')
|
||||
if (!isRunning(compileName)) {
|
||||
logger.warn({ compileName }, 'no such compile to kill')
|
||||
return callback(null)
|
||||
}
|
||||
CommandRunner.kill(ProcessTable[compileName], callback)
|
||||
}
|
||||
|
||||
export default {
|
||||
isRunning,
|
||||
runTypst,
|
||||
killTypst,
|
||||
promises: {
|
||||
runTypst: promisify(runTypst),
|
||||
killTypst: promisify(killTypst),
|
||||
},
|
||||
}
|
||||
@@ -344,6 +344,14 @@ const _ProjectController = {
|
||||
'quarto'
|
||||
)
|
||||
break
|
||||
case 'example_typst':
|
||||
project = await ProjectCreationHandler.promises.createExampleProject(
|
||||
userId,
|
||||
projectName,
|
||||
{},
|
||||
'typst'
|
||||
)
|
||||
break
|
||||
case 'blank_latex':
|
||||
project = await ProjectCreationHandler.promises.createBasicProject(
|
||||
userId,
|
||||
@@ -351,6 +359,13 @@ const _ProjectController = {
|
||||
'latex'
|
||||
)
|
||||
break
|
||||
case 'blank_typst':
|
||||
project = await ProjectCreationHandler.promises.createBasicProject(
|
||||
userId,
|
||||
projectName,
|
||||
'typst'
|
||||
)
|
||||
break
|
||||
case 'blank_quarto':
|
||||
case 'none':
|
||||
default:
|
||||
|
||||
@@ -88,13 +88,23 @@ async function createProjectFromSnippet(ownerId, projectName, docLines) {
|
||||
async function createBasicProject(ownerId, projectName, flavour = 'quarto') {
|
||||
const project = await _createBlankProject(ownerId, projectName)
|
||||
|
||||
// Verso compiles .qmd with Quarto and .tex with latexmk; the root file's
|
||||
// extension selects the runner (see CompileManager._isQuartoFile in CLSI),
|
||||
// Verso compiles .qmd with Quarto, .tex with latexmk and .typ with Typst;
|
||||
// the root file's extension selects the runner (see CompileManager in CLSI),
|
||||
// so a blank project's flavour is just a choice of template + root name.
|
||||
const { templateName, rootDocName } =
|
||||
flavour === 'latex'
|
||||
? { templateName: 'mainbasic.tex', rootDocName: 'main.tex' }
|
||||
: { templateName: 'mainbasic.qmd', rootDocName: 'main.qmd' }
|
||||
let templateName, rootDocName
|
||||
switch (flavour) {
|
||||
case 'latex':
|
||||
templateName = 'mainbasic.tex'
|
||||
rootDocName = 'main.tex'
|
||||
break
|
||||
case 'typst':
|
||||
templateName = 'mainbasic.typ'
|
||||
rootDocName = 'main.typ'
|
||||
break
|
||||
default:
|
||||
templateName = 'mainbasic.qmd'
|
||||
rootDocName = 'main.qmd'
|
||||
}
|
||||
|
||||
const docLines = await _buildTemplate(templateName, ownerId, projectName)
|
||||
await _createRootDoc(project, ownerId, docLines, rootDocName)
|
||||
@@ -179,12 +189,20 @@ async function createExampleProject(
|
||||
) {
|
||||
const project = await _createBlankProject(ownerId, projectName, attributes)
|
||||
|
||||
const { fileEntries, docEntries } =
|
||||
flavour === 'quarto'
|
||||
? await _addQuartoExampleProjectFiles(ownerId, projectName, project)
|
||||
: await _addExampleProjectFiles(ownerId, projectName, project)
|
||||
let result
|
||||
switch (flavour) {
|
||||
case 'quarto':
|
||||
result = await _addQuartoExampleProjectFiles(ownerId, projectName, project)
|
||||
break
|
||||
case 'typst':
|
||||
result = await _addTypstExampleProjectFiles(ownerId, projectName, project)
|
||||
break
|
||||
default:
|
||||
result = await _addExampleProjectFiles(ownerId, projectName, project)
|
||||
}
|
||||
const { fileEntries, docEntries } = result
|
||||
|
||||
if (flavour !== 'quarto') {
|
||||
if (flavour === 'latex') {
|
||||
// clsi-cache warming keys on a single static example; only do it for the
|
||||
// long-standing LaTeX example to avoid the "content is not static" guard.
|
||||
await populateClsiCacheForExampleProject(
|
||||
@@ -285,6 +303,40 @@ async function _addQuartoExampleProjectFiles(ownerId, projectName, project) {
|
||||
}
|
||||
}
|
||||
|
||||
async function _addTypstExampleProjectFiles(ownerId, projectName, project) {
|
||||
const mainDocLines = await _buildTemplate(
|
||||
'example-project-typst/main.typ',
|
||||
ownerId,
|
||||
projectName
|
||||
)
|
||||
const rootDoc = await _createRootDoc(
|
||||
project,
|
||||
ownerId,
|
||||
mainDocLines,
|
||||
'main.typ'
|
||||
)
|
||||
|
||||
const imagePath = path.join(
|
||||
import.meta.dirname,
|
||||
'/../../../templates/project_files/example-project-typst/frog.jpg'
|
||||
)
|
||||
const { fileRef } = await ProjectEntityUpdateHandler.promises.addFile(
|
||||
project._id,
|
||||
project.rootFolder[0]._id,
|
||||
'frog.jpg',
|
||||
imagePath,
|
||||
null,
|
||||
ownerId,
|
||||
null
|
||||
)
|
||||
return {
|
||||
fileEntries: [{ path: fileRef.name, file: fileRef }],
|
||||
docEntries: [
|
||||
{ path: 'main.typ', doc: rootDoc, docLines: mainDocLines.join('\n') },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
async function _createBlankProject(
|
||||
ownerId,
|
||||
projectName,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
@@ -0,0 +1,57 @@
|
||||
#set document(
|
||||
title: "<%= project_name %>",
|
||||
author: "<%= user.first_name %> <%= user.last_name %>",
|
||||
)
|
||||
#set page(numbering: "1")
|
||||
#set heading(numbering: "1.1")
|
||||
#set par(justify: true)
|
||||
|
||||
#align(center)[
|
||||
#text(size: 22pt, weight: "bold")[<%= project_name %>] \
|
||||
#v(0.4em)
|
||||
<%= user.first_name %> <%= user.last_name %> · <%= month %> <%= year %>
|
||||
]
|
||||
|
||||
= Introduction
|
||||
|
||||
Welcome to *Typst* in Verso. Typst is a modern typesetting system that
|
||||
compiles in milliseconds — edit `main.typ` and hit *Recompile* to see the PDF
|
||||
update. This example shows off math, figures, tables and lists.
|
||||
|
||||
= Mathematics
|
||||
|
||||
Inline math such as $e^(i pi) + 1 = 0$ sits naturally in the text, while block
|
||||
equations are centred and can be numbered:
|
||||
|
||||
$ integral_(-oo)^(oo) e^(-x^2) dif x = sqrt(pi) $
|
||||
|
||||
= Figures
|
||||
|
||||
Images stored in the project are included with the `image` function:
|
||||
|
||||
#figure(
|
||||
image("frog.jpg", width: 50%),
|
||||
caption: [A friendly frog, loaded from a project file.],
|
||||
)
|
||||
|
||||
= Tables
|
||||
|
||||
#figure(
|
||||
table(
|
||||
columns: 3,
|
||||
align: (left, center, left),
|
||||
[*Engine*], [*Output*], [*Best for*],
|
||||
[Typst], [PDF], [Fast, modern layout],
|
||||
[Quarto], [PDF / HTML], [Reproducible documents],
|
||||
[LaTeX], [PDF], [Classic STEM articles],
|
||||
),
|
||||
caption: [Verso compiles three source formats side by side.],
|
||||
)
|
||||
|
||||
= Lists
|
||||
|
||||
- Write documents in concise, readable markup
|
||||
- Get near-instant PDF output
|
||||
- Script your layout with a real programming language
|
||||
|
||||
Now make this document your own!
|
||||
@@ -0,0 +1,14 @@
|
||||
#set document(
|
||||
title: "<%= project_name %>",
|
||||
author: "<%= user.first_name %> <%= user.last_name %>",
|
||||
)
|
||||
#set page(numbering: "1")
|
||||
#set heading(numbering: "1.1")
|
||||
|
||||
#align(center)[
|
||||
#text(size: 20pt, weight: "bold")[<%= project_name %>] \
|
||||
#v(0.4em)
|
||||
<%= user.first_name %> <%= user.last_name %> · <%= month %> <%= year %>
|
||||
]
|
||||
|
||||
= Introduction
|
||||
@@ -55,6 +55,7 @@ const defaultTextExtensions = [
|
||||
'ldf',
|
||||
'rmd',
|
||||
'qmd',
|
||||
'typ',
|
||||
'lua',
|
||||
'py',
|
||||
'gv',
|
||||
@@ -115,7 +116,14 @@ const httpPermissionsPolicy = {
|
||||
},
|
||||
}
|
||||
|
||||
const safeCompilers = ['quarto', 'xelatex', 'pdflatex', 'latex', 'lualatex']
|
||||
const safeCompilers = [
|
||||
'quarto',
|
||||
'typst',
|
||||
'xelatex',
|
||||
'pdflatex',
|
||||
'latex',
|
||||
'lualatex',
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
env: 'server-ce',
|
||||
@@ -889,7 +897,7 @@ module.exports = {
|
||||
process.env.FILE_IGNORE_PATTERN ||
|
||||
'**/{{__MACOSX,.git,.texpadtmp,.R}{,/**},.!(latexmkrc),*.{dvi,aux,log,toc,out,pdfsync,synctex,synctex(busy),fdb_latexmk,fls,nlo,ind,glo,gls,glg,bbl,blg,doc,docx,gz,swp}}',
|
||||
|
||||
validRootDocExtensions: ['qmd', 'tex', 'Rtex', 'ltx', 'Rnw'],
|
||||
validRootDocExtensions: ['qmd', 'typ', 'tex', 'Rtex', 'ltx', 'Rnw'],
|
||||
|
||||
emailConfirmationDisabled:
|
||||
process.env.EMAIL_CONFIRMATION_DISABLED === 'true' || false,
|
||||
|
||||
@@ -130,11 +130,11 @@ export async function handleLogFiles(
|
||||
MAX_LOG_SIZE
|
||||
)
|
||||
try {
|
||||
// Quarto compiles (.qmd/.md/.Rmd, dispatched to QuartoRunner in CLSI by
|
||||
// root-file extension) produce Typst/Pandoc/Quarto diagnostics that the
|
||||
// LaTeX log parser does not understand. Route those to a dedicated parser
|
||||
// so their errors and warnings populate the log tabs like LaTeX ones.
|
||||
if (isQuartoCompile(data)) {
|
||||
// Quarto (.qmd/.md/.Rmd) and bare Typst (.typ) compiles produce
|
||||
// Typst/Pandoc/Quarto diagnostics that the LaTeX log parser does not
|
||||
// understand. Route those to a dedicated parser so their errors and
|
||||
// warnings populate the log tabs like LaTeX ones.
|
||||
if (usesQuartoLogParser(data)) {
|
||||
const { errors, warnings, typesetting } = parseQuartoLog(result.log)
|
||||
accumulateResults({ errors, warnings, typesetting })
|
||||
} else {
|
||||
@@ -301,12 +301,13 @@ function isTransientWarning(warning: LatexLogEntry): boolean {
|
||||
return TRANSIENT_WARNING_REGEX.test(warning.message || '')
|
||||
}
|
||||
|
||||
// Mirror of CompileManager._isQuartoFile in CLSI: the runner is chosen by the
|
||||
// root file's extension, so we detect Quarto compiles the same way client-side.
|
||||
const QUARTO_ROOT_REGEX = /\.(qmd|md|rmd)$/i
|
||||
// Mirrors CompileManager's runner dispatch in CLSI: both the Quarto runner
|
||||
// (.qmd/.md/.Rmd) and the Typst runner (.typ) emit Typst-style diagnostics, so
|
||||
// we pick the Quarto/Typst log parser for either, keyed on the root extension.
|
||||
const QUARTO_TYPST_ROOT_REGEX = /\.(qmd|md|rmd|typ)$/i
|
||||
|
||||
function isQuartoCompile(data: CompileResponseData): boolean {
|
||||
return QUARTO_ROOT_REGEX.test(data.options?.rootResourcePath || '')
|
||||
function usesQuartoLogParser(data: CompileResponseData): boolean {
|
||||
return QUARTO_TYPST_ROOT_REGEX.test(data.options?.rootResourcePath || '')
|
||||
}
|
||||
|
||||
async function fetchFileWithSizeLimit(
|
||||
|
||||
@@ -212,6 +212,18 @@ function NewProjectButton({
|
||||
{t('blank_latex_project')}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li role="none">
|
||||
<DropdownItem
|
||||
onClick={e =>
|
||||
handleModalMenuClick(e, {
|
||||
modalVariant: 'blank_typst',
|
||||
dropdownMenuEvent: 'blank-project-typst',
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('blank_typst_project')}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li role="none">
|
||||
<DropdownItem
|
||||
onClick={e =>
|
||||
@@ -236,6 +248,18 @@ function NewProjectButton({
|
||||
{t('example_latex_project')}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li role="none">
|
||||
<DropdownItem
|
||||
onClick={e =>
|
||||
handleModalMenuClick(e, {
|
||||
modalVariant: 'example_typst',
|
||||
dropdownMenuEvent: 'example-project-typst',
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('example_typst_project')}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li role="none">
|
||||
<DropdownItem
|
||||
onClick={e =>
|
||||
|
||||
+18
@@ -13,8 +13,10 @@ const ImportDocumentModal = lazy(() => import('./import-document-modal'))
|
||||
export type NewProjectButtonModalVariant =
|
||||
| 'blank_quarto'
|
||||
| 'blank_latex'
|
||||
| 'blank_typst'
|
||||
| 'example_quarto'
|
||||
| 'example_latex'
|
||||
| 'example_typst'
|
||||
| 'upload_project'
|
||||
| 'import_from_github'
|
||||
| 'import_docx'
|
||||
@@ -68,6 +70,14 @@ function NewProjectButtonModal({
|
||||
template="blank_latex"
|
||||
/>
|
||||
)
|
||||
case 'blank_typst':
|
||||
return (
|
||||
<BlankProjectModal
|
||||
onHide={onHide}
|
||||
initialTags={initialTags}
|
||||
template="blank_typst"
|
||||
/>
|
||||
)
|
||||
case 'example_quarto':
|
||||
return (
|
||||
<ExampleProjectModal
|
||||
@@ -84,6 +94,14 @@ function NewProjectButtonModal({
|
||||
template="example_latex"
|
||||
/>
|
||||
)
|
||||
case 'example_typst':
|
||||
return (
|
||||
<ExampleProjectModal
|
||||
onHide={onHide}
|
||||
initialTags={initialTags}
|
||||
template="example_typst"
|
||||
/>
|
||||
)
|
||||
case 'upload_project':
|
||||
return (
|
||||
<Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
|
||||
|
||||
+26
@@ -135,6 +135,17 @@ function WelcomeMessageCreateNewProjectDropdown({
|
||||
{t('blank_latex_project')}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li role="none">
|
||||
<DropdownItem
|
||||
as="button"
|
||||
onClick={e =>
|
||||
handleDropdownItemClick(e, 'blank_typst', 'blank-project-typst')
|
||||
}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{t('blank_typst_project')}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li role="none">
|
||||
<DropdownItem
|
||||
as="button"
|
||||
@@ -165,6 +176,21 @@ function WelcomeMessageCreateNewProjectDropdown({
|
||||
{t('example_latex_project')}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li role="none">
|
||||
<DropdownItem
|
||||
as="button"
|
||||
onClick={e =>
|
||||
handleDropdownItemClick(
|
||||
e,
|
||||
'example_typst',
|
||||
'example-project-typst'
|
||||
)
|
||||
}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{t('example_typst_project')}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li role="none">
|
||||
<DropdownItem
|
||||
as="button"
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ function getCompilerOptions(): Option<ProjectCompiler>[] {
|
||||
// LaTeX users pick which TeX engine latexmk should use.
|
||||
return [
|
||||
{ value: 'quarto', label: 'Quarto' },
|
||||
{ value: 'typst', label: 'Typst' },
|
||||
{ value: 'pdflatex', label: 'pdfLaTeX' },
|
||||
{ value: 'latex', label: 'LaTeX' },
|
||||
{ value: 'xelatex', label: 'XeLaTeX' },
|
||||
|
||||
@@ -295,6 +295,7 @@
|
||||
"blank_latex_project": "Blank LaTeX project",
|
||||
"blank_project": "Blank project",
|
||||
"blank_quarto_project": "Blank Quarto project",
|
||||
"blank_typst_project": "Blank Typst project",
|
||||
"blocked_filename": "This file name is blocked.",
|
||||
"blog": "Blog",
|
||||
"bold": "Bold",
|
||||
@@ -842,6 +843,7 @@
|
||||
"example_latex_project": "Example LaTeX project",
|
||||
"example_project": "Example project",
|
||||
"example_quarto_project": "Example Quarto project",
|
||||
"example_typst_project": "Example Typst project",
|
||||
"examples": "Examples",
|
||||
"examples_lowercase": "examples",
|
||||
"examples_to_help_you_learn": "Examples to help you learn how to use powerful LaTeX packages and techniques.",
|
||||
|
||||
@@ -17,7 +17,13 @@ export type MainDocument = {
|
||||
path: string
|
||||
}
|
||||
|
||||
export type ProjectCompiler = 'quarto' | 'pdflatex' | 'latex' | 'xelatex' | 'lualatex'
|
||||
export type ProjectCompiler =
|
||||
| 'quarto'
|
||||
| 'typst'
|
||||
| 'pdflatex'
|
||||
| 'latex'
|
||||
| 'xelatex'
|
||||
| 'lualatex'
|
||||
|
||||
export type OverallThemeMeta = {
|
||||
name: string
|
||||
|
||||
Reference in New Issue
Block a user