collective

Browse / github.com/storytold/vectorcraft / patch

Type › Typst…: typeset Typst markup (math for scientific figures) as editable vector art

Type › Typst…: typeset Typst markup (math for scientific figures) as editable vector art ## What and why Adds **Type › Typst…**, which typesets [Typst](https://typst.app) markup and places it in the document as vector art you can edit again later. Its main use is scientific figures: equations, symbols and short typeset labels in diagrams, posters and slides that should match the paper they belong to. Researchers who draw figures in Inkscape rely on [TexText](https://github.com/textext/textext) for this (it supports both LaTeX and Typst). Typst is spreading quickly in academia because it typesets in milliseconds instead of seconds, which makes edit-and-see in a drawing app pr

Status
open
Score
0.00
Adopters
0 other operators
Author
svandenhaute (imported from pull request #3168, open)
Base
a4f5ae3b1620ecbfe6db95c6f2a0243402ec37b3
Size
+1732 −1 in 37 files
Review
pending
Shared
2026-10-28
Id
d7133b94be2a0cdd0680d477

How it went

No other operator has reported using it yet.

Diff

raw · ROADMAP.md crates/doc/src/lib.rs crates/doc/src/node.rs crates/doc/src/typst.rs crates/engine/src/cmd/brushsym.rs crates/engine/src/cmd/mod.rs crates/engine/src/cmd/prefscmds.rs crates/engine/src/cmd/typst.rs crates/engine/src/lib.rs crates/engine/src/tests_typst.rs crates/ui-egui/src/dialogs/mod.rs crates/ui-egui/src/dialogs/typst.rs crates/ui-egui/src/i18n/bg.tsv crates/ui-egui/src/i18n/cs.tsv crates/ui-egui/src/i18n/da.tsv crates/ui-egui/src/i18n/de.tsv crates/ui-egui/src/i18n/el.tsv crates/ui-egui/src/i18n/es.tsv crates/ui-egui/src/i18n/fi.tsv crates/ui-egui/src/i18n/fr.tsv crates/ui-egui/src/i18n/hu.tsv crates/ui-egui/src/i18n/id.tsv crates/ui-egui/src/i18n/it.tsv crates/ui-egui/src/i18n/ja.tsv crates/ui-egui/src/i18n/ka.tsv crates/ui-egui/src/i18n/ko.tsv crates/ui-egui/src/i18n/nl.tsv crates/ui-egui/src/i18n/pl.tsv crates/ui-egui/src/i18n/pt-br.tsv crates/ui-egui/src/i18n/ru.tsv crates/ui-egui/src/i18n/sv.tsv crates/ui-egui/src/i18n/uk.tsv crates/ui-egui/src/i18n/vi.tsv crates/ui-egui/src/i18n/zh-hans.tsv crates/ui-egui/src/i18n/zh-hant.tsv crates/ui-egui/src/menus.rs docs/typst.md

diff --git a/ROADMAP.md b/ROADMAP.md--- a/ROADMAP.md+++ b/ROADMAP.md@@ -62,6 +62,7 @@ everything is measured from our code against Illustrator's public documentation. | Ecosystem and plug-ins | 15% | 30–60 | [gaps.md › G17](docs/gaps.md#g17-ecosystem-third-party-plug-ins) | | AI features (generative) | 0% | 30–60 + owner decision | [gaps.md › G18](docs/gaps.md#g18-generative-ai-features) | | Agent automation | beyond Illustrator | — | [mcp.md](docs/mcp.md) |+| Typst typesetting | beyond Illustrator | — | [typst.md](docs/typst.md) |  The dimension hours overlap (feature rows hold some UI, format and AI work), so they don't add up to the total. @@ -138,6 +139,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap |---|---| | 2026-10-21 | 3D lighting: four original lighting presets shared by Revolve, Extrude & Bevel, Inflate and Rotate, with command access and manual Custom settings; scores and estimates unchanged. | | 2026-10-21 | Print: the Print dialog's Media list has the Japanese papers — JIS B4 (257 × 364 mm), JIS B5 (182 × 257 mm) and JIS B6 next to the ISO B4/B5 (which keep their ISO 216 dimensions for settings saved before them), はがき 100 × 148 mm, 往復はがき 148 × 200 mm, 封筒 長形3号 120 × 235 mm / 長形4号 90 × 225 mm / 角形2号 240 × 332 mm / 角形3号 216 × 277 mm and 四六判 127 × 188 mm. A paper is named by its JSON id or its dialog label, any case and spacing (`B5 (JIS)` or `b5jis`), and every added size prints as its own PDF page size. Headline numbers unchanged. |+| 2026-10-19 | Type › Typst…: Typst markup (math, text, tables) typeset by the installed `typst` program and placed as a group of paths that keeps its source; select it and choose Typst… again to edit it in place, keeping its centre, scale and rotation. Desktop builds; commands `type.typst.insert`, `type.typst.edit`, `type.typst.info` ([typst.md](docs/typst.md)). | | 2026-10-18 | Inch rulers divide the inch in half, not in ten. Zooming in stepped an inch ruler through 0.2, 0.1 and 0.05 of an inch, marks an inch ruler does not carry; below an inch the steps are binary fractions now, 1/2 down to 1/16, with the unlabelled ticks between them binary too (down to 1/128, so the 32nds, 64ths and 128ths appear as the report expects), while the metric units, points, pixels and picas keep their tenths. A label carries the decimals its step needs, so a sixteenth reads 0.0625 rather than the rounded 0.06, with trailing zeros dropped (#2827). Headline numbers unchanged. | | 2026-10-18 | Exported and saved files can repeat their bytes: PNG, PDF, EPS, legacy `.ai`, packaged zips and `.vectorcraft` saves recorded the wall-clock time they were made in, so the same commands run twice gave different bytes though the pixels matched. `SOURCE_DATE_EPOCH` now stands in for the clock in everything a file records, as the reproducible-builds specification defines it; a date a command is given still wins, and a malformed value is ignored rather than refusing the export. The recovery heartbeat and the seed PDF encryption draws on keep the real clock, so a password-protected PDF still differs between runs (#2880). Headline numbers unchanged. | | 2026-10-18 | About 85 community PRs landed in two batches through the full gate, with our fixes on top. New: Word `.docx` files place as area type with their formatting (#2766), CCITT Group 3 TIFF files open (#2770), Paragraph panel menu › Roman Hanging Punctuation (#2775), the Anchor Point tool edits blend spine points and handles (#2643, fixes #2544), shadows and glows each draw from the art with the effects before them, without the old exponential cost (#2395), one to ten coloured lights in 3D effects (#2706), Artboard Options X and Y with a reference point (#2739), more Contextual Task Bar actions (#2471), Optical Margin Alignment for a whole thread (#2773), History panel Step Backward/Forward and Set History Limit (#2764), Edit › Undo and Redo name the step as the History panel does (#2774), bounding-box handles snap to the grid, pixels and guides (#2715), Ctrl/Cmd+A in the Artboards panel (#2761), Tabs panel menu items (#2742), Import Data Sets defines variables from header cells (#2736), a Linux option to use X11 when a pen is attached (#2717). Fidelity and robustness: variable fonts named by their default weight and fallback glyphs keep bold and italic (#2733, #2732), legacy `.ai` layer names in Windows Latin-1 (#2722), WMF window mapping (#2721), Magic Wand inside isolation (#2720), type values out of range in files are clamped (#2728, #2757), raster effects stay responsive in drags (#2745), exports and Save for Web report undecodable images (#2762, #2737, #2684), Windows-safe exported file names (#2738), a marquee skips a Shaper Group's originals (#2772). Second batch: an export progress dialog with Cancel on the shared cancel token (#2564), CMYK gradient meshes drawn several times faster within a 16 MB cap (#2802), Paragraph panel menu › Justification… with file values clamped (#2771), Edit › Clear History (#2843), SVG image links keep their spelling (#2837, fixes main's Windows test failures), the Linux save dialog through the XDG portal (#2783), RGB input stored as CMYK in CMYK documents (#2812), legacy `.ai` export of images and radial highlights (#2801, #2807), `.ai` import notes for damaged editing data, page sizes and variable libraries (#2799, #2800, #2842), PDF and `.ai` keep their creation date (#2838), undecodable images left out of PDF exports with a note (#2790), MCP input waits for queue room (#2776), slice, snapping, selection, type and accessibility fixes (#2794–#2797, #2805, #2806, #2809, #2815, #2844–#2846), dropdowns close after a keyboard pick (#2824), plug-in dialogs scroll (#2840). Languages: the Dutch catalog completed (#2730). Headline numbers unchanged (estimated). |@@ -191,6 +193,7 @@ The full pre-standard record of what landed is kept in [docs/roadmap.md](docs/ro |---|---|---| | 2026-10-21 | minor | 3D: original Standard, Diffuse, Top Left and Right lighting presets; command access, effective-setting recognition and preview/history; scores and estimates unchanged | | 2026-10-21 | minor | Print: Japanese paper sizes in the Print dialog's Media list (JIS B4/B5/B6, はがき, 往復はがき, 封筒 長形3号/長形4号/角形2号/角形3号, 四六判); ISO B4/B5 keep their sizes, and a paper is named by id or label, any case and spacing. Headline numbers unchanged. |+| 2026-10-19 | minor | Typst typesetting (Type › Typst…): a beyond-Illustrator dimension row and a progress log row; parity scores unchanged. | | 2026-10-17 | minor | Colored Inflate: shared membrane normals and smooth RGB vector ramps replace flat polygon bands; channel groups are batched per solid-fill surface. White-light Inflate retains its original polygons, paint composition and seam joins; the 0.05 seam minimum is restored. Scores and hour estimates unchanged. | | 2026-10-16 | minor | 3D lighting: one to ten directional lights, a selectable light list, Add Light and Delete Light, independent settings and artboard gestures; ambient applied once and smooth RGB vector export; scores and hour estimates unchanged | | 2026-10-16 | minor | 3D lighting: opaque RGB Color for the shared directional light, with neutral ambient, preview/reset controls and smooth vector Revolve shading; scores and hour estimates unchanged |diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs--- a/crates/doc/src/lib.rs+++ b/crates/doc/src/lib.rs@@ -60,6 +60,7 @@ mod symbols; pub use symbols::SymbolBoxes; pub mod text; pub mod trace;+pub mod typst; pub mod variables; pub mod xml_id; diff --git a/crates/doc/src/node.rs b/crates/doc/src/node.rs--- a/crates/doc/src/node.rs+++ b/crates/doc/src/node.rs@@ -615,6 +615,8 @@ pub struct NodeExtras {     /// Object › Objects on Path: the group's path and the objects attached to it (see     /// [`crate::objects_on_path`]).     pub objects_on_path: Option<Box<crate::objects_on_path::ObjectsOnPath>>,+    /// Typst object: the group's art was typeset from this source (Type › Typst…).+    pub typst: Option<Box<crate::typst::TypstSpec>>, }  /// No extras: what [`Node::extras`] gives for a node without any.@@ -634,6 +636,7 @@ static NO_EXTRAS: NodeExtras = NodeExtras {     intertwine: None,     live_paint_gaps: None,     objects_on_path: None,+    typst: None, };  /// A node's [`NodeExtras`], if it has any ([`Node::extras`]). Extras that are all unset equal@@ -769,6 +772,7 @@ impl Serialize for Node {             e.intertwine.is_none(),             e.live_paint_gaps.is_none(),             e.objects_on_path.is_none(),+            e.typst.is_none(),         ];         let mut st = s.serialize_struct("Node", skips.iter().filter(|s| !**s).count())?;         let mut skip = skips.into_iter();@@ -808,6 +812,7 @@ impl Serialize for Node {         field!("intertwine", &e.intertwine);         field!("live_paint_gaps", &e.live_paint_gaps);         field!("objects_on_path", &e.objects_on_path);+        field!("typst", &e.typst);         st.end()     } }@@ -869,6 +874,8 @@ struct NodeWire {     live_paint_gaps: Option<Box<crate::live_paint::GapOptions>>,     #[serde(default)]     objects_on_path: Option<Box<crate::objects_on_path::ObjectsOnPath>>,+    #[serde(default)]+    typst: Option<Box<crate::typst::TypstSpec>>, }  impl From<NodeWire> for Node {@@ -889,6 +896,7 @@ impl From<NodeWire> for Node {             intertwine: w.intertwine,             live_paint_gaps: w.live_paint_gaps,             objects_on_path: w.objects_on_path,+            typst: w.typst,         };         Node {             id: w.id,@@ -1075,6 +1083,7 @@ impl Node {             NodeKind::Layer { .. } => "Layer",             NodeKind::Group { clip: true, .. } => "Clip Group",             NodeKind::Group { .. } if self.extras().objects_on_path.is_some() => "Objects on Path",+            NodeKind::Group { .. } if self.extras().typst.is_some() => "Typst",             NodeKind::Group { .. } => "Group",             NodeKind::Path { live: Some(l), .. } => l.label(),             NodeKind::Path { guide: true, .. } => "Guide",@@ -1332,6 +1341,12 @@ impl Node {         {             on_path.transform(a);         }+        // A Typst object's page moves with its art, so typesetting it again keeps its place.+        if let Some(e) = self.extras_if_any()+            && let Some(typst) = &mut e.typst+        {+            typst.transform(a);+        }         if !self.is_layer() {             self.bbox_angle = crate::orient::transformed_angle(self.bbox_angle, a);         }diff --git a/crates/doc/src/typst.rs b/crates/doc/src/typst.rsnew file mode 100644--- /dev/null+++ b/crates/doc/src/typst.rs@@ -0,0 +1,34 @@+//! Typst objects (Type › Typst…): a group whose art was typeset by [Typst](https://typst.app) from+//! the [`TypstSpec`] it keeps, so it can be edited and typeset again.++use serde::{Deserialize, Serialize};+use vectorcraft_geom::{Affine, Rect};++/// The longest source or preamble typeset (bytes). A file's longer one is kept as it is, but+/// editing refuses to typeset it.+pub const MAX_TYPST_SOURCE: usize = 1 << 20;++/// What a Typst object was typeset from, and where its art went.+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]+#[serde(rename_all = "camelCase")]+pub struct TypstSpec {+    /// The Typst markup typeset (`$ … $` for math).+    pub source: String,+    /// Markup read before the source: `#set` and `#import` rules, `#let` definitions.+    #[serde(default, skip_serializing_if = "String::is_empty")]+    pub preamble: String,+    /// The text size the source starts with (pt).+    pub font_size: f64,+    /// The typeset page: its art's own coordinates, before [`TypstSpec::xf`].+    pub page: Rect,+    /// Where the page is in the document: follows the object as it is moved, scaled or rotated, so+    /// typesetting it again keeps its place.+    pub xf: Affine,+}++impl TypstSpec {+    /// The object moved by `a`.+    pub fn transform(&mut self, a: Affine) {+        self.xf = a * self.xf;+    }+}diff --git a/crates/engine/src/cmd/brushsym.rs b/crates/engine/src/cmd/brushsym.rs--- a/crates/engine/src/cmd/brushsym.rs+++ b/crates/engine/src/cmd/brushsym.rs@@ -1006,7 +1006,7 @@ fn expanded_instance(d: &mut Document, inst: &Node) -> Option<Node> {     Some(art) } -fn break_links(d: &mut Document, ids: &[NodeId]) -> Result<Vec<NodeId>> {+pub(crate) fn break_links(d: &mut Document, ids: &[NodeId]) -> Result<Vec<NodeId>> {     let mut out = vec![];     for id in ids {         let Some(inst) = d.node(*id).cloned() else { continue };diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs--- a/crates/engine/src/cmd/mod.rs+++ b/crates/engine/src/cmd/mod.rs@@ -86,6 +86,7 @@ pub(crate) mod textwrap; pub(crate) mod threads; pub(crate) mod typecmd; mod typemenu;+pub mod typst; mod variablecmds; pub(crate) mod views; pub mod wand;@@ -299,6 +300,7 @@ pub fn command_specs() -> &'static [CommandSpec] {         v.extend(perspgrid::specs());         v.extend(shortcutset::specs());         v.extend(importmatches::specs());+        v.extend(typst::specs());         v     }) }diff --git a/crates/engine/src/cmd/prefscmds.rs b/crates/engine/src/cmd/prefscmds.rs--- a/crates/engine/src/cmd/prefscmds.rs+++ b/crates/engine/src/cmd/prefscmds.rs@@ -195,6 +195,7 @@ pub const PREF_SPECS: &[PrefSpec] = &[     p!("highlightAlternateGlyphs", "Type", "Options", "Highlight Alternate Glyphs", bool),     p!("placeholderText", "Type", "Options", "Fill New Type Objects With Placeholder Text", bool),     p!("fontsFolder", "Type", "Options", "Additional Fonts Folder", text),+    p!("enableTypst", "Type", "Options", "Enable Typst", bool),     // Units     p!("unitsGeneral", "Units", "", "General", choice(UNITS)),     p!("unitsStroke", "Units", "", "Stroke", choice(UNITS)),@@ -499,6 +500,17 @@ impl Prefs {         refused     } +    /// Keep Enable Typst off in `next` while automation is confined ([`crate::file_access`]): it+    /// lets markup run the `typst` program, which reads files and downloads packages on its own.+    /// → why it was refused.+    fn keep_typst_off(&self, next: &mut Prefs) -> Option<String> {+        if crate::file_access::current().is_none() || self.enable_typst || !next.enable_typst {+            return None;+        }+        next.enable_typst = false;+        Some("automation can't turn on Enable Typst: turn it on in Preferences › Type".into())+    }+     /// Values of one category (`None` = all) reset to defaults.     pub fn reset(&mut self, category: Option<&str>) {         let d = Prefs::default().to_json();@@ -531,6 +543,9 @@ impl Session {         for e in self.prefs.keep_folders(&mut p) {             log::warn!("{e}");         }+        if let Some(e) = self.prefs.keep_typst_off(&mut p) {+            log::warn!("{e}");+        }         let grid_changed = p.gridline_every != self.prefs.gridline_every || p.grid_subdivisions != self.prefs.grid_subdivisions;         let history_changed = p.history_states != self.prefs.history_states;         let tile_edge_changed = p.pattern_tile_edge_color != self.prefs.pattern_tile_edge_color;@@ -615,6 +630,9 @@ fn set(s: &mut Session, p: &Value) -> Result<Value> {     if let Some(e) = s.prefs.keep_folders(&mut next).into_iter().next() {         return Err(EngineError::Other(e));     }+    if let Some(e) = s.prefs.keep_typst_off(&mut next) {+        return Err(EngineError::Other(e));+    }     s.apply_prefs(next);     // Units ▸ General is also the open document's units (as Document Setup sets them).     if values.contains_key("unitsGeneral") && s.active().is_some() {diff --git a/crates/engine/src/cmd/typst.rs b/crates/engine/src/cmd/typst.rsnew file mode 100644--- /dev/null+++ b/crates/engine/src/cmd/typst.rs@@ -0,0 +1,528 @@+//! Type › Typst…: Typst markup typeset by the `typst` program (desktop builds) and placed as one+//! group of paths that keeps its source ([`TypstSpec`]), to edit and typeset again in place.+//!+//! The program is `typst` on the `PATH`, or the one the `VECTORCRAFT_TYPST` environment variable+//! names. It is never taken from command params: the control channel and MCP reach these commands,+//! and must not choose what runs. It reads the markup from standard input and writes the first+//! page as SVG ([`vectorcraft_svg::import`] reads it). Its glyphs, which the SVG draws as `<use>`+//! references, come in as plain paths rather than as symbols. The project root (what `#image` and+//! `#read` can see) is the document's folder, or an empty folder for an unsaved document.+//!+//! Nothing runs until Preferences › Type › Enable Typst is on (`enableTypst`, off by default):+//! Typst downloads the `@preview` packages markup imports, a network fetch the app doesn't make+//! or check itself.+//!+//! The page is as big as its contents (no margins, no fill). An edit typesets the source again and+//! puts the new page's centre where the old one's is, with the object's scale and rotation+//! ([`TypstSpec::xf`]).++use std::sync::Arc;++use serde_json::{Value, json};+use vectorcraft_doc::typst::{MAX_TYPST_SOURCE, TypstSpec};+use vectorcraft_doc::{Document, Node, NodeId, NodeKind};+use vectorcraft_geom::{Affine, Point, Rect};++use super::*;+use crate::MAX_COORD;++const INSERT: &str = "type.typst.insert";+const EDIT: &str = "type.typst.edit";+const INFO: &str = "type.typst.info";++/// The text size the source starts with when none is given (pt), Typst's own.+pub const DEFAULT_FONT_SIZE: f64 = 11.0;+/// The environment variable naming the `typst` program.+pub const PROGRAM_VAR: &str = "VECTORCRAFT_TYPST";++pub fn specs() -> Vec<CommandSpec> {+    vec![+        cmd!(+            "type.typst.insert",+            "Insert Typst",+            [],+            None,+            "{source: string (Typst markup; `$ … $` for math), preamble?: string (markup read first: #set, #import, #let), fontSize?: pt (default 11), at?: [x, y] (centre; default the first artboard's)} typeset with the `typst` program (desktop builds, once prefs enableTypst is on; `typst` on the PATH or $VECTORCRAFT_TYPST) and place it as one selected group of paths that keeps its source → {id, width, height}",+            has_doc,+            insert+        ),+        cmd!(+            "type.typst.edit",+            "Edit Typst",+            [],+            None,+            "{id?: the Typst object (default: the selected one), source?, preamble?, fontSize?} typeset a Typst object again (what is not given stays as it was), keeping its centre, scale and rotation → {id, width, height}",+            has_typst,+            edit+        ),+        cmd!(+            query "type.typst.info",+            "Typst Info",+            [],+            None,+            "{id?: the Typst object (default: the selected one)} → {id, source, preamble, fontSize}",+            has_typst,+            info+        ),+    ]+}++fn has_typst(s: &Session) -> std::result::Result<(), String> {+    target(s, &Value::Null, INFO).map(|_| ()).map_err(|e| e.to_string())+}++/// The Typst objects the selection is in: each selected object, or the nearest group around it+/// that is a Typst object (Type › Typst… edits when there is one, inserts when there is none).+pub fn selected(s: &Session) -> Result<Vec<NodeId>> {+    let st = s.doc()?;+    let d = &st.doc;+    let typst = |id: NodeId| d.node(id).is_some_and(|n| n.extras().typst.is_some());+    let mut found: Vec<NodeId> =+        st.selection.objects.iter().filter_map(|id| std::iter::successors(Some(*id), |c| d.parent_of(*c)).find(|c| typst(*c))).collect();+    found.sort_by_key(|id| id.0);+    found.dedup();+    Ok(found)+}++/// The Typst object `id` names, else the one selected: a selected object inside one counts.+fn target(s: &Session, p: &Value, cmd: &str) -> Result<NodeId> {+    let st = s.doc()?;+    let d = &st.doc;+    let typst = |id: NodeId| d.node(id).is_some_and(|n| n.extras().typst.is_some());+    // The object, or the nearest group around it that is a Typst object.+    let around = |id: NodeId| std::iter::successors(Some(id), |c| d.parent_of(*c)).find(|c| typst(*c));+    if let Some(v) = p.get("id").filter(|v| !v.is_null()) {+        let id = checked_id(s, v, cmd)?;+        return around(id).ok_or_else(|| bad(cmd, format!("object {} is not a Typst object", id.0)));+    }+    match selected(s)?[..] {+        [id] => Ok(id),+        [] => Err(EngineError::Other("select a Typst object".into())),+        _ => Err(EngineError::Other("select one Typst object".into())),+    }+}++/// A string param no longer than [`MAX_TYPST_SOURCE`].+fn text_param(p: &Value, key: &str, cmd: &str) -> Result<Option<String>> {+    match p.get(key).filter(|v| !v.is_null()) {+        None => Ok(None),+        Some(Value::String(t)) if t.len() <= MAX_TYPST_SOURCE => Ok(Some(t.clone())),+        Some(Value::String(_)) => Err(bad(cmd, format!("{key} is longer than {MAX_TYPST_SOURCE} bytes"))),+        Some(v) => Err(bad(cmd, format!("{key} must be a string, not {v}"))),+    }+}++/// `fontSize`, from 1 to 1296 pt.+fn size_param(p: &Value, cmd: &str) -> Result<Option<f64>> {+    match p.get("fontSize").filter(|v| !v.is_null()) {+        None => Ok(None),+        Some(v) => {+            v.as_f64().filter(|n| (1.0..=1296.0).contains(n)).map(Some).ok_or_else(|| bad(cmd, "fontSize must be a number from 1 to 1296 (pt)"))+        }+    }+}++fn insert(s: &mut Session, p: &Value) -> Result<Value> {+    let source = text_param(p, "source", INSERT)?.ok_or_else(|| bad(INSERT, "missing string `source`"))?;+    let preamble = text_param(p, "preamble", INSERT)?.unwrap_or_default();+    let font_size = size_param(p, INSERT)?.unwrap_or(DEFAULT_FONT_SIZE);+    let at = match p.get("at").filter(|v| !v.is_null()) {+        None => None,+        Some(v) => Some(finite_numbers(v).map(|[x, y]| Point::new(x, y)).ok_or_else(|| bad(INSERT, "at must be [x, y]"))?),+    };+    if at.is_some_and(|c| !(c.x.abs() <= MAX_COORD && c.y.abs() <= MAX_COORD)) {+        return Err(bad(INSERT, "at lies outside the canvas"));+    }+    enabled(s)?;+    let st = s.doc()?;+    let art = typeset(&source, &preamble, font_size, st.path.as_deref(), INSERT)?;+    let centre = at.or_else(|| st.doc.artboards.first().map(|a| a.rect.center())).unwrap_or_default();+    let m = Affine::translate(centre - art.page.center());+    let parent = st.insertion_parent();+    let spec = TypstSpec { source, preamble, font_size, page: art.page, xf: Affine::IDENTITY };+    s.edit("Insert Typst", |d, sel| {+        let node = group(d, art, spec, m);+        let size = node.geometric_bounds_with(&d.symbol_boxes()).unwrap_or_default();+        let id = d.insert(parent, usize::MAX, node)?;+        sel.set([id]);+        Ok(json!({ "id": id.0, "width": size.width(), "height": size.height() }))+    })+}++fn edit(s: &mut Session, p: &Value) -> Result<Value> {+    let id = target(s, p, EDIT)?;+    enabled(s)?;+    let st = s.doc()?;+    let old = st.doc.node(id).ok_or(EngineError::NoNode(id))?;+    let Some(was) = old.extras().typst.as_deref().cloned() else { return Err(bad(EDIT, "not a Typst object")) };+    let source = text_param(p, "source", EDIT)?.unwrap_or(was.source);+    let preamble = text_param(p, "preamble", EDIT)?.unwrap_or(was.preamble);+    // What the object keeps came from a file, unchecked: too long is refused, and a size Typst+    // can't use is its default.+    if source.len() > MAX_TYPST_SOURCE || preamble.len() > MAX_TYPST_SOURCE {+        return Err(bad(EDIT, format!("the Typst source is longer than {MAX_TYPST_SOURCE} bytes")));+    }+    let font_size = size_param(p, EDIT)?.unwrap_or(was.font_size);+    let font_size = if (1.0..=1296.0).contains(&font_size) { font_size } else { DEFAULT_FONT_SIZE };+    // A file's spec is untrusted: one that can't place the page puts it where the art was.+    let xf = Some(was.xf).filter(|a| a.as_coeffs().iter().all(|c| c.is_finite()) && a.determinant().abs() > 1e-12);+    let art = typeset(&source, &preamble, font_size, st.path.as_deref(), EDIT)?;+    let m = match xf {+        // The new page's centre where the old one's was, as the object was moved, scaled and rotated.+        Some(xf) => xf * Affine::translate(was.page.center() - art.page.center()),+        None => {+            let c = old.geometric_bounds_with(&st.doc.symbol_boxes()).map_or(art.page.center(), |b| b.center());+            Affine::translate(c - art.page.center())+        }+    };+    // The group keeps everything of its own (name, appearance, transparency, opacity mask,+    // attributes, graphic style…): only its contents and its spec are new.+    let kept = old.clone();+    let spec = TypstSpec { source, preamble, font_size, page: art.page, xf: Affine::IDENTITY };+    s.edit("Edit Typst", |d, sel| {+        let new = group(d, art, spec, m);+        let mut node = kept;+        node.id = new.id;+        node.bbox_angle = new.bbox_angle;+        node.extras_mut().typst = new.extras().typst.clone();+        node.kind = new.kind;+        let size = node.geometric_bounds_with(&d.symbol_boxes()).unwrap_or_default();+        let (par, index, _) = d.position(id).ok_or(EngineError::NoNode(id))?;+        d.remove(id)?;+        let new = d.insert(par, index, node)?;+        sel.set([new]);+        Ok(json!({ "id": new.0, "width": size.width(), "height": size.height() }))+    })+}++fn info(s: &mut Session, p: &Value) -> Result<Value> {+    let id = target(s, p, INFO)?;+    let n = s.doc()?.doc.node(id).ok_or(EngineError::NoNode(id))?;+    let t = n.extras().typst.as_deref().ok_or_else(|| bad(INFO, "not a Typst object"))?;+    Ok(json!({ "id": id.0, "source": t.source, "preamble": t.preamble, "fontSize": t.font_size }))+}++/// Typeset art: the SVG import it came from (for its resources), its objects and its page.+pub(crate) struct Typeset {+    doc: Document,+    nodes: Vec<Node>,+    page: Rect,+}++/// The art of a Typst object as a group of `d` that keeps `spec`, moved by `m`.+fn group(d: &mut Document, mut art: Typeset, spec: TypstSpec, m: Affine) -> Node {+    super::place::adopt::adopt(d, &art.doc, &mut art.nodes);+    let children = art.nodes.iter().map(|n| Arc::new(d.reid(n))).collect();+    let mut g = Node::group(d.alloc_id(), children);+    g.extras_mut().typst = Some(Box::new(spec));+    // The spec's `xf` becomes `m` with the art.+    super::place::transformed(g, m)+}++/// Refused while Preferences › Type › Enable Typst is off.+fn enabled(s: &Session) -> Result<()> {+    if s.prefs.enable_typst {+        return Ok(());+    }+    Err(EngineError::Other("Typst is off: turn on Enable Typst in Preferences › Type".into()))+}++/// `source` typeset, with `preamble` before it, from the folder of the document at `doc_path`.+pub(crate) fn typeset(source: &str, preamble: &str, font_size: f64, doc_path: Option<&str>, cmd: &str) -> Result<Typeset> {+    if source.trim().is_empty() {+        return Err(bad(cmd, "the Typst source is empty"));+    }+    let (input, header) = wrapped(source, preamble, font_size);+    let svg = run::compile(&input, doc_path).map_err(|e| EngineError::Other(diagnostics(&e, header, line_count(preamble))))?;+    art(&svg).map_err(|e| EngineError::Other(format!("Typst's SVG: {e}")))+}++/// The markup the program reads, and how many lines come before the preamble.+fn wrapped(source: &str, preamble: &str, font_size: f64) -> (String, usize) {+    let header = format!("#set page(width: auto, height: auto, margin: 0pt, fill: none)\n#set text(size: {font_size}pt)\n");+    let lines = header.lines().count();+    (format!("{header}{preamble}\n{source}\n"), lines)+}++/// Lines `text` takes in [`wrapped`] (it is followed by a line break).+fn line_count(text: &str) -> usize {+    text.split('\n').count()+}++/// The program's messages with their `<stdin>:line:column` places counted in the source (or the+/// preamble) as the user wrote it.+fn diagnostics(stderr: &str, header: usize, preamble: usize) -> String {+    let place = |line: usize| match line.checked_sub(header) {+        Some(l) if l > preamble => format!("line {}", l - preamble),+        Some(l) if l > 0 => format!("preamble line {l}"),+        _ => format!("line {line} of the page setup"),+    };+    let mut out: Vec<String> = stderr+        .lines()+        .filter(|l| !l.trim().is_empty())+        .take(20)+        .map(|l| match l.strip_prefix("<stdin>:").and_then(|r| r.split_once(':')) {+            Some((n, rest)) if let Ok(n) = n.parse::<usize>() => format!("{}:{rest}", place(n)),+            _ => l.to_string(),+        })+        .collect();+    if out.is_empty() {+        out.push("Typst failed without a message".into());+    }+    out.join("\n")+}++/// The objects of a Typst SVG page, its glyphs as plain paths, and the page.+fn art(svg: &str) -> std::result::Result<Typeset, String> {+    let mut doc = vectorcraft_svg::import(svg).map_err(|e| e.to_string())?;+    let page = doc.artboards.first().map(|a| a.rect).ok_or("the page is empty")?;+    // Each glyph is a `<use>` of a glyph outline: an instance, broken here (art inside a symbol+    // can be an instance too, so a few rounds).+    for _ in 0..4 {+        let mut ids = vec![];+        for l in &doc.layers {+            l.walk(&mut |n| {+                if matches!(n.kind, NodeKind::SymbolInstance { .. }) {+                    ids.push(n.id)+                }+            });+        }+        if ids.is_empty() {+            break;+        }+        super::brushsym::break_links(&mut doc, &ids).map_err(|e| e.to_string())?;+    }+    doc.symbols.clear();+    let layers = std::mem::take(&mut doc.layers);+    let nodes: Vec<Node> = layers.iter().flat_map(|l| l.children().map_or(&[][..], Vec::as_slice)).map(|n| collapsed((**n).clone())).collect();+    if nodes.is_empty() {+        return Err("the page is empty".into());+    }+    Ok(Typeset { doc, nodes, page })+}++/// `n` with each plain group of one object (as each glyph's `<g>` and its outline come in) replaced+/// by that object.+fn collapsed(mut n: Node) -> Node {+    if let Some(ch) = n.children_mut() {+        for c in ch.iter_mut() {+            *c = Arc::new(collapsed((**c).clone()));+        }+    }+    let plain = n.has_default_transparency() && *n.appearance == Default::default() && n.extras().typst.is_none();+    match &n.kind {+        NodeKind::Group { children, clip: false } if plain && children.len() == 1 => children.first().map_or(n.clone(), |c| (**c).clone()),+        _ => n,+    }+}++/// Running the `typst` program.+#[cfg(not(target_arch = "wasm32"))]+mod run {+    use std::io::{Read, Write};+    use std::process::{Command, Stdio};+    use std::time::{Duration, Instant};++    /// How long the program may take.+    const TIMEOUT: Duration = Duration::from_secs(30);+    /// The most SVG read (bytes).+    const MAX_SVG: u64 = 64 << 20;+    /// The most messages read (bytes).+    const MAX_MESSAGES: u64 = 64 << 10;++    /// The first page of `input` as SVG, else the program's messages.+    pub(super) fn compile(input: &str, doc_path: Option<&str>) -> Result<String, String> {+        let program = std::env::var(super::PROGRAM_VAR).ok().filter(|p| !p.is_empty()).unwrap_or_else(|| "typst".into());+        // Lives to the end of the run: an empty root is removed when it drops.+        let root = root(doc_path)?;+        let mut child = Command::new(&program)+            .args(["compile", "--format", "svg", "--pages", "1", "--diagnostic-format", "short", "--root"])+            .arg(root.path())+            .args(["-", "-"])+            .current_dir(root.path())+            .stdin(Stdio::piped())+            .stdout(Stdio::piped())+            .stderr(Stdio::piped())+            .spawn()+            .map_err(|e| match e.kind() {+                std::io::ErrorKind::NotFound => {+                    format!("Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)")+                }+                _ => format!("couldn't run `{program}`: {e}"),+            })?;+        // Both outputs are read while it runs, so neither pipe fills and stalls it.+        let read = |pipe: Option<Box<dyn Read + Send>>, max: u64| {+            std::thread::spawn(move || {+                let mut buf = vec![];+                if let Some(mut p) = pipe {+                    // A read error leaves what came before it, and the exit status says what went wrong.+                    let _ = (&mut p).take(max).read_to_end(&mut buf);+                    // Past the cap: drained, so the program can finish.+                    let _ = std::io::copy(&mut p, &mut std::io::sink());+                }+                buf+            })+        };+        let out = read(child.stdout.take().map(|p| Box::new(p) as Box<dyn Read + Send>), MAX_SVG + 1);+        let err = read(child.stderr.take().map(|p| Box::new(p) as Box<dyn Read + Send>), MAX_MESSAGES);+        let deadline = Instant::now() + TIMEOUT;+        // Written while it runs too: a program that doesn't read it can't stall this past the+        // deadline (killed, it closes the pipe and the writer ends).+        if let Some(mut stdin) = child.stdin.take() {+            let input = input.as_bytes().to_vec();+            std::thread::spawn(move || {+                // A program that exits before reading it all says why on stderr.+                let _ = stdin.write_all(&input);+            });+        }+        let status = loop {+            match child.try_wait() {+                Ok(Some(status)) => break status,+                Ok(None) if Instant::now() < deadline => std::thread::sleep(Duration::from_millis(10)),+                Ok(None) => {+                    // It is ended either way; a failure to kill it leaves nothing else to do.+                    let _ = child.kill();+                    let _ = child.wait();+                    return Err(format!("Typst took longer than {} seconds", TIMEOUT.as_secs()));+                }+                Err(e) => return Err(format!("couldn't wait for `{program}`: {e}")),+            }+        };+        let out = out.join().unwrap_or_default();+        let err = err.join().unwrap_or_default();+        if !status.success() {+            return Err(String::from_utf8_lossy(&err).into_owned());+        }+        if out.len() as u64 > MAX_SVG {+            return Err(format!("Typst's SVG is larger than {} MB", MAX_SVG >> 20));+        }+        String::from_utf8(out).map_err(|_| "Typst's SVG isn't UTF-8".into())+    }++    /// The project root: the document's folder, else an empty folder of our own. While automation+    /// is confined ([`crate::file_access`]), always the empty folder: Typst reads its root itself,+    /// past the checks the app's own reads go through, and follows links in it to anywhere.+    fn root(doc_path: Option<&str>) -> Result<Root, String> {+        let confined = crate::file_access::current().is_some();+        if let Some(dir) = doc_path.and_then(|p| std::path::Path::new(p).parent()).filter(|d| !confined && d.is_dir()) {+            return Ok(Root::Doc(dir.to_path_buf()));+        }+        EmptyRoot::new().map(Root::Empty)+    }++    /// Where Typst's `--root` points for one run.+    pub(super) enum Root {+        /// The document's folder.+        Doc(std::path::PathBuf),+        /// A private empty folder, removed after the run.+        Empty(EmptyRoot),+    }++    impl Root {+        pub(super) fn path(&self) -> &std::path::Path {+            match self {+                Root::Doc(p) => p,+                Root::Empty(e) => &e.0,+            }+        }+    }++    /// A private empty folder made for one Typst run and removed when dropped. Its name is random+    /// and it is created, not reused, so nobody else can have put files in it first (a guessable+    /// name made with `create_dir_all` would accept a folder another user prepared); on Unix only+    /// this user can open it.+    pub(super) struct EmptyRoot(std::path::PathBuf);++    impl EmptyRoot {+        pub(super) fn new() -> Result<Self, String> {+            use std::hash::{BuildHasher, Hasher};+            let base = std::env::temp_dir();+            let mut last = None;+            for _ in 0..16 {+                // A fresh `RandomState` is keyed from the OS, so the name can't be guessed ahead.+                let mut h = std::collections::hash_map::RandomState::new().build_hasher();+                h.write_u128(std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0));+                h.write_u32(std::process::id());+                let dir = base.join(format!("vectorcraft-typst-{:016x}", h.finish()));+                let mut builder = std::fs::DirBuilder::new();+                #[cfg(unix)]+                std::os::unix::fs::DirBuilderExt::mode(&mut builder, 0o700);+                match builder.create(&dir) {+                    Ok(()) => return Ok(Self(dir)),+                    // Taken (by an earlier run, or by someone guessing): another name.+                    Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => last = Some(e),+                    Err(e) => return Err(format!("couldn't make a folder for Typst: {e}")),+                }+            }+            Err(format!("couldn't make a folder for Typst: {}", last.map(|e| e.to_string()).unwrap_or_default()))+        }+    }++    impl Drop for EmptyRoot {+        fn drop(&mut self) {+            // Best effort: a folder left behind is empty and harmless.+            let _ = std::fs::remove_dir_all(&self.0);+        }+    }+}++/// The web build can't run programs.+#[cfg(target_arch = "wasm32")]+mod run {+    pub(super) fn compile(_: &str, _: Option<&str>) -> Result<String, String> {+        Err("Typst needs the desktop app: the web app can't run the typst program".into())+    }+}++#[cfg(test)]+mod tests {+    use super::*;++    /// The empty root is a fresh private folder each time, and it goes away with its guard.+    #[cfg(not(target_arch = "wasm32"))]+    #[test]+    fn the_empty_root_is_private_fresh_and_removed() {+        let a = run::EmptyRoot::new().unwrap();+        let b = run::EmptyRoot::new().unwrap();+        let (pa, pb) = (run::Root::Empty(a).path().to_path_buf(), run::Root::Empty(b).path().to_path_buf());+        assert_ne!(pa, pb, "two runs share a root");+        assert!(!pa.exists() && !pb.exists(), "a dropped root stays");+        let c = run::EmptyRoot::new().unwrap();+        let pc = run::Root::Empty(c);+        let dir = pc.path().to_path_buf();+        assert!(dir.is_dir());+        assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 0, "the root isn't empty");+        assert!(dir.file_name().and_then(|n| n.to_str()).is_some_and(|n| n.starts_with("vectorcraft-typst-") && n.len() == 34));+        #[cfg(unix)]+        {+            use std::os::unix::fs::PermissionsExt;+            assert_eq!(std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777, 0o700);+        }+        drop(pc);+        assert!(!dir.exists(), "the root stays after the run");+    }++    #[test]+    fn diagnostics_count_lines_in_the_source_and_preamble() {+        let (_, header) = wrapped("x", "a\nb", 11.0);+        assert_eq!(header, 2);+        let pre = line_count("a\nb");+        assert_eq!(diagnostics("<stdin>:6:4: error: expected expression", header, pre), "line 2:4: error: expected expression");+        assert_eq!(diagnostics("<stdin>:3:1: error: unknown variable: foo", header, pre), "preamble line 1:1: error: unknown variable: foo");+        assert_eq!(diagnostics("error: file not found", header, pre), "error: file not found");+        assert_eq!(diagnostics("", header, pre), "Typst failed without a message");+    }++    #[test]+    fn glyph_uses_come_in_as_paths() {+        // A page as Typst writes it: each glyph a `<use>` of an outline in `<defs>`, flipped.+        let svg = r##"<svg viewBox="0 0 20 10" width="20pt" height="10pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g transform="matrix(1 0 0 -1 0 8)"><use xlink:href="#g1" x="0" y="0" fill="#0000ff" fill-rule="nonzero"/></g><g transform="matrix(1 0 0 -1 10 8)"><use xlink:href="#g1" x="0" y="0" fill="#0000ff" fill-rule="nonzero"/></g><defs><symbol id="g1" overflow="visible"><path d="M 0 0 L 8 0 L 8 6 Z"/></symbol></defs></svg>"##;+        let t = art(svg).unwrap();+        assert!((t.page.width() - 20.0).abs() < 1e-4 && (t.page.height() - 10.0).abs() < 1e-4, "{:?}", t.page);+        assert!(t.doc.symbols.is_empty());+        assert_eq!(t.nodes.len(), 2);+        for n in &t.nodes {+            n.walk(&mut |c| assert!(!matches!(c.kind, NodeKind::SymbolInstance { .. } | NodeKind::Group { .. }), "{:?}", c.kind));+        }+    }+}diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs--- a/crates/engine/src/lib.rs+++ b/crates/engine/src/lib.rs@@ -940,6 +940,9 @@ pub struct Prefs {     pub missing_glyph_protection: bool,     pub highlight_alternate_glyphs: bool,     pub placeholder_text: bool,+    /// Type › Typst… may run the `typst` program, which downloads the `@preview` packages markup+    /// imports. Off by default; automation confined to its roots can't turn it on.+    pub enable_typst: bool,     // Units     pub units_general: String,     pub units_stroke: String,@@ -1194,6 +1197,7 @@ impl Default for Prefs {             missing_glyph_protection: true,             highlight_alternate_glyphs: false,             placeholder_text: true,+            enable_typst: false,             units_general: s("points"),             units_stroke: s("points"),             units_type: s("points"),@@ -2542,6 +2546,8 @@ mod tests_typearea; #[cfg(test)] mod tests_typescale; #[cfg(test)]+mod tests_typst;+#[cfg(test)] mod tests_units; #[cfg(test)] mod tests_variables;diff --git a/crates/engine/src/tests_typst.rs b/crates/engine/src/tests_typst.rsnew file mode 100644--- /dev/null+++ b/crates/engine/src/tests_typst.rs@@ -0,0 +1,258 @@+//! Type › Typst… (`type.typst.insert`, `type.typst.edit`, `type.typst.info`) with the `typst`+//! program: each test is skipped when it isn't installed.++use serde_json::{Value, json};+use vectorcraft_doc::{NodeId, NodeKind};+use vectorcraft_geom::Rect;++use super::*;++/// Is the `typst` program there to run?+fn have_typst() -> bool {+    let program = std::env::var(cmd::typst::PROGRAM_VAR).unwrap_or_else(|_| "typst".into());+    let found = std::process::Command::new(program).arg("--version").output().is_ok_and(|o| o.status.success());+    if !found {+        eprintln!("typst isn't installed: skipped");+    }+    found+}++fn session() -> Session {+    let mut s = Session::new();+    s.prefs.enable_typst = true;+    s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();+    s+}++fn bounds(s: &Session, id: u64) -> Rect {+    let d = &s.doc().unwrap().doc;+    d.node(NodeId(id)).unwrap().geometric_bounds_with(&d.symbol_boxes()).unwrap()+}++fn id_of(r: &Value) -> u64 {+    r["id"].as_u64().unwrap()+}++fn close(a: f64, b: f64) -> bool {+    (a - b).abs() < 1e-3+}++#[test]+fn nothing_runs_until_enable_typst_is_on() {+    let mut s = session();+    s.prefs.enable_typst = false;+    let e = s.execute("type.typst.insert", &json!({"source": "x"})).unwrap_err();+    assert_eq!(e.to_string(), "Typst is off: turn on Enable Typst in Preferences › Type");+    // Off by default, and the preference turns it on.+    assert!(!Prefs::default().enable_typst);+    s.execute("prefs.set", &json!({"key": "enableTypst", "value": true})).unwrap();+    assert!(s.prefs.enable_typst);+}++#[test]+fn confined_automation_cant_turn_typst_on() {+    let dir = std::env::temp_dir().join(format!("vectorcraft-typst-confined-{}", std::process::id()));+    std::fs::create_dir_all(&dir).unwrap();+    let roots = crate::file_access::AutomationRoots::new(Some(&dir), None).unwrap();+    let mut s = session();+    s.prefs.enable_typst = false;+    crate::file_access::confine(roots.as_ref(), || {+        let e = s.execute("prefs.set", &json!({"key": "enableTypst", "value": true})).unwrap_err();+        assert!(e.to_string().contains("Enable Typst"), "{e}");+        let mut p = s.prefs.clone();+        p.enable_typst = true;+        s.apply_prefs(p);+        assert!(!s.prefs.enable_typst);+        // Once the user turned it on, automation may turn it off.+        s.prefs.enable_typst = true;+        s.execute("prefs.set", &json!({"key": "enableTypst", "value": false})).unwrap();+        assert!(!s.prefs.enable_typst);+    });+    let _ = std::fs::remove_dir_all(&dir);+}++#[test]+fn insert_places_paths_that_keep_their_source() {+    if !have_typst() {+        return;+    }+    let mut s = session();+    let r = s.execute("type.typst.insert", &json!({"source": "$ x^2 + y^2 = z^2 $", "at": [100, 80]})).unwrap();+    let id = id_of(&r);+    let d = &s.doc().unwrap().doc;+    let g = d.node(NodeId(id)).unwrap();+    assert!(matches!(g.kind, NodeKind::Group { .. }));+    assert_eq!(g.kind_label(), "Typst");+    // Glyphs are plain paths: no symbols join the document.+    assert!(d.symbols.is_empty());+    let mut paths = 0;+    g.walk(&mut |n| {+        assert!(!matches!(n.kind, NodeKind::SymbolInstance { .. }));+        paths += matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. }) as usize;+    });+    assert!(paths >= 9, "x 2 + y 2 = z 2: {paths} paths");+    assert_eq!(s.doc().unwrap().selection.objects, vec![NodeId(id)]);+    let b = bounds(&s, id);+    // Centred on `at` (the page is; the ink lies a little off its centre).+    assert!((b.center().x - 100.0).abs() < b.width() / 4.0 && (b.center().y - 80.0).abs() < b.height(), "{b:?}");+    let info = s.execute("type.typst.info", &json!({})).unwrap();+    assert_eq!(info["source"], "$ x^2 + y^2 = z^2 $");+    assert_eq!(info["fontSize"], 11.0);+    // One undo step.+    s.execute("edit.undo", &json!({})).unwrap();+    assert!(s.doc().unwrap().doc.node(NodeId(id)).is_none());+}++#[test]+fn a_bigger_font_size_typesets_bigger() {+    if !have_typst() {+        return;+    }+    let mut s = session();+    let a = id_of(&s.execute("type.typst.insert", &json!({"source": "Hello"})).unwrap());+    let b = id_of(&s.execute("type.typst.insert", &json!({"source": "Hello", "fontSize": 22})).unwrap());+    let (a, b) = (bounds(&s, a).width(), bounds(&s, b).width());+    assert!((b / a - 2.0).abs() < 0.05, "{a} → {b}");+}++#[test]+fn edit_keeps_the_objects_place_scale_and_rotation() {+    if !have_typst() {+        return;+    }+    let mut s = session();+    let id = id_of(&s.execute("type.typst.insert", &json!({"source": "$ a $", "at": [200, 150]})).unwrap());+    s.execute("object.scale", &json!({"sx": 200})).unwrap();+    s.execute("object.move", &json!({"dx": 30, "dy": 10})).unwrap();+    let before = bounds(&s, id);+    let r = s.execute("type.typst.edit", &json!({"source": "$ a + b + c $"})).unwrap();+    let new = id_of(&r);+    let after = bounds(&s, new);+    // The centre stays; the art is twice the size it typesets at.+    assert!(close(after.center().y, before.center().y) && (after.center().x - before.center().x).abs() < 0.5, "{before:?} → {after:?}");+    let mut fresh = session();+    let plain = id_of(&fresh.execute("type.typst.insert", &json!({"source": "$ a + b + c $"})).unwrap());+    let natural = bounds(&fresh, plain);+    assert!((after.height() / natural.height() - 2.0).abs() < 0.01, "{natural:?} → {after:?}");+    // What isn't given stays: the edit is selected, and its info is the new source.+    assert_eq!(s.doc().unwrap().selection.objects, vec![NodeId(new)]);+    assert_eq!(s.execute("type.typst.info", &json!({})).unwrap()["source"], "$ a + b + c $");+    // Rotated, the edit stays rotated.+    s.execute("object.rotate", &json!({"angle": 90})).unwrap();+    let tall = bounds(&s, new);+    let r = s.execute("type.typst.edit", &json!({"fontSize": 11})).unwrap();+    let b = bounds(&s, id_of(&r));+    assert!(b.height() > b.width() && close(b.height(), tall.height()), "{tall:?} → {b:?}");+}++#[test]+fn the_source_is_saved_with_the_document() {+    if !have_typst() {+        return;+    }+    let mut s = session();+    let id = id_of(&s.execute("type.typst.insert", &json!({"source": "#emph[hi]", "preamble": "#set text(fill: red)", "fontSize": 14})).unwrap());+    let bytes = vectorcraft_format::save(&s.doc().unwrap().doc, false);+    let doc = vectorcraft_format::load(&bytes).unwrap();+    let t = doc.node(NodeId(id)).unwrap().extras().typst.clone().unwrap();+    assert_eq!((t.source.as_str(), t.preamble.as_str(), t.font_size), ("#emph[hi]", "#set text(fill: red)", 14.0));+}++#[test]+fn typst_errors_name_the_line_of_the_source() {+    if !have_typst() {+        return;+    }+    let mut s = session();+    let e = s.execute("type.typst.insert", &json!({"source": "ok\n$ x^ $", "preamble": "#let a = 1"})).unwrap_err().to_string();+    assert!(e.contains("line 2:"), "{e}");+    let e = s.execute("type.typst.insert", &json!({"source": "ok", "preamble": "#nope"})).unwrap_err().to_string();+    assert!(e.contains("preamble line 1:"), "{e}");+    // Nothing was added.+    assert!(s.execute("type.typst.info", &json!({})).is_err());+}++#[test]+fn bad_params_fail_before_typesetting() {+    let mut s = session();+    for p in [json!({}), json!({"source": "  "}), json!({"source": 3}), json!({"source": "a", "fontSize": 0}), json!({"source": "a", "at": [1]})] {+        assert!(s.execute("type.typst.insert", &p).is_err(), "{p}");+    }+    // Edit and info need a Typst object.+    assert!(s.execute("type.typst.edit", &json!({"source": "a"})).is_err());+    s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();+    assert!(s.execute("type.typst.info", &json!({})).is_err());+}++/// The node `id` of the active document, to change as a file could have.+fn node_mut(s: &mut Session, id: u64) -> &mut vectorcraft_doc::Node {+    std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).node_mut(NodeId(id)).unwrap()+}++#[test]+fn edit_keeps_what_the_group_itself_has() {+    if !have_typst() {+        return;+    }+    let mut s = session();+    let id = id_of(&s.execute("type.typst.insert", &json!({"source": "$ a $"})).unwrap());+    {+        let n = node_mut(&mut s, id);+        n.name = Some("Formula".into());+        n.opacity = 0.5;+        n.isolate = true;+        n.extras_mut().attrs = Some(Box::new(vectorcraft_doc::ObjectAttributes { note: "eq. 3".into(), ..Default::default() }));+        n.extras_mut().graphic_style = Some(7);+    }+    let new = id_of(&s.execute("type.typst.edit", &json!({"source": "$ b $"})).unwrap());+    let d = &s.doc().unwrap().doc;+    let n = d.node(NodeId(new)).unwrap();+    assert_eq!((n.name.as_deref(), n.opacity, n.isolate), (Some("Formula"), 0.5, true));+    assert_eq!(n.extras().attrs.as_ref().map(|a| a.note.as_str()), Some("eq. 3"));+    assert_eq!(n.extras().graphic_style, Some(7));+    assert_eq!(n.extras().typst.as_ref().map(|t| t.source.as_str()), Some("$ b $"));+}++#[test]+fn a_files_spec_is_checked_before_typesetting() {+    if !have_typst() {+        return;+    }+    let mut s = session();+    let id = id_of(&s.execute("type.typst.insert", &json!({"source": "$ a $"})).unwrap());+    // A size Typst can't use is typeset at the default.+    node_mut(&mut s, id).extras_mut().typst.as_mut().unwrap().font_size = f64::NAN;+    let new = id_of(&s.execute("type.typst.edit", &json!({})).unwrap());+    assert_eq!(s.execute("type.typst.info", &json!({})).unwrap()["fontSize"], cmd::typst::DEFAULT_FONT_SIZE);+    // A source too long to typeset is refused, unless the edit replaces it.+    node_mut(&mut s, new).extras_mut().typst.as_mut().unwrap().source = "a".repeat(vectorcraft_doc::typst::MAX_TYPST_SOURCE + 1);+    let e = s.execute("type.typst.edit", &json!({})).unwrap_err().to_string();+    assert!(e.contains("longer than"), "{e}");+    s.execute("type.typst.edit", &json!({"source": "$ c $"})).unwrap();+}++#[test]+fn confined_automation_keeps_typst_out_of_folders_it_may_not_read() {+    if !have_typst() {+        return;+    }+    let base = std::env::temp_dir().join(format!("vectorcraft-typst-roots-{}", std::process::id()));+    let (docs, allowed) = (base.join("docs"), base.join("allowed"));+    std::fs::create_dir_all(&docs).unwrap();+    std::fs::create_dir_all(&allowed).unwrap();+    std::fs::write(docs.join("secret.txt"), "secret").unwrap();+    let mut s = session();+    s.doc_mut().unwrap().path = Some(docs.join("figure.vectorcraft").to_string_lossy().into_owned());+    let read = json!({"source": "#read(\"secret.txt\")"});+    // Unconfined (the person at the keyboard), the document's folder is Typst's root.+    s.execute("type.typst.insert", &read).unwrap();+    // Confined to another folder, it isn't.+    let roots = crate::file_access::AutomationRoots::new(Some(&allowed), None).unwrap();+    let r = crate::file_access::confine(roots.as_ref(), || s.execute("type.typst.insert", &read));+    assert!(r.is_err(), "{r:?}");+    // Nor confined to a folder that holds it: a link in it could lead anywhere.+    let roots = crate::file_access::AutomationRoots::new(Some(&base), None).unwrap();+    let r = crate::file_access::confine(roots.as_ref(), || s.execute("type.typst.insert", &read));+    assert!(r.is_err(), "{r:?}");+    let _ = std::fs::remove_dir_all(&base);+}diff --git a/crates/ui-egui/src/dialogs/mod.rs b/crates/ui-egui/src/dialogs/mod.rs--- a/crates/ui-egui/src/dialogs/mod.rs+++ b/crates/ui-egui/src/dialogs/mod.rs@@ -106,6 +106,7 @@ pub mod tile_edge_color; mod tools; pub(crate) mod transform; pub mod transform_each;+pub mod typst; pub mod variables; mod warp_effect; pub mod width_point;@@ -342,6 +343,7 @@ registry! {     PerspectiveGridOptions: [perspective_options::KIND] => perspective_options::SPEC,     BlendOptions: [blend_options::KIND] => blend_options::SPEC,     LivePaintGaps: [live_paint_gaps::KIND] => live_paint_gaps::SPEC,+    Typst: [typst::KIND] => typst::SPEC,     LivePaintSelectionOptions: [live_paint_selection::KIND] => live_paint_selection::SPEC,     BrushOptions: [brush_options::KIND] => brush_options::SPEC,     NewBrush: [new_brush::KIND] => new_brush::SPEC,diff --git a/crates/ui-egui/src/dialogs/typst.rs b/crates/ui-egui/src/dialogs/typst.rsnew file mode 100644--- /dev/null+++ b/crates/ui-egui/src/dialogs/typst.rs@@ -0,0 +1,195 @@+//! Type › Typst…: Typst markup typeset into the document (`type.typst.insert`), or, with a Typst+//! object selected, that object's source to change and typeset again (`type.typst.edit`). OK+//! typesets; when Typst reports an error the dialog stays open and shows it under the source.+//!+//! Nothing is typeset until Enable Typst (Preferences › Type) is on; the dialog has the same+//! switch, and says that Typst downloads the packages markup imports.+//!+//! Fields: `source`, `preamble`, `fontSize` (pt), `__id` (the Typst object edited; null to insert)+//! and `__error` (Typst's last message).++use serde_json::{Value, json};++use super::DialogSpec;+use crate::state::Dialog;+use crate::theme::Tokens;+use crate::{VectorcraftApp, widgets};++/// The dialog kind of Type › Typst….+pub const KIND: &str = "typst";++/// Width of the label column.+const LABEL_W: f32 = 82.0;+/// Width of the source and preamble boxes.+const BOX_W: f32 = 440.0;+/// What the source box starts with when inserting.+const EXAMPLE: &str = "$ sum_(k=1)^n k = (n(n+1))/2 $";++pub(super) const SPEC: DialogSpec = DialogSpec {+    heading: |d| if d.fields.get("__id").is_some_and(|v| !v.is_null()) { tl!("Edit Typst") } else { tl!("Insert Typst") }.into(),+    body,+    confirm,+    min_width: 480.0,+    ..DialogSpec::FORM+};++/// Open Type › Typst…: on the selected Typst object's source, else to insert.+pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {+    let selected = vectorcraft_engine::cmd::typst::selected(&app.session).unwrap_or_default();+    let fields = match selected[..] {+        [] => json!({+            "source": EXAMPLE,+            "preamble": "",+            "fontSize": vectorcraft_engine::cmd::typst::DEFAULT_FONT_SIZE,+            "__id": null,+        }),+        [id] => {+            let i = app.session.execute("type.typst.info", &json!({ "id": id.0 })).map_err(|e| e.to_string())?;+            json!({ "source": i["source"], "preamble": i["preamble"], "fontSize": i["fontSize"], "__id": i["id"] })+        }+        // Not a new object either: which one to edit is unclear.+        _ => return Err("select one Typst object".into()),+    };+    app.ui.dialog = Some(Dialog::new(KIND, fields));+    Ok(Value::Null)+}++/// A monospaced box `rows` lines tall bound to `d.fields[key]`. Enter adds a line.+fn code(ui: &mut egui::Ui, d: &mut Dialog, key: &str, rows: usize) {+    let t = Tokens::get(ui.ctx());+    let mut s = d.str(key);+    let id = ui.id().with(("typst-code", key));+    // The source box (the first) takes the keyboard focus when the dialog opens.+    widgets::take_dialog_focus(ui, id, &s);+    let r = egui::Frame::NONE+        .fill(t.input)+        .stroke(egui::Stroke::new(1.0, t.input_border))+        .corner_radius(egui::CornerRadius::same(3))+        .inner_margin(egui::Margin::symmetric(6, 3))+        .show(ui, |ui| {+            crate::theme::strong_selected_text(ui);+            let edit = egui::TextEdit::multiline(&mut s)+                .id(id)+                .frame(egui::Frame::NONE)+                .desired_width(BOX_W)+                .desired_rows(rows)+                .font(egui::TextStyle::Monospace);+            ui.add(edit)+        });+    if r.inner.changed() {+        d.fields.insert(key.into(), Value::String(s));+    }+    if r.inner.has_focus() {+        ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Enter));+    }+}++fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {+    let t = Tokens::get(ui.ctx());+    widgets::label_row(ui, "Text:", LABEL_W, |_| {});+    code(ui, d, "source", 8);+    if let Some(e) = d.fields.get("__error").and_then(Value::as_str).filter(|e| !e.is_empty()) {+        ui.add_space(4.0);+        ui.label(egui::RichText::new(e).monospace().size(11.0).color(t.error));+    }+    ui.add_space(6.0);+    widgets::label_row(ui, "Preamble:", LABEL_W, |_| {});+    code(ui, d, "preamble", 3);+    ui.add_space(6.0);+    widgets::label_row(ui, "Size:", LABEL_W, |ui| {+        if let Some(v) = widgets::plain_field(ui, "typst-size", d.f64("fontSize", 11.0), "pt", 1, 64.0) {+            d.fields.insert("fontSize".into(), json!(v.clamp(1.0, 1296.0)));+        }+    });+    ui.add_space(8.0);+    let on = app.session.prefs.enable_typst;+    if widgets::check(ui, "Enable Typst", on, true) {+        // A refusal (automation confined to its roots) shows in the status bar.+        let _ = app.run("prefs.set", json!({ "key": "enableTypst", "value": !on }));+    }+    ui.label(+        egui::RichText::new(tl!("Runs the typst program, which downloads the @preview packages the markup imports.")).size(11.0).color(t.text_dim),+    );+    false+}++fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {+    let mut p = json!({ "source": d.str("source"), "preamble": d.str("preamble"), "fontSize": d.f64("fontSize", 11.0) });+    let id = d.fields.get("__id").filter(|v| !v.is_null()).cloned();+    let cmd = match id {+        Some(id) => {+            p["id"] = id;+            "type.typst.edit"+        }+        None => "type.typst.insert",+    };+    match app.run(cmd, p) {+        Ok(v) => {+            // The message of a failed try no longer applies.+            if d.fields.get("__error").and_then(Value::as_str).is_some_and(|e| !e.is_empty()) {+                app.status(String::new());+            }+            app.ui.dialog = None;+            Ok(v)+        }+        // The dialog stays open with the message, to fix the source.+        Err(e) => {+            if let Some(open) = app.ui.dialog.as_mut().filter(|o| o.kind == KIND) {+                open.fields.insert("__error".into(), json!(e));+            }+            Err(e)+        }+    }+}++#[cfg(test)]+mod tests {+    use super::*;++    fn have_typst() -> bool {+        std::process::Command::new("typst").arg("--version").output().is_ok_and(|o| o.status.success())+    }++    #[test]+    fn inserts_then_reopens_on_the_selection_to_edit_and_keeps_errors_open() {+        let mut app = crate::VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());+        app.session.prefs.enable_typst = true;+        app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();+        open(&mut app).unwrap();+        let d = app.ui.dialog.clone().unwrap();+        assert_eq!((d.kind.as_str(), d.str("source").as_str()), (KIND, EXAMPLE));+        assert!(d.fields["__id"].is_null());+        assert_eq!((SPEC.heading)(&d), "Insert Typst");+        if !have_typst() {+            eprintln!("typst isn't installed: the rest is skipped");+            return;+        }+        super::super::confirm(&mut app).unwrap();+        assert!(app.ui.dialog.is_none());+        // Open again on the new object: Edit, with its source.+        open(&mut app).unwrap();+        let d = app.ui.dialog.clone().unwrap();+        assert!(d.fields["__id"].is_u64());+        assert_eq!((SPEC.heading)(&d), "Edit Typst");+        // An error keeps the dialog open, showing Typst's message.+        app.ui.dialog.as_mut().unwrap().fields.insert("source".into(), json!("#nope"));+        assert!(super::super::confirm(&mut app).is_err());+        let d = app.ui.dialog.clone().unwrap();+        assert!(d.str("__error").contains("line 1:"), "{:?}", d.fields);+    }++    #[test]+    fn two_typst_objects_selected_open_nothing() {+        if !have_typst() {+            return;+        }+        let mut app = crate::VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());+        app.session.prefs.enable_typst = true;+        app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();+        app.session.execute("type.typst.insert", &json!({"source": "a", "at": [100, 100]})).unwrap();+        app.session.execute("type.typst.insert", &json!({"source": "b", "at": [300, 100]})).unwrap();+        app.session.execute("select.all", &json!({})).unwrap();+        assert_eq!(open(&mut app).unwrap_err(), "select one Typst object");+        assert!(app.ui.dialog.is_none());+    }+}diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv--- a/crates/ui-egui/src/i18n/bg.tsv+++ b/crates/ui-egui/src/i18n/bg.tsv@@ -5331,6 +5331,16 @@ theme	Appearance	Външен вид 	Plaster	Гипс 	Note Paper…	Хартия за писма… 	Note Paper	Хартия за писма+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Вмъкване на Typst+	Edit Typst	Редактиране на Typst+	Typst Info	Информация за Typst+	Preamble:	Преамбюл:+	Enable Typst	Включване на Typst+	Runs the typst program, which downloads the @preview packages the markup imports.	Изпълнява програмата typst, която изтегля пакетите @preview, импортирани от маркировката.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst е изключен: включете „Включване на Typst“ в Настройки › Тип+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	автоматизацията не може да включи „Включване на Typst“: включете го в Настройки › Тип 	Water Paper…	Мокра хартия… 	Water Paper	Мокра хартия 	Ruby…	Руби…diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv--- a/crates/ui-egui/src/i18n/cs.tsv+++ b/crates/ui-egui/src/i18n/cs.tsv@@ -5374,6 +5374,35 @@ mark-style	Japanese	Japonské 	Plaster	Sádra 	Note Paper…	Dopisní papír… 	Note Paper	Dopisní papír+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Vložit Typst+	Edit Typst	Upravit Typst+	Typst Info	Informace o Typst+	Preamble:	Preambule:+	Enable Typst	Povolit Typst+	Runs the typst program, which downloads the @preview packages the markup imports.	Spouští program typst, který stahuje balíčky @preview importované značkováním.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst je vypnutý: zapněte Povolit Typst v Předvolby › Text+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	automatizace nemůže zapnout Povolit Typst: zapněte ho v Předvolby › Text+@msg	Typst needs the desktop app: the web app can't run the typst program	Typst potřebuje desktopovou aplikaci: webová aplikace nemůže spustit program typst+@msg	Typst took longer than {_1} seconds	Typst trval déle než {_1} sekund+@msg	Typst's SVG is larger than {_1} MB	SVG z Typstu je větší než {_1} MB+@msg	Typst's SVG: {e}	SVG z Typstu: {e}+@msg	couldn't wait for `{program}`: {e}	nelze počkat na `{program}`: {e}+@msg	select a Typst object	vyberte objekt Typst+@msg	select one Typst object	vyberte jeden objekt Typst+@msg	the page is empty	stránka je prázdná+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	Typst není nainstalován: `{program}` nebyl nalezen (nainstalujte jej z typst.app nebo nastavte VECTORCRAFT_TYPST na jeho cestu)+@msg	couldn't run `{program}`: {e}	nelze spustit `{program}`: {e}+@msg	at lies outside the canvas	at leží mimo plátno+@msg	fontSize must be a number from 1 to 1296 (pt)	fontSize musí být číslo od 1 do 1296 (pt)+@msg	missing string `source`	chybí řetězec `source`+@msg	not a Typst object	nejde o objekt Typst+@msg	object {_1} is not a Typst object	objekt {_1} není objekt Typst+@msg	the Typst source is empty	zdroj Typst je prázdný+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	zdroj Typst je delší než {MAX_TYPST_SOURCE} bajtů+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	{key} je delší než {MAX_TYPST_SOURCE} bajtů+@msg	{key} must be a string, not {v}	{key} musí být řetězec, ne {v} 	Water Paper…	Mokrý papír… 	Water Paper	Mokrý papír 	Ruby…	Ruby…diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv--- a/crates/ui-egui/src/i18n/da.tsv+++ b/crates/ui-egui/src/i18n/da.tsv@@ -5347,6 +5347,16 @@ swatch	Bright Violet	Kraftig violet 	Plaster	Gips 	Note Paper…	Notatpapir… 	Note Paper	Notatpapir+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Indsæt Typst+	Edit Typst	Rediger Typst+	Typst Info	Typst-oplysninger+	Preamble:	Præambel:+	Enable Typst	Aktivér Typst+	Runs the typst program, which downloads the @preview packages the markup imports.	Kører programmet typst, som henter de @preview-pakker, opmærkningen importerer.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst er slået fra: slå Aktivér Typst til under Præferencer › Tekst+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	automatisering kan ikke slå Aktivér Typst til: slå det til under Præferencer › Tekst 	Water Paper…	Vådt papir… 	Water Paper	Vådt papir 	Ruby…	Ruby…diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv--- a/crates/ui-egui/src/i18n/de.tsv+++ b/crates/ui-egui/src/i18n/de.tsv@@ -4955,6 +4955,35 @@ effect	Soft	Weich 	Plaster	Gipsabdruck 	Note Paper…	Prägepapier… 	Note Paper	Prägepapier+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Typst einfügen+	Edit Typst	Typst bearbeiten+	Typst Info	Typst-Informationen+	Preamble:	Präambel:+	Enable Typst	Typst aktivieren+	Runs the typst program, which downloads the @preview packages the markup imports.	Führt das Programm typst aus, das die vom Markup importierten @preview-Pakete herunterlädt.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst ist aus: Aktivieren Sie „Typst aktivieren“ unter Voreinstellungen › Text+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	Automatisierung kann „Typst aktivieren“ nicht einschalten: Schalten Sie es unter Voreinstellungen › Text ein+@msg	Typst needs the desktop app: the web app can't run the typst program	Typst benötigt die Desktop-App: Die Web-App kann das Programm typst nicht ausführen+@msg	Typst took longer than {_1} seconds	Typst hat länger als {_1} Sekunden gebraucht+@msg	Typst's SVG is larger than {_1} MB	Das SVG von Typst ist größer als {_1} MB+@msg	Typst's SVG: {e}	SVG von Typst: {e}+@msg	couldn't wait for `{program}`: {e}	Warten auf `{program}` nicht möglich: {e}+@msg	select a Typst object	Wählen Sie ein Typst-Objekt aus+@msg	select one Typst object	Wählen Sie genau ein Typst-Objekt aus+@msg	the page is empty	Die Seite ist leer+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	Typst ist nicht installiert: `{program}` wurde nicht gefunden (installieren Sie es von typst.app oder setzen Sie VECTORCRAFT_TYPST auf seinen Pfad)+@msg	couldn't run `{program}`: {e}	`{program}` konnte nicht ausgeführt werden: {e}+@msg	at lies outside the canvas	at liegt außerhalb der Arbeitsfläche+@msg	fontSize must be a number from 1 to 1296 (pt)	fontSize muss eine Zahl von 1 bis 1296 (pt) sein+@msg	missing string `source`	Zeichenfolge `source` fehlt+@msg	not a Typst object	kein Typst-Objekt+@msg	object {_1} is not a Typst object	Objekt {_1} ist kein Typst-Objekt+@msg	the Typst source is empty	der Typst-Quelltext ist leer+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	der Typst-Quelltext ist länger als {MAX_TYPST_SOURCE} Bytes+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	{key} ist länger als {MAX_TYPST_SOURCE} Bytes+@msg	{key} must be a string, not {v}	{key} muss eine Zeichenfolge sein, nicht {v} 	Water Paper…	Feuchtes Papier… 	Water Paper	Feuchtes Papier 	Ruby…	Ruby…diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv--- a/crates/ui-egui/src/i18n/el.tsv+++ b/crates/ui-egui/src/i18n/el.tsv@@ -5326,6 +5326,35 @@ effect	Points	Σημεία 	Plaster	Γύψος 	Note Paper…	Χαρτί σημειώσεων… 	Note Paper	Χαρτί σημειώσεων+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Εισαγωγή Typst+	Edit Typst	Επεξεργασία Typst+	Typst Info	Πληροφορίες Typst+	Preamble:	Προοίμιο:+	Enable Typst	Ενεργοποίηση Typst+	Runs the typst program, which downloads the @preview packages the markup imports.	Εκτελεί το πρόγραμμα typst, το οποίο κατεβάζει τα πακέτα @preview που εισάγει η σήμανση.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Το Typst είναι απενεργοποιημένο: ενεργοποιήστε την επιλογή Ενεργοποίηση Typst στις Προτιμήσεις › Κείμενο+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	ο αυτοματισμός δεν μπορεί να ενεργοποιήσει την επιλογή Ενεργοποίηση Typst: ενεργοποιήστε την στις Προτιμήσεις › Κείμενο+@msg	Typst needs the desktop app: the web app can't run the typst program	Το Typst χρειάζεται την εφαρμογή υπολογιστή: η εφαρμογή ιστού δεν μπορεί να εκτελέσει το πρόγραμμα typst+@msg	Typst took longer than {_1} seconds	Το Typst χρειάστηκε περισσότερο από {_1} δευτερόλεπτα+@msg	Typst's SVG is larger than {_1} MB	Το SVG του Typst είναι μεγαλύτερο από {_1} MB+@msg	Typst's SVG: {e}	SVG του Typst: {e}+@msg	couldn't wait for `{program}`: {e}	δεν ήταν δυνατή η αναμονή για το `{program}`: {e}+@msg	select a Typst object	επιλέξτε ένα αντικείμενο Typst+@msg	select one Typst object	επιλέξτε μόνο ένα αντικείμενο Typst+@msg	the page is empty	η σελίδα είναι κενή+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	Το Typst δεν είναι εγκατεστημένο: το `{program}` δεν βρέθηκε (εγκαταστήστε το από το typst.app ή ορίστε το VECTORCRAFT_TYPST στη διαδρομή του)+@msg	couldn't run `{program}`: {e}	δεν ήταν δυνατή η εκτέλεση του `{program}`: {e}+@msg	at lies outside the canvas	το at βρίσκεται εκτός καμβά+@msg	fontSize must be a number from 1 to 1296 (pt)	το fontSize πρέπει να είναι αριθμός από 1 έως 1296 (pt)+@msg	missing string `source`	λείπει η συμβολοσειρά `source`+@msg	not a Typst object	δεν είναι αντικείμενο Typst+@msg	object {_1} is not a Typst object	το αντικείμενο {_1} δεν είναι αντικείμενο Typst+@msg	the Typst source is empty	ο πηγαίος κώδικας Typst είναι κενός+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	ο πηγαίος κώδικας Typst είναι μεγαλύτερος από {MAX_TYPST_SOURCE} byte+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	το {key} είναι μεγαλύτερο από {MAX_TYPST_SOURCE} byte+@msg	{key} must be a string, not {v}	το {key} πρέπει να είναι συμβολοσειρά, όχι {v} 	Water Paper…	Βρεγμένο χαρτί… 	Water Paper	Βρεγμένο χαρτί 	Ruby…	Ruby…diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv--- a/crates/ui-egui/src/i18n/es.tsv+++ b/crates/ui-egui/src/i18n/es.tsv@@ -5316,6 +5316,35 @@ mark-style	Japanese	Japonesas 	Plaster	Yeso 	Note Paper…	Papel con relieve… 	Note Paper	Papel con relieve+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Insertar Typst+	Edit Typst	Editar Typst+	Typst Info	Información de Typst+	Preamble:	Preámbulo:+	Enable Typst	Activar Typst+	Runs the typst program, which downloads the @preview packages the markup imports.	Ejecuta el programa typst, que descarga los paquetes @preview que importa el marcado.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst está desactivado: active Activar Typst en Preferencias › Texto+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	la automatización no puede activar Activar Typst: actívelo en Preferencias › Texto+@msg	Typst needs the desktop app: the web app can't run the typst program	Typst necesita la aplicación de escritorio: la aplicación web no puede ejecutar el programa typst+@msg	Typst took longer than {_1} seconds	Typst tardó más de {_1} segundos+@msg	Typst's SVG is larger than {_1} MB	El SVG de Typst ocupa más de {_1} MB+@msg	Typst's SVG: {e}	SVG de Typst: {e}+@msg	couldn't wait for `{program}`: {e}	no se pudo esperar a `{program}`: {e}+@msg	select a Typst object	selecciona un objeto Typst+@msg	select one Typst object	selecciona un solo objeto Typst+@msg	the page is empty	la página está vacía+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	Typst no está instalado: no se encontró `{program}` (instálalo desde typst.app o define VECTORCRAFT_TYPST con su ruta)+@msg	couldn't run `{program}`: {e}	no se pudo ejecutar `{program}`: {e}+@msg	at lies outside the canvas	at está fuera del lienzo+@msg	fontSize must be a number from 1 to 1296 (pt)	fontSize debe ser un número de 1 a 1296 (pt)+@msg	missing string `source`	falta el texto `source`+@msg	not a Typst object	no es un objeto Typst+@msg	object {_1} is not a Typst object	el objeto {_1} no es un objeto Typst+@msg	the Typst source is empty	el código Typst está vacío+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	el código Typst supera los {MAX_TYPST_SOURCE} bytes+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	{key} supera los {MAX_TYPST_SOURCE} bytes+@msg	{key} must be a string, not {v}	{key} debe ser un texto, no {v} 	Water Paper…	Papel mojado… 	Water Paper	Papel mojado 	Ruby…	Ruby…diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv--- a/crates/ui-egui/src/i18n/fi.tsv+++ b/crates/ui-egui/src/i18n/fi.tsv@@ -5405,6 +5405,35 @@ swatch	Bright Violet	Kirkas violetti 	Plaster	Kipsi 	Note Paper…	Kirjepaperi… 	Note Paper	Kirjepaperi+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Lisää Typst+	Edit Typst	Muokkaa Typstia+	Typst Info	Typst-tiedot+	Preamble:	Johdanto:+	Enable Typst	Ota Typst käyttöön+	Runs the typst program, which downloads the @preview packages the markup imports.	Suorittaa typst-ohjelman, joka lataa merkintäkielen tuomat @preview-paketit.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst ei ole käytössä: valitse Ota Typst käyttöön kohdassa Asetukset › Teksti+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	automaatio ei voi ottaa Typstia käyttöön: valitse Ota Typst käyttöön kohdassa Asetukset › Teksti+@msg	Typst needs the desktop app: the web app can't run the typst program	Typst tarvitsee työpöytäsovelluksen: verkkosovellus ei voi suorittaa typst-ohjelmaa+@msg	Typst took longer than {_1} seconds	Typst kesti yli {_1} sekuntia+@msg	Typst's SVG is larger than {_1} MB	Typstin SVG on yli {_1} Mt+@msg	Typst's SVG: {e}	Typstin SVG: {e}+@msg	couldn't wait for `{program}`: {e}	ohjelmaa `{program}` ei voitu odottaa: {e}+@msg	select a Typst object	valitse Typst-objekti+@msg	select one Typst object	valitse vain yksi Typst-objekti+@msg	the page is empty	sivu on tyhjä+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	Typst ei ole asennettu: ohjelmaa `{program}` ei löytynyt (asenna se osoitteesta typst.app tai aseta VECTORCRAFT_TYPST sen poluksi)+@msg	couldn't run `{program}`: {e}	ohjelmaa `{program}` ei voitu suorittaa: {e}+@msg	at lies outside the canvas	at on kankaan ulkopuolella+@msg	fontSize must be a number from 1 to 1296 (pt)	fontSize on oltava luku väliltä 1–1296 (pt)+@msg	missing string `source`	merkkijono `source` puuttuu+@msg	not a Typst object	ei Typst-objekti+@msg	object {_1} is not a Typst object	objekti {_1} ei ole Typst-objekti+@msg	the Typst source is empty	Typst-lähde on tyhjä+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	Typst-lähde on pidempi kuin {MAX_TYPST_SOURCE} tavua+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	{key} on pidempi kuin {MAX_TYPST_SOURCE} tavua+@msg	{key} must be a string, not {v}	{key} on oltava merkkijono, ei {v} 	Water Paper…	Märkä paperi… 	Water Paper	Märkä paperi 	Ruby…	Ruby…diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv--- a/crates/ui-egui/src/i18n/fr.tsv+++ b/crates/ui-egui/src/i18n/fr.tsv@@ -5373,6 +5373,35 @@ theme	Appearance	Apparence 	Plaster	Plâtre 	Note Paper…	Papier gaufré… 	Note Paper	Papier gaufré+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Insérer du Typst+	Edit Typst	Modifier le Typst+	Typst Info	Informations Typst+	Preamble:	Préambule :+	Enable Typst	Activer Typst+	Runs the typst program, which downloads the @preview packages the markup imports.	Exécute le programme typst, qui télécharge les paquets @preview importés par le balisage.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst est désactivé : cochez Activer Typst dans Préférences › Texte+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	l’automatisation ne peut pas cocher Activer Typst : cochez-le dans Préférences › Texte+@msg	Typst needs the desktop app: the web app can't run the typst program	Typst nécessite l’application de bureau : l’application web ne peut pas exécuter le programme typst+@msg	Typst took longer than {_1} seconds	Typst a pris plus de {_1} secondes+@msg	Typst's SVG is larger than {_1} MB	Le SVG de Typst dépasse {_1} Mo+@msg	Typst's SVG: {e}	SVG de Typst : {e}+@msg	couldn't wait for `{program}`: {e}	impossible d’attendre `{program}` : {e}+@msg	select a Typst object	sélectionnez un objet Typst+@msg	select one Typst object	sélectionnez un seul objet Typst+@msg	the page is empty	la page est vide+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	Typst n’est pas installé : `{program}` est introuvable (installez-le depuis typst.app ou définissez VECTORCRAFT_TYPST sur son chemin)+@msg	couldn't run `{program}`: {e}	impossible d’exécuter `{program}` : {e}+@msg	at lies outside the canvas	at est hors de la zone de travail+@msg	fontSize must be a number from 1 to 1296 (pt)	fontSize doit être un nombre compris entre 1 et 1296 (pt)+@msg	missing string `source`	chaîne `source` manquante+@msg	not a Typst object	ce n’est pas un objet Typst+@msg	object {_1} is not a Typst object	l’objet {_1} n’est pas un objet Typst+@msg	the Typst source is empty	la source Typst est vide+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	la source Typst dépasse {MAX_TYPST_SOURCE} octets+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	{key} dépasse {MAX_TYPST_SOURCE} octets+@msg	{key} must be a string, not {v}	{key} doit être une chaîne, pas {v} 	Water Paper…	Papier humide… 	Water Paper	Papier humide 	Ruby…	Ruby…diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv--- a/crates/ui-egui/src/i18n/hu.tsv+++ b/crates/ui-egui/src/i18n/hu.tsv@@ -5360,6 +5360,35 @@ imageTrace	Outline	Körvonal 	Plaster	Gipsz 	Note Paper…	Jegyzetpapír… 	Note Paper	Jegyzetpapír+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Typst beszúrása+	Edit Typst	Typst szerkesztése+	Typst Info	Typst-információ+	Preamble:	Preambulum:+	Enable Typst	Typst engedélyezése+	Runs the typst program, which downloads the @preview packages the markup imports.	Futtatja a typst programot, amely letölti a jelölés által importált @preview csomagokat.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	A Typst ki van kapcsolva: kapcsolja be a Typst engedélyezése beállítást itt: Beállítások › Szöveg+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	az automatizálás nem kapcsolhatja be a Typst engedélyezése beállítást: kapcsolja be itt: Beállítások › Szöveg+@msg	Typst needs the desktop app: the web app can't run the typst program	A Typsthez az asztali alkalmazás kell: a webes alkalmazás nem tudja futtatni a typst programot+@msg	Typst took longer than {_1} seconds	A Typst {_1} másodpercnél tovább futott+@msg	Typst's SVG is larger than {_1} MB	A Typst SVG-je nagyobb, mint {_1} MB+@msg	Typst's SVG: {e}	A Typst SVG-je: {e}+@msg	couldn't wait for `{program}`: {e}	nem sikerült megvárni a(z) `{program}` programot: {e}+@msg	select a Typst object	jelöljön ki egy Typst-objektumot+@msg	select one Typst object	csak egy Typst-objektumot jelöljön ki+@msg	the page is empty	az oldal üres+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	A Typst nincs telepítve: a(z) `{program}` nem található (telepítse a typst.app oldalról, vagy állítsa a VECTORCRAFT_TYPST változót az elérési útjára)+@msg	couldn't run `{program}`: {e}	nem sikerült futtatni a(z) `{program}` programot: {e}+@msg	at lies outside the canvas	az at a vásznon kívül esik+@msg	fontSize must be a number from 1 to 1296 (pt)	a fontSize 1 és 1296 (pt) közötti szám lehet+@msg	missing string `source`	hiányzik a `source` szöveg+@msg	not a Typst object	nem Typst-objektum+@msg	object {_1} is not a Typst object	az objektum nem Typst-objektum: {_1}+@msg	the Typst source is empty	a Typst-forrás üres+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	a Typst-forrás hosszabb {MAX_TYPST_SOURCE} bájtnál+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	a(z) {key} hosszabb {MAX_TYPST_SOURCE} bájtnál+@msg	{key} must be a string, not {v}	{key}: szöveg kell, ez nem az: {v} 	Water Paper…	Nedves papír… 	Water Paper	Nedves papír 	Ruby…	Ruby…diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv--- a/crates/ui-egui/src/i18n/id.tsv+++ b/crates/ui-egui/src/i18n/id.tsv@@ -5347,6 +5347,16 @@ swatch	Bright Violet	Violet cerah 	Plaster	Plester 	Note Paper…	Kertas Catatan… 	Note Paper	Kertas Catatan+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Sisipkan Typst+	Edit Typst	Edit Typst+	Typst Info	Info Typst+	Preamble:	Preambul:+	Enable Typst	Aktifkan Typst+	Runs the typst program, which downloads the @preview packages the markup imports.	Menjalankan program typst, yang mengunduh paket @preview yang diimpor markup.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst nonaktif: aktifkan Aktifkan Typst di Preferensi › Teks+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	otomatisasi tidak dapat mengaktifkan Aktifkan Typst: aktifkan di Preferensi › Teks 	Water Paper…	Kertas Basah… 	Water Paper	Kertas Basah 	Ruby…	Ruby…diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv--- a/crates/ui-egui/src/i18n/it.tsv+++ b/crates/ui-egui/src/i18n/it.tsv@@ -4989,6 +4989,35 @@ lighting	Light	Luce 	Plaster	Gesso 	Note Paper…	Carta da lettere… 	Note Paper	Carta da lettere+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Inserisci Typst+	Edit Typst	Modifica Typst+	Typst Info	Informazioni Typst+	Preamble:	Preambolo:+	Enable Typst	Abilita Typst+	Runs the typst program, which downloads the @preview packages the markup imports.	Esegue il programma typst, che scarica i pacchetti @preview importati dal markup.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst è disattivato: attiva Abilita Typst in Preferenze › Testo+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	l’automazione non può attivare Abilita Typst: attivalo in Preferenze › Testo+@msg	Typst needs the desktop app: the web app can't run the typst program	Typst richiede l'app desktop: l'app web non può eseguire il programma typst+@msg	Typst took longer than {_1} seconds	Typst ha impiegato più di {_1} secondi+@msg	Typst's SVG is larger than {_1} MB	L'SVG di Typst supera {_1} MB+@msg	Typst's SVG: {e}	SVG di Typst: {e}+@msg	couldn't wait for `{program}`: {e}	impossibile attendere `{program}`: {e}+@msg	select a Typst object	seleziona un oggetto Typst+@msg	select one Typst object	seleziona un solo oggetto Typst+@msg	the page is empty	la pagina è vuota+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	Typst non è installato: `{program}` non trovato (installalo da typst.app o imposta VECTORCRAFT_TYPST sul suo percorso)+@msg	couldn't run `{program}`: {e}	impossibile eseguire `{program}`: {e}+@msg	at lies outside the canvas	at è fuori dall'area di disegno+@msg	fontSize must be a number from 1 to 1296 (pt)	fontSize deve essere un numero da 1 a 1296 (pt)+@msg	missing string `source`	manca la stringa `source`+@msg	not a Typst object	non è un oggetto Typst+@msg	object {_1} is not a Typst object	l'oggetto {_1} non è un oggetto Typst+@msg	the Typst source is empty	il sorgente Typst è vuoto+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	il sorgente Typst supera {MAX_TYPST_SOURCE} byte+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	{key} supera {MAX_TYPST_SOURCE} byte+@msg	{key} must be a string, not {v}	{key} deve essere una stringa, non {v} 	Water Paper…	Carta bagnata… 	Water Paper	Carta bagnata 	Ruby…	Ruby…diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv--- a/crates/ui-egui/src/i18n/ja.tsv+++ b/crates/ui-egui/src/i18n/ja.tsv@@ -5029,6 +5029,35 @@ lighting	Light	ライト 	Plaster	石膏 	Note Paper…	ノート用紙… 	Note Paper	ノート用紙+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Typst を挿入+	Edit Typst	Typst を編集+	Typst Info	Typst 情報+	Preamble:	プリアンブル:+	Enable Typst	Typst を有効にする+	Runs the typst program, which downloads the @preview packages the markup imports.	typst プログラムを実行します。マークアップが読み込む @preview パッケージはダウンロードされます。+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst はオフです:環境設定 › 書式 で「Typst を有効にする」をオンにしてください+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	自動操作では「Typst を有効にする」をオンにできません:環境設定 › 書式 でオンにしてください+@msg	Typst needs the desktop app: the web app can't run the typst program	Typst にはデスクトップアプリが必要です: Web アプリでは typst プログラムを実行できません+@msg	Typst took longer than {_1} seconds	Typst の処理が {_1} 秒を超えました+@msg	Typst's SVG is larger than {_1} MB	Typst の SVG が {_1} MB を超えています+@msg	Typst's SVG: {e}	Typst の SVG: {e}+@msg	couldn't wait for `{program}`: {e}	`{program}` の終了を待てませんでした: {e}+@msg	select a Typst object	Typst オブジェクトを選択してください+@msg	select one Typst object	Typst オブジェクトを 1 つだけ選択してください+@msg	the page is empty	ページが空です+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	Typst がインストールされていません: `{program}` が見つかりません (typst.app からインストールするか、VECTORCRAFT_TYPST にパスを設定してください)+@msg	couldn't run `{program}`: {e}	`{program}` を実行できませんでした: {e}+@msg	at lies outside the canvas	at がカンバスの外にあります+@msg	fontSize must be a number from 1 to 1296 (pt)	fontSize は 1 から 1296 (pt) までの数値にしてください+@msg	missing string `source`	文字列 `source` が指定されていません+@msg	not a Typst object	Typst オブジェクトではありません+@msg	object {_1} is not a Typst object	オブジェクト {_1} は Typst オブジェクトではありません+@msg	the Typst source is empty	Typst のソースが空です+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	Typst のソースが {MAX_TYPST_SOURCE} バイトを超えています+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	{key} が {MAX_TYPST_SOURCE} バイトを超えています+@msg	{key} must be a string, not {v}	{key} は文字列にしてください ({v} は指定できません) 	Water Paper…	ぬれた紙… 	Water Paper	ぬれた紙 	Japanese Postcard	はがきdiff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv--- a/crates/ui-egui/src/i18n/ka.tsv+++ b/crates/ui-egui/src/i18n/ka.tsv@@ -5280,6 +5280,35 @@ imageTrace	Outline	კონტური 	Plaster	თაბაშირი 	Note Paper…	ჩანაწერების ქაღალდი… 	Note Paper	ჩანაწერების ქაღალდი+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Typst-ის ჩასმა+	Edit Typst	Typst-ის რედაქტირება+	Typst Info	Typst-ის ინფორმაცია+	Preamble:	პრეამბულა:+	Enable Typst	Typst-ის ჩართვა+	Runs the typst program, which downloads the @preview packages the markup imports.	უშვებს პროგრამას typst, რომელიც ჩამოტვირთავს მარკირების მიერ იმპორტირებულ @preview პაკეტებს.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst გამორთულია: ჩართეთ „Typst-ის ჩართვა“ აქ: პარამეტრები › ტექსტი+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	ავტომატიზაციას არ შეუძლია „Typst-ის ჩართვა“-ს ჩართვა: ჩართეთ აქ: პარამეტრები › ტექსტი+@msg	Typst needs the desktop app: the web app can't run the typst program	Typst-ს დესკტოპის აპი სჭირდება: ვებ-აპს typst პროგრამის გაშვება არ შეუძლია+@msg	Typst took longer than {_1} seconds	Typst-მა {_1} წამზე მეტი დრო მოითხოვა+@msg	Typst's SVG is larger than {_1} MB	Typst-ის SVG {_1} MB-ზე დიდია+@msg	Typst's SVG: {e}	Typst-ის SVG: {e}+@msg	couldn't wait for `{program}`: {e}	`{program}`-ის მოლოდინი ვერ მოხერხდა: {e}+@msg	select a Typst object	მონიშნეთ Typst-ის ობიექტი+@msg	select one Typst object	მონიშნეთ მხოლოდ ერთი Typst-ის ობიექტი+@msg	the page is empty	გვერდი ცარიელია+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	Typst არ არის დაყენებული: `{program}` ვერ მოიძებნა (დააყენეთ typst.app-დან ან VECTORCRAFT_TYPST-ში მიუთითეთ მისი მისამართი)+@msg	couldn't run `{program}`: {e}	`{program}`-ის გაშვება ვერ მოხერხდა: {e}+@msg	at lies outside the canvas	at ტილოს გარეთაა+@msg	fontSize must be a number from 1 to 1296 (pt)	fontSize უნდა იყოს რიცხვი 1-დან 1296-მდე (pt)+@msg	missing string `source`	აკლია სტრიქონი `source`+@msg	not a Typst object	Typst-ობიექტი არ არის+@msg	object {_1} is not a Typst object	ობიექტი {_1} Typst-ობიექტი არ არის+@msg	the Typst source is empty	Typst-ის წყარო ცარიელია+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	Typst-ის წყარო {MAX_TYPST_SOURCE} ბაიტზე გრძელია+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	{key} {MAX_TYPST_SOURCE} ბაიტზე გრძელია+@msg	{key} must be a string, not {v}	{key} უნდა იყოს სტრიქონი და არა {v} 	Water Paper…	სველი ქაღალდი… 	Water Paper	სველი ქაღალდი diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv--- a/crates/ui-egui/src/i18n/ko.tsv+++ b/crates/ui-egui/src/i18n/ko.tsv@@ -4952,6 +4952,35 @@ lighting	Light	광원 	Plaster	석고 	Note Paper…	메모지… 	Note Paper	메모지+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Typst 삽입+	Edit Typst	Typst 편집+	Typst Info	Typst 정보+	Preamble:	프리앰블:+	Enable Typst	Typst 사용+	Runs the typst program, which downloads the @preview packages the markup imports.	typst 프로그램을 실행하며, 마크업이 가져오는 @preview 패키지를 다운로드합니다.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst가 꺼져 있습니다. 환경 설정 › 문자에서 Typst 사용을 켜십시오+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	자동화로는 Typst 사용을 켤 수 없습니다. 환경 설정 › 문자에서 켜십시오+@msg	Typst needs the desktop app: the web app can't run the typst program	Typst에는 데스크톱 앱이 필요합니다: 웹 앱은 typst 프로그램을 실행할 수 없습니다+@msg	Typst took longer than {_1} seconds	Typst가 {_1}초 넘게 걸렸습니다+@msg	Typst's SVG is larger than {_1} MB	Typst의 SVG가 {_1}MB보다 큽니다+@msg	Typst's SVG: {e}	Typst의 SVG: {e}+@msg	couldn't wait for `{program}`: {e}	`{program}`을(를) 기다릴 수 없습니다: {e}+@msg	select a Typst object	Typst 개체를 선택하십시오+@msg	select one Typst object	Typst 개체를 하나만 선택하십시오+@msg	the page is empty	페이지가 비어 있습니다+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	Typst가 설치되어 있지 않습니다: `{program}`을(를) 찾을 수 없습니다(typst.app에서 설치하거나 VECTORCRAFT_TYPST에 경로를 설정하십시오)+@msg	couldn't run `{program}`: {e}	`{program}`을(를) 실행할 수 없습니다: {e}+@msg	at lies outside the canvas	at이(가) 캔버스 밖에 있습니다+@msg	fontSize must be a number from 1 to 1296 (pt)	fontSize는 1에서 1296 (pt) 사이의 숫자여야 합니다+@msg	missing string `source`	문자열 `source`가 없습니다+@msg	not a Typst object	Typst 개체가 아닙니다+@msg	object {_1} is not a Typst object	개체 {_1}은(는) Typst 개체가 아닙니다+@msg	the Typst source is empty	Typst 소스가 비어 있습니다+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	Typst 소스가 {MAX_TYPST_SOURCE}바이트보다 깁니다+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	{key}이(가) {MAX_TYPST_SOURCE}바이트보다 깁니다+@msg	{key} must be a string, not {v}	{key}은(는) 문자열이어야 합니다: {v} 	Water Paper…	물 종이… 	Water Paper	물 종이 	Ruby…	루비…diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv--- a/crates/ui-egui/src/i18n/nl.tsv+++ b/crates/ui-egui/src/i18n/nl.tsv@@ -5389,6 +5389,16 @@ swatch	Bright Violet	Felviolet 	Plaster	Gips 	Note Paper…	Notitiepapier… 	Note Paper	Notitiepapier+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Typst invoegen+	Edit Typst	Typst bewerken+	Typst Info	Typst-informatie+	Preamble:	Preambule:+	Enable Typst	Typst inschakelen+	Runs the typst program, which downloads the @preview packages the markup imports.	Voert het programma typst uit, dat de @preview-pakketten downloadt die de opmaak importeert.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst staat uit: schakel Typst inschakelen in bij Voorkeuren › Type+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	automatisering kan Typst inschakelen niet aanzetten: zet het aan bij Voorkeuren › Type 	Water Paper…	Nat papier… 	Water Paper	Nat papier 	Ruby…	Ruby…diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv--- a/crates/ui-egui/src/i18n/pl.tsv+++ b/crates/ui-egui/src/i18n/pl.tsv@@ -5226,6 +5226,35 @@ mark-style	Japanese	Japońskie 	Plaster	Gips 	Note Paper…	Papier listowy… 	Note Paper	Papier listowy+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Wstaw Typst+	Edit Typst	Edytuj Typst+	Typst Info	Informacje o Typst+	Preamble:	Preambuła:+	Enable Typst	Włącz Typst+	Runs the typst program, which downloads the @preview packages the markup imports.	Uruchamia program typst, który pobiera pakiety @preview importowane przez znaczniki.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst jest wyłączony: zaznacz Włącz Typst w Preferencje › Tekst+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	automatyzacja nie może włączyć opcji Włącz Typst: włącz ją w Preferencje › Tekst+@msg	Typst needs the desktop app: the web app can't run the typst program	Typst wymaga aplikacji komputerowej: aplikacja internetowa nie może uruchomić programu typst+@msg	Typst took longer than {_1} seconds	Typst działał dłużej niż {_1} s+@msg	Typst's SVG is larger than {_1} MB	SVG z Typsta jest większy niż {_1} MB+@msg	Typst's SVG: {e}	SVG z Typsta: {e}+@msg	couldn't wait for `{program}`: {e}	nie można zaczekać na `{program}`: {e}+@msg	select a Typst object	zaznacz obiekt Typst+@msg	select one Typst object	zaznacz tylko jeden obiekt Typst+@msg	the page is empty	strona jest pusta+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	Typst nie jest zainstalowany: nie znaleziono `{program}` (zainstaluj go z typst.app lub ustaw VECTORCRAFT_TYPST na jego ścieżkę)+@msg	couldn't run `{program}`: {e}	nie można uruchomić `{program}`: {e}+@msg	at lies outside the canvas	at leży poza płótnem+@msg	fontSize must be a number from 1 to 1296 (pt)	fontSize musi być liczbą od 1 do 1296 (pt)+@msg	missing string `source`	brak ciągu znaków `source`+@msg	not a Typst object	to nie jest obiekt Typst+@msg	object {_1} is not a Typst object	obiekt {_1} nie jest obiektem Typst+@msg	the Typst source is empty	źródło Typst jest puste+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	źródło Typst jest dłuższe niż {MAX_TYPST_SOURCE} bajtów+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	{key} jest dłuższy niż {MAX_TYPST_SOURCE} bajtów+@msg	{key} must be a string, not {v}	{key} musi być ciągiem znaków, nie {v} 	Water Paper…	Mokry papier… 	Water Paper	Mokry papier 	Ruby…	Ruby…diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv--- a/crates/ui-egui/src/i18n/pt-br.tsv+++ b/crates/ui-egui/src/i18n/pt-br.tsv@@ -5291,6 +5291,35 @@ mark-style	Japanese	Japonesas 	Plaster	Gesso 	Note Paper…	Papel de carta… 	Note Paper	Papel de carta+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Inserir Typst+	Edit Typst	Editar Typst+	Typst Info	Informações do Typst+	Preamble:	Preâmbulo:+	Enable Typst	Ativar Typst+	Runs the typst program, which downloads the @preview packages the markup imports.	Executa o programa typst, que baixa os pacotes @preview importados pela marcação.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	O Typst está desativado: ative Ativar Typst em Preferências › Texto+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	a automação não pode ativar Ativar Typst: ative-o em Preferências › Texto+@msg	Typst needs the desktop app: the web app can't run the typst program	O Typst precisa do aplicativo para desktop: o aplicativo web não pode executar o programa typst+@msg	Typst took longer than {_1} seconds	O Typst levou mais de {_1} segundos+@msg	Typst's SVG is larger than {_1} MB	O SVG do Typst tem mais de {_1} MB+@msg	Typst's SVG: {e}	SVG do Typst: {e}+@msg	couldn't wait for `{program}`: {e}	não foi possível aguardar `{program}`: {e}+@msg	select a Typst object	selecione um objeto Typst+@msg	select one Typst object	selecione um único objeto Typst+@msg	the page is empty	a página está vazia+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	O Typst não está instalado: `{program}` não foi encontrado (instale-o de typst.app ou defina VECTORCRAFT_TYPST com o caminho dele)+@msg	couldn't run `{program}`: {e}	não foi possível executar `{program}`: {e}+@msg	at lies outside the canvas	at está fora da tela+@msg	fontSize must be a number from 1 to 1296 (pt)	fontSize deve ser um número de 1 a 1296 (pt)+@msg	missing string `source`	falta a string `source`+@msg	not a Typst object	não é um objeto Typst+@msg	object {_1} is not a Typst object	o objeto {_1} não é um objeto Typst+@msg	the Typst source is empty	o código Typst está vazio+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	o código Typst tem mais de {MAX_TYPST_SOURCE} bytes+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	{key} tem mais de {MAX_TYPST_SOURCE} bytes+@msg	{key} must be a string, not {v}	{key} deve ser uma string, não {v} 	Water Paper…	Papel molhado… 	Water Paper	Papel molhado 	Ruby…	Ruby…diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv--- a/crates/ui-egui/src/i18n/ru.tsv+++ b/crates/ui-egui/src/i18n/ru.tsv@@ -4985,6 +4985,35 @@ lighting	Light	Свет 	Plaster	Гипс 	Note Paper…	Почтовая бумага… 	Note Paper	Почтовая бумага+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Вставить Typst+	Edit Typst	Изменить Typst+	Typst Info	Сведения о Typst+	Preamble:	Преамбула:+	Enable Typst	Включить Typst+	Runs the typst program, which downloads the @preview packages the markup imports.	Запускает программу typst, которая загружает пакеты @preview, импортируемые разметкой.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst выключен: включите «Включить Typst» в Настройки › Текст+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	автоматизация не может включить «Включить Typst»: включите его в Настройки › Текст+@msg	Typst needs the desktop app: the web app can't run the typst program	Для Typst нужно настольное приложение: веб-приложение не может запустить программу typst+@msg	Typst took longer than {_1} seconds	Typst работал дольше {_1} с+@msg	Typst's SVG is larger than {_1} MB	SVG от Typst больше {_1} МБ+@msg	Typst's SVG: {e}	SVG от Typst: {e}+@msg	couldn't wait for `{program}`: {e}	не удалось дождаться `{program}`: {e}+@msg	select a Typst object	выделите объект Typst+@msg	select one Typst object	выделите только один объект Typst+@msg	the page is empty	страница пуста+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	Typst не установлен: `{program}` не найден (установите его с typst.app или укажите путь к нему в VECTORCRAFT_TYPST)+@msg	couldn't run `{program}`: {e}	не удалось запустить `{program}`: {e}+@msg	at lies outside the canvas	at находится вне холста+@msg	fontSize must be a number from 1 to 1296 (pt)	fontSize должно быть числом от 1 до 1296 (пт)+@msg	missing string `source`	отсутствует строка `source`+@msg	not a Typst object	это не объект Typst+@msg	object {_1} is not a Typst object	объект {_1} не является объектом Typst+@msg	the Typst source is empty	исходный текст Typst пуст+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	исходный текст Typst длиннее {MAX_TYPST_SOURCE} байт+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	{key} длиннее {MAX_TYPST_SOURCE} байт+@msg	{key} must be a string, not {v}	{key} должно быть строкой, а не {v} 	Water Paper…	Мокрая бумага… 	Water Paper	Мокрая бумага 	Ruby…	Руби…diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv--- a/crates/ui-egui/src/i18n/sv.tsv+++ b/crates/ui-egui/src/i18n/sv.tsv@@ -5330,6 +5330,35 @@ theme	Appearance	Utseende 	Plaster	Gips 	Note Paper…	Anteckningspapper… 	Note Paper	Anteckningspapper+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Infoga Typst+	Edit Typst	Redigera Typst+	Typst Info	Typst-information+	Preamble:	Preambel:+	Enable Typst	Aktivera Typst+	Runs the typst program, which downloads the @preview packages the markup imports.	Kör programmet typst, som hämtar de @preview-paket som märkspråket importerar.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst är avstängt: slå på Aktivera Typst under Inställningar › Typ+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	automatisering kan inte slå på Aktivera Typst: slå på det under Inställningar › Typ+@msg	Typst needs the desktop app: the web app can't run the typst program	Typst kräver skrivbordsappen: webbappen kan inte köra programmet typst+@msg	Typst took longer than {_1} seconds	Typst tog längre tid än {_1} sekunder+@msg	Typst's SVG is larger than {_1} MB	Typsts SVG är större än {_1} MB+@msg	Typst's SVG: {e}	Typsts SVG: {e}+@msg	couldn't wait for `{program}`: {e}	kunde inte vänta på `{program}`: {e}+@msg	select a Typst object	markera ett Typst-objekt+@msg	select one Typst object	markera bara ett Typst-objekt+@msg	the page is empty	sidan är tom+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	Typst är inte installerat: `{program}` hittades inte (installera det från typst.app eller ange dess sökväg i VECTORCRAFT_TYPST)+@msg	couldn't run `{program}`: {e}	kunde inte köra `{program}`: {e}+@msg	at lies outside the canvas	at ligger utanför arbetsytan+@msg	fontSize must be a number from 1 to 1296 (pt)	fontSize måste vara ett tal från 1 till 1296 (pt)+@msg	missing string `source`	strängen `source` saknas+@msg	not a Typst object	inte ett Typst-objekt+@msg	object {_1} is not a Typst object	objekt {_1} är inte ett Typst-objekt+@msg	the Typst source is empty	Typst-källan är tom+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	Typst-källan är längre än {MAX_TYPST_SOURCE} byte+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	{key} är längre än {MAX_TYPST_SOURCE} byte+@msg	{key} must be a string, not {v}	{key} måste vara en sträng, inte {v} 	Water Paper…	Vått papper… 	Water Paper	Vått papper 	Ruby…	Ruby…diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv--- a/crates/ui-egui/src/i18n/uk.tsv+++ b/crates/ui-egui/src/i18n/uk.tsv@@ -4969,6 +4969,35 @@ lighting	Light	Світло 	Plaster	Гіпс 	Note Paper…	Поштовий папір… 	Note Paper	Поштовий папір+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Вставити Typst+	Edit Typst	Редагувати Typst+	Typst Info	Відомості про Typst+	Preamble:	Преамбула:+	Enable Typst	Увімкнути Typst+	Runs the typst program, which downloads the @preview packages the markup imports.	Запускає програму typst, яка завантажує пакети @preview, що їх імпортує розмітка.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst вимкнено: увімкніть «Увімкнути Typst» у Налаштування › Текст+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	автоматизація не може ввімкнути «Увімкнути Typst»: увімкніть його в Налаштування › Текст+@msg	Typst needs the desktop app: the web app can't run the typst program	Для Typst потрібна настільна програма: вебпрограма не може запустити програму typst+@msg	Typst took longer than {_1} seconds	Typst працював довше за {_1} с+@msg	Typst's SVG is larger than {_1} MB	SVG від Typst більший за {_1} МБ+@msg	Typst's SVG: {e}	SVG від Typst: {e}+@msg	couldn't wait for `{program}`: {e}	не вдалося дочекатися `{program}`: {e}+@msg	select a Typst object	виділіть об’єкт Typst+@msg	select one Typst object	виділіть лише один об’єкт Typst+@msg	the page is empty	сторінка порожня+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	Typst не встановлено: `{program}` не знайдено (установіть його з typst.app або вкажіть шлях до нього у VECTORCRAFT_TYPST)+@msg	couldn't run `{program}`: {e}	не вдалося запустити `{program}`: {e}+@msg	at lies outside the canvas	at розташовано поза полотном+@msg	fontSize must be a number from 1 to 1296 (pt)	fontSize має бути числом від 1 до 1296 (пт)+@msg	missing string `source`	відсутній рядок `source`+@msg	not a Typst object	це не об’єкт Typst+@msg	object {_1} is not a Typst object	об’єкт {_1} не є об’єктом Typst+@msg	the Typst source is empty	вихідний текст Typst порожній+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	вихідний текст Typst довший за {MAX_TYPST_SOURCE} байт+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	{key} довший за {MAX_TYPST_SOURCE} байт+@msg	{key} must be a string, not {v}	{key} має бути рядком, а не {v} 	Water Paper…	Мокрий папір… 	Water Paper	Мокрий папір 	Ruby…	Рубі…diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv--- a/crates/ui-egui/src/i18n/vi.tsv+++ b/crates/ui-egui/src/i18n/vi.tsv@@ -4969,6 +4969,35 @@ theme	Appearance	Giao diện 	Plaster	Thạch cao 	Note Paper…	Giấy ghi chú… 	Note Paper	Giấy ghi chú+	Typst…	Typst…+	Typst	Typst+	Insert Typst	Chèn Typst+	Edit Typst	Sửa Typst+	Typst Info	Thông tin Typst+	Preamble:	Phần mở đầu:+	Enable Typst	Bật Typst+	Runs the typst program, which downloads the @preview packages the markup imports.	Chạy chương trình typst, chương trình này tải xuống các gói @preview mà mã đánh dấu nhập.+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst đang tắt: hãy bật Bật Typst trong Tùy chọn › Chữ+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	tự động hóa không thể bật Bật Typst: hãy bật trong Tùy chọn › Chữ+@msg	Typst needs the desktop app: the web app can't run the typst program	Typst cần ứng dụng máy tính: ứng dụng web không thể chạy chương trình typst+@msg	Typst took longer than {_1} seconds	Typst mất hơn {_1} giây+@msg	Typst's SVG is larger than {_1} MB	SVG của Typst lớn hơn {_1} MB+@msg	Typst's SVG: {e}	SVG của Typst: {e}+@msg	couldn't wait for `{program}`: {e}	không thể chờ `{program}`: {e}+@msg	select a Typst object	hãy chọn một đối tượng Typst+@msg	select one Typst object	chỉ chọn một đối tượng Typst+@msg	the page is empty	trang trống+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	Typst chưa được cài đặt: không tìm thấy `{program}` (hãy cài đặt từ typst.app hoặc đặt VECTORCRAFT_TYPST thành đường dẫn của nó)+@msg	couldn't run `{program}`: {e}	không thể chạy `{program}`: {e}+@msg	at lies outside the canvas	at nằm ngoài vùng vẽ+@msg	fontSize must be a number from 1 to 1296 (pt)	fontSize phải là số từ 1 đến 1296 (pt)+@msg	missing string `source`	thiếu chuỗi `source`+@msg	not a Typst object	không phải đối tượng Typst+@msg	object {_1} is not a Typst object	đối tượng {_1} không phải đối tượng Typst+@msg	the Typst source is empty	mã nguồn Typst trống+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	mã nguồn Typst dài hơn {MAX_TYPST_SOURCE} byte+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	{key} dài hơn {MAX_TYPST_SOURCE} byte+@msg	{key} must be a string, not {v}	{key} phải là một chuỗi, không phải {v} 	Water Paper…	Giấy ướt… 	Water Paper	Giấy ướt 	Ruby…	Ruby…diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv--- a/crates/ui-egui/src/i18n/zh-hans.tsv+++ b/crates/ui-egui/src/i18n/zh-hans.tsv@@ -5213,6 +5213,35 @@ lighting	Light	光源 	Plaster	塑料效果 	Note Paper…	便条纸… 	Note Paper	便条纸+	Typst…	Typst…+	Typst	Typst+	Insert Typst	插入 Typst+	Edit Typst	编辑 Typst+	Typst Info	Typst 信息+	Preamble:	导言区:+	Enable Typst	启用 Typst+	Runs the typst program, which downloads the @preview packages the markup imports.	运行 typst 程序,它会下载标记导入的 @preview 包。+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst 已关闭:请在 首选项 › 文字 中开启“启用 Typst”+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	自动化无法开启“启用 Typst”:请在 首选项 › 文字 中开启+@msg	Typst needs the desktop app: the web app can't run the typst program	Typst 需要桌面应用:网页应用无法运行 typst 程序+@msg	Typst took longer than {_1} seconds	Typst 运行超过 {_1} 秒+@msg	Typst's SVG is larger than {_1} MB	Typst 的 SVG 大于 {_1} MB+@msg	Typst's SVG: {e}	Typst 的 SVG:{e}+@msg	couldn't wait for `{program}`: {e}	无法等待 `{program}`:{e}+@msg	select a Typst object	请选择一个 Typst 对象+@msg	select one Typst object	请只选择一个 Typst 对象+@msg	the page is empty	页面为空+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	未安装 Typst:找不到 `{program}`(请从 typst.app 安装,或将 VECTORCRAFT_TYPST 设为其路径)+@msg	couldn't run `{program}`: {e}	无法运行 `{program}`:{e}+@msg	at lies outside the canvas	at 在画布之外+@msg	fontSize must be a number from 1 to 1296 (pt)	fontSize 必须是 1 到 1296 (pt) 之间的数字+@msg	missing string `source`	缺少字符串 `source`+@msg	not a Typst object	不是 Typst 对象+@msg	object {_1} is not a Typst object	对象 {_1} 不是 Typst 对象+@msg	the Typst source is empty	Typst 源码为空+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	Typst 源码超过 {MAX_TYPST_SOURCE} 字节+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	{key} 超过 {MAX_TYPST_SOURCE} 字节+@msg	{key} must be a string, not {v}	{key} 必须是字符串,而不是 {v} 	Water Paper…	水彩画纸… 	Water Paper	水彩画纸 	Ruby…	注音…diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv--- a/crates/ui-egui/src/i18n/zh-hant.tsv+++ b/crates/ui-egui/src/i18n/zh-hant.tsv@@ -5008,6 +5008,35 @@ lighting	Light	光源 	Plaster	塑膠效果 	Note Paper…	便條紙… 	Note Paper	便條紙+	Typst…	Typst…+	Typst	Typst+	Insert Typst	插入 Typst+	Edit Typst	編輯 Typst+	Typst Info	Typst 資訊+	Preamble:	導言區:+	Enable Typst	啟用 Typst+	Runs the typst program, which downloads the @preview packages the markup imports.	執行 typst 程式,它會下載標記匯入的 @preview 套件。+@msg	Typst is off: turn on Enable Typst in Preferences › Type	Typst 已關閉:請在 偏好設定 › 文字 中開啟「啟用 Typst」+@msg	automation can't turn on Enable Typst: turn it on in Preferences › Type	自動化無法開啟「啟用 Typst」:請在 偏好設定 › 文字 中開啟+@msg	Typst needs the desktop app: the web app can't run the typst program	Typst 需要桌面版應用程式:網頁版應用程式無法執行 typst 程式+@msg	Typst took longer than {_1} seconds	Typst 執行超過 {_1} 秒+@msg	Typst's SVG is larger than {_1} MB	Typst 的 SVG 大於 {_1} MB+@msg	Typst's SVG: {e}	Typst 的 SVG:{e}+@msg	couldn't wait for `{program}`: {e}	無法等待 `{program}`:{e}+@msg	select a Typst object	請選取一個 Typst 物件+@msg	select one Typst object	請只選取一個 Typst 物件+@msg	the page is empty	頁面是空的+@msg	Typst isn't installed: `{program}` wasn't found (install it from typst.app, or set VECTORCRAFT_TYPST to its path)	未安裝 Typst:找不到 `{program}`(請從 typst.app 安裝,或將 VECTORCRAFT_TYPST 設為其路徑)+@msg	couldn't run `{program}`: {e}	無法執行 `{program}`:{e}+@msg	at lies outside the canvas	at 在畫布之外+@msg	fontSize must be a number from 1 to 1296 (pt)	fontSize 必須是 1 到 1296 (pt) 之間的數字+@msg	missing string `source`	未提供字串 `source`+@msg	not a Typst object	不是 Typst 物件+@msg	object {_1} is not a Typst object	物件 {_1} 不是 Typst 物件+@msg	the Typst source is empty	Typst 原始碼是空的+@msg	the Typst source is longer than {MAX_TYPST_SOURCE} bytes	Typst 原始碼超過 {MAX_TYPST_SOURCE} 位元組+@msg	{key} is longer than {MAX_TYPST_SOURCE} bytes	{key} 超過 {MAX_TYPST_SOURCE} 位元組+@msg	{key} must be a string, not {v}	{key} 必須是字串,而非 {v} 	Water Paper…	水彩畫紙… 	Water Paper	水彩畫紙 	Ruby…	注音…diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs--- a/crates/ui-egui/src/menus.rs+++ b/crates/ui-egui/src/menus.rs@@ -1144,6 +1144,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[         "",         "{} open Blend Options (Object › Blend › Blend Options…, the Blend tool's double-click, Alt-click and toolbar button; dialog `blendOptions`: spacing smooth|steps|distance, steps, distance (pt), orientation page|path, preview) on the selected blend's options, previewed live: OK runs object.blend.options as one undo step; with no blend selected it sets what new blends start with",     ),+    (+        "ui.typst",+        "Typst…",+        "",+        "{} open Type › Typst… (dialog `typst`: source, preamble, fontSize (pt)) on the selected Typst object's source, else to insert new Typst markup: OK runs type.typst.edit or type.typst.insert, and on a Typst error the dialog stays open showing it (field __error)",+    ),     (         "ui.livePaintGapOptions",         "Gap Options…",@@ -1852,6 +1858,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R         },         "ui.blendOptions" => crate::dialogs::blend_options::open(app),         "ui.livePaintGapOptions" => crate::dialogs::live_paint_gaps::open(app),+        "ui.typst" => crate::dialogs::typst::open(app),         "ui.perspectivePlane" => crate::dialogs::perspective_plane::open(app, p),         _ => return None,     };@@ -2908,6 +2915,7 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {                 Sep,                 c("Fill with Placeholder Text", "type.fillPlaceholder"),                 c("Insert Inline Symbol", "text.insertInline"),+                c("Typst…", "ui.typst"),                 Sep,                 c("Show Hidden Characters", "type.hiddenCharacters"),                 sub("Type Orientation", vec![c("Horizontal", "type.orientation.horizontal"), c("Vertical", "type.orientation.vertical")]),diff --git a/docs/typst.md b/docs/typst.mdnew file mode 100644--- /dev/null+++ b/docs/typst.md@@ -0,0 +1,70 @@+# Typst++Type › **Typst…** typesets [Typst](https://typst.app) markup (math, text, tables, anything Typst draws) and places+it as one group of paths that keeps its source. Select the group and choose Type › Typst… again to change the+source: it is typeset again where it was, at the size and angle you gave it. Like TexText in Inkscape, but for+Typst. It isn't an Illustrator feature.++## Using it++Typst is **off by default**: turn on **Enable Typst** in Preferences › Type, or in the dialog. Until then+nothing is typeset, so a document, the control channel or MCP can't make the app run `typst`. Automation+confined with `--automation-read-root` can't turn it on (`prefs.set` refuses), only off.++- **Text**: the Typst markup. Write math between dollar signs (`$ sum_(k=1)^n k = (n(n+1))/2 $`) or ordinary+  Typst markup (`#emph[hello]`, `#table(…)`).+- **Preamble**: markup read before the text, for `#set`, `#show`, `#import` and `#let` rules, such as+  `#set text(font: "New Computer Modern", fill: blue)`.+- **Size**: the text size the markup starts with (pt; 11 by default, as in Typst).++OK typesets. When Typst reports an error, the dialog stays open and shows it, with its line counted in the text+or in the preamble (`line 2:4: error: expected expression`).++The page is as big as what it draws, with no margins and no fill; only the first page is placed. Glyphs come in+as plain filled paths (no fonts are needed to open, print or export the document), in the colours the markup+gives them. The group can be moved, scaled, rotated and recoloured like any other. Editing it typesets it+again: its centre stays where it is, with its scale and rotation, and the group keeps its own name,+appearance, opacity, opacity mask, attributes and graphic style, but colours and other changes to the paths+inside are replaced by what Typst draws. Ungroup it to keep the paths as ordinary art.++The source is saved in the VectorCraft document (`.vectorcraft`). Formats that only carry art (SVG, PDF, EPS…)+export the paths.++## Requirements++- The **desktop app** with Typst installed: it runs the `typst` program on the `PATH` (tested with Typst 0.15), or the+  one the `VECTORCRAFT_TYPST` environment variable names. The web app can't run programs, so there the command+  explains that.+- Typst finds fonts as it always does: the fonts it embeds, the system's, and `TYPST_FONT_PATHS`.+- **Network:** `#import "@preview/…"` packages are downloaded by Typst from its package registry the first+  time they are used, as on the command line, and cached by Typst. The app doesn't make or check that+  download, and Typst has no setting to turn it off; the dialog says so next to Enable Typst.+- `#image` and `#read` paths are relative to the document's folder (for a document not yet saved, a private+  empty folder made for the run, under a random name, and removed after it). Typst can't read outside it,+  except through links in it. When automation is confined (`--automation-read-root`), Typst always gets the+  empty folder, since a link in the document's folder could lead outside the read root.+- Typst gets 30 seconds per typesetting.++## Commands++| Command | Params | Result |+|---|---|---|+| `type.typst.insert` | `{source, preamble?, fontSize?, at?: [x, y]}` | `{id, width, height}`: the group, selected, centred on `at` (default: the first artboard's centre) |+| `type.typst.edit` | `{id?, source?, preamble?, fontSize?}` (what isn't given stays) | `{id, width, height}` (the edited group has a new id) |+| `type.typst.info` | `{id?}` | `{id, source, preamble, fontSize}` |+| `ui.typst` | `{}` | opens the dialog (`typst`: fields `source`, `preamble`, `fontSize`) |++`id` defaults to the selected Typst object (an object selected inside one counts); with two or more selected,+`ui.typst` and the commands ask to select one. Insert and edit need `enableTypst` on (`prefs.set`); info doesn't. Each insert or edit is one+undo step. The program is never chosen by command params, so the control channel and MCP can't make the app run+another one.++## How it works++The markup is wrapped with `#set page(width: auto, height: auto, margin: 0pt, fill: none)` and the text size,+piped to `typst compile --format svg --pages 1 - -`, and the SVG read back with the SVG importer+(`crates/engine/src/cmd/typst.rs`). Typst draws each glyph as a `<use>` of an outline; those instances are+broken into paths, and the one-object groups around them dropped. The group stores a `TypstSpec`+(`crates/doc/src/typst.rs`): the source, preamble and size, the typeset page, and the transform from the page+to the document, which every transform of the group updates. An edit places the new page through that transform,+centred where the old page was.