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
@@ -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 |@@ -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; @@ -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); }new file mode 100644@@ -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;+ }+}@@ -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 };@@ -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 }) }@@ -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() {new file mode 100644@@ -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));+ }+ }+}@@ -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;new file mode 100644@@ -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);+}@@ -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,new file mode 100644@@ -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());+ }+}@@ -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… Руби…@@ -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…@@ -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…@@ -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…@@ -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…@@ -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…@@ -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…@@ -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…@@ -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…@@ -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…@@ -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…@@ -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 はがき@@ -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 სველი ქაღალდი @@ -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… 루비…@@ -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…@@ -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…@@ -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…@@ -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… Руби…@@ -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…@@ -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… Рубі…@@ -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…@@ -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… 注音…@@ -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… 注音…@@ -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")]),new file mode 100644@@ -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.