typst 0.0.5 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,131 @@
1
+ use comemo::Track;
2
+ use ecow::{eco_format, EcoString};
3
+ use serde::Serialize;
4
+ use typst::diag::{bail, StrResult, Warned};
5
+ use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
6
+ use typst::layout::PagedDocument;
7
+ use typst::syntax::Span;
8
+ use typst::World;
9
+ use typst_eval::{eval_string, EvalMode};
10
+
11
+ use crate::world::SystemWorld;
12
+
13
+ /// Processes an input file to extract provided metadata
14
+ #[derive(Debug, Clone)]
15
+ pub struct QueryCommand {
16
+ /// Defines which elements to retrieve
17
+ pub selector: String,
18
+
19
+ /// Extracts just one field from all retrieved elements
20
+ pub field: Option<String>,
21
+
22
+ /// Expects and retrieves exactly one element
23
+ pub one: bool,
24
+
25
+ /// The format to serialize in
26
+ pub format: SerializationFormat,
27
+ }
28
+
29
+ // Output file format for query command
30
+ #[derive(Debug, Copy, Clone, Eq, PartialEq)]
31
+ pub enum SerializationFormat {
32
+ Json,
33
+ Yaml,
34
+ }
35
+
36
+ /// Execute a query command.
37
+ pub fn query(world: &mut SystemWorld, command: &QueryCommand) -> StrResult<String> {
38
+ // Reset everything and ensure that the main file is present.
39
+ world.reset();
40
+ world.source(world.main()).map_err(|err| err.to_string())?;
41
+
42
+ let Warned { output, warnings } = typst::compile(world);
43
+
44
+ match output {
45
+ // Retrieve and print query results.
46
+ Ok(document) => {
47
+ let data = retrieve(world, command, &document)?;
48
+ let serialized = format(data, command)?;
49
+ Ok(serialized)
50
+ }
51
+ // Print errors and warnings.
52
+ Err(errors) => {
53
+ let mut message = EcoString::from("failed to compile document");
54
+ for (i, error) in errors.into_iter().enumerate() {
55
+ message.push_str(if i == 0 { ": " } else { ", " });
56
+ message.push_str(&error.message);
57
+ }
58
+ for warning in warnings {
59
+ message.push_str(": ");
60
+ message.push_str(&warning.message);
61
+ }
62
+ Err(message)
63
+ }
64
+ }
65
+ }
66
+
67
+ /// Retrieve the matches for the selector.
68
+ fn retrieve(
69
+ world: &dyn World,
70
+ command: &QueryCommand,
71
+ document: &PagedDocument,
72
+ ) -> StrResult<Vec<Content>> {
73
+ let selector = eval_string(
74
+ &typst::ROUTINES,
75
+ world.track(),
76
+ &command.selector,
77
+ Span::detached(),
78
+ EvalMode::Code,
79
+ Scope::default(),
80
+ )
81
+ .map_err(|errors| {
82
+ let mut message = EcoString::from("failed to evaluate selector");
83
+ for (i, error) in errors.into_iter().enumerate() {
84
+ message.push_str(if i == 0 { ": " } else { ", " });
85
+ message.push_str(&error.message);
86
+ }
87
+ message
88
+ })?
89
+ .cast::<LocatableSelector>()
90
+ .map_err(|e| e.message().clone())?;
91
+
92
+ Ok(document
93
+ .introspector
94
+ .query(&selector.0)
95
+ .into_iter()
96
+ .collect::<Vec<_>>())
97
+ }
98
+
99
+ /// Format the query result in the output format.
100
+ fn format(elements: Vec<Content>, command: &QueryCommand) -> StrResult<String> {
101
+ if command.one && elements.len() != 1 {
102
+ bail!("expected exactly one element, found {}", elements.len());
103
+ }
104
+
105
+ let mapped: Vec<_> = elements
106
+ .into_iter()
107
+ .filter_map(|c| match &command.field {
108
+ Some(field) => c.get_by_name(field).ok(),
109
+ _ => Some(c.into_value()),
110
+ })
111
+ .collect();
112
+
113
+ if command.one {
114
+ let Some(value) = mapped.first() else {
115
+ bail!("no such field found for element");
116
+ };
117
+ serialize(value, command.format)
118
+ } else {
119
+ serialize(&mapped, command.format)
120
+ }
121
+ }
122
+
123
+ /// Serialize data to the output format.
124
+ fn serialize(data: &impl Serialize, format: SerializationFormat) -> StrResult<String> {
125
+ match format {
126
+ SerializationFormat::Json => {
127
+ serde_json::to_string_pretty(data).map_err(|e| eco_format!("{e}"))
128
+ }
129
+ SerializationFormat::Yaml => serde_yaml::to_string(&data).map_err(|e| eco_format!("{e}")),
130
+ }
131
+ }
@@ -1,24 +1,23 @@
1
- use std::cell::{Cell, OnceCell, RefCell, RefMut};
2
- use std::collections::HashMap;
3
1
  use std::fs;
4
- use std::hash::Hash;
5
2
  use std::path::{Path, PathBuf};
6
-
7
- use log::{debug};
3
+ use std::sync::{Mutex, OnceLock};
8
4
 
9
5
  use chrono::{DateTime, Datelike, Local};
10
- use comemo::Prehashed;
11
- use filetime::FileTime;
12
- use same_file::Handle;
13
- use siphasher::sip128::{Hasher128, SipHasher13};
6
+ use ecow::eco_format;
14
7
  use typst::diag::{FileError, FileResult, StrResult};
15
- use typst::eval::{eco_format, Bytes, Datetime, Library};
16
- use typst::font::{Font, FontBook, FontVariant};
8
+ use typst::foundations::{Bytes, Datetime, Dict};
17
9
  use typst::syntax::{FileId, Source, VirtualPath};
18
- use typst::World;
10
+ use typst::text::{Font, FontBook};
11
+ use typst::utils::LazyHash;
12
+ use typst::{Features, Library, LibraryBuilder, World};
13
+ use typst_kit::{
14
+ fonts::{FontSearcher, FontSlot},
15
+ package::PackageStorage,
16
+ };
17
+
18
+ use std::collections::HashMap;
19
19
 
20
- use crate::fonts::{FontSearcher, FontSlot};
21
- use crate::package::prepare_package;
20
+ use crate::download::SlientDownload;
22
21
 
23
22
  /// A world that provides access to the operating system.
24
23
  pub struct SystemWorld {
@@ -31,41 +30,39 @@ pub struct SystemWorld {
31
30
  /// The input path.
32
31
  main: FileId,
33
32
  /// Typst's standard library.
34
- library: Prehashed<Library>,
33
+ library: LazyHash<Library>,
35
34
  /// Metadata about discovered fonts.
36
- book: Prehashed<FontBook>,
35
+ book: LazyHash<FontBook>,
37
36
  /// Locations of and storage for lazily loaded fonts.
38
37
  fonts: Vec<FontSlot>,
39
- /// Maps package-path combinations to canonical hashes. All package-path
40
- /// combinations that point to the same file are mapped to the same hash. To
41
- /// be used in conjunction with `paths`.
42
- hashes: RefCell<HashMap<FileId, FileResult<PathHash>>>,
43
- /// Maps canonical path hashes to source files and buffers.
44
- slots: RefCell<HashMap<PathHash, PathSlot>>,
38
+ /// Maps file ids to source files and buffers.
39
+ slots: Mutex<HashMap<FileId, FileSlot>>,
40
+ /// Holds information about where packages are stored.
41
+ package_storage: PackageStorage,
45
42
  /// The current datetime if requested. This is stored here to ensure it is
46
43
  /// always the same within one compilation. Reset between compilations.
47
- now: OnceCell<DateTime<Local>>,
44
+ now: OnceLock<DateTime<Local>>,
48
45
  }
49
46
 
50
47
  impl World for SystemWorld {
51
- fn library(&self) -> &Prehashed<Library> {
48
+ fn library(&self) -> &LazyHash<Library> {
52
49
  &self.library
53
50
  }
54
51
 
55
- fn book(&self) -> &Prehashed<FontBook> {
52
+ fn book(&self) -> &LazyHash<FontBook> {
56
53
  &self.book
57
54
  }
58
55
 
59
- fn main(&self) -> Source {
60
- self.source(self.main).unwrap()
56
+ fn main(&self) -> FileId {
57
+ self.main
61
58
  }
62
59
 
63
60
  fn source(&self, id: FileId) -> FileResult<Source> {
64
- self.slot(id)?.source()
61
+ self.slot(id, |slot| slot.source(&self.root, &self.package_storage))
65
62
  }
66
63
 
67
64
  fn file(&self, id: FileId) -> FileResult<Bytes> {
68
- self.slot(id)?.file()
65
+ self.slot(id, |slot| slot.file(&self.root, &self.package_storage))
69
66
  }
70
67
 
71
68
  fn font(&self, index: usize) -> Option<Font> {
@@ -94,34 +91,12 @@ impl SystemWorld {
94
91
  }
95
92
 
96
93
  /// Access the canonical slot for the given file id.
97
- fn slot(&self, id: FileId) -> FileResult<RefMut<PathSlot>> {
98
- let mut system_path = PathBuf::new();
99
- let hash = self
100
- .hashes
101
- .borrow_mut()
102
- .entry(id)
103
- .or_insert_with(|| {
104
- // Determine the root path relative to which the file path
105
- // will be resolved.
106
- let buf;
107
- let mut root = &self.root;
108
- if let Some(spec) = id.package() {
109
- buf = prepare_package(spec)?;
110
- root = &buf;
111
- }
112
- // Join the path to the root. If it tries to escape, deny
113
- // access. Note: It can still escape via symlinks.
114
- system_path = id.vpath().resolve(root).ok_or(FileError::AccessDenied)?;
115
-
116
- PathHash::new(&system_path)
117
- })
118
- .clone()?;
119
-
120
- Ok(RefMut::map(self.slots.borrow_mut(), |paths| {
121
- paths
122
- .entry(hash)
123
- .or_insert_with(|| PathSlot::new(id, system_path))
124
- }))
94
+ fn slot<F, T>(&self, id: FileId, f: F) -> T
95
+ where
96
+ F: FnOnce(&mut FileSlot) -> T,
97
+ {
98
+ let mut map = self.slots.lock().unwrap();
99
+ f(map.entry(id).or_insert_with(|| FileSlot::new(id)))
125
100
  }
126
101
 
127
102
  /// The id of the main source file.
@@ -141,8 +116,7 @@ impl SystemWorld {
141
116
 
142
117
  /// Reset the compilation state in preparation of a new compilation.
143
118
  pub fn reset(&mut self) {
144
- self.hashes.borrow_mut().clear();
145
- for slot in self.slots.borrow_mut().values_mut() {
119
+ for slot in self.slots.lock().unwrap().values_mut() {
146
120
  slot.reset();
147
121
  }
148
122
  self.now.take();
@@ -165,7 +139,9 @@ pub struct SystemWorldBuilder {
165
139
  root: PathBuf,
166
140
  main: PathBuf,
167
141
  font_paths: Vec<PathBuf>,
168
- font_files: Vec<PathBuf>,
142
+ ignore_system_fonts: bool,
143
+ inputs: Dict,
144
+ features: Features,
169
145
  }
170
146
 
171
147
  impl SystemWorldBuilder {
@@ -174,7 +150,9 @@ impl SystemWorldBuilder {
174
150
  root,
175
151
  main,
176
152
  font_paths: Vec::new(),
177
- font_files: Vec::new(),
153
+ ignore_system_fonts: false,
154
+ inputs: Dict::default(),
155
+ features: Features::default(),
178
156
  }
179
157
  }
180
158
 
@@ -183,21 +161,25 @@ impl SystemWorldBuilder {
183
161
  self
184
162
  }
185
163
 
186
- pub fn font_files(mut self, font_files: Vec<PathBuf>) -> Self {
187
- self.font_files = font_files;
164
+ pub fn ignore_system_fonts(mut self, ignore: bool) -> Self {
165
+ self.ignore_system_fonts = ignore;
188
166
  self
189
167
  }
190
168
 
191
- pub fn build(self) -> StrResult<SystemWorld> {
192
- let mut searcher = FontSearcher::new();
193
- searcher.search(&self.font_paths, &self.font_files);
169
+ pub fn inputs(mut self, inputs: Dict) -> Self {
170
+ self.inputs = inputs;
171
+ self
172
+ }
194
173
 
195
- for (name, infos) in searcher.book.families() {
196
- for info in infos {
197
- let FontVariant { style, weight, stretch } = info.variant;
198
- debug!(target: "typst-rb", "font {name:?} variants - Style: {style:?}, Weight: {weight:?}, Stretch: {stretch:?}");
199
- }
200
- }
174
+ pub fn features(mut self, features: Features) -> Self {
175
+ self.features = features;
176
+ self
177
+ }
178
+
179
+ pub fn build(self) -> StrResult<SystemWorld> {
180
+ let fonts = FontSearcher::new()
181
+ .include_system_fonts(!self.ignore_system_fonts)
182
+ .search_with(&self.font_paths);
201
183
 
202
184
  let input = self.main.canonicalize().map_err(|_| {
203
185
  eco_format!("input file not found (searched at {})", self.main.display())
@@ -211,12 +193,12 @@ impl SystemWorldBuilder {
211
193
  input,
212
194
  root: self.root,
213
195
  main: FileId::new(None, main_path),
214
- library: Prehashed::new(typst_library::build()),
215
- book: Prehashed::new(searcher.book),
216
- fonts: searcher.fonts,
217
- hashes: RefCell::default(),
218
- slots: RefCell::default(),
219
- now: OnceCell::new(),
196
+ library: LazyHash::new(LibraryBuilder::default().with_inputs(self.inputs).with_features(self.features).build()),
197
+ book: LazyHash::new(fonts.book),
198
+ fonts: fonts.fonts,
199
+ slots: Mutex::default(),
200
+ package_storage: PackageStorage::new(None, None, crate::download::downloader()),
201
+ now: OnceLock::new(),
220
202
  };
221
203
  Ok(world)
222
204
  }
@@ -225,23 +207,20 @@ impl SystemWorldBuilder {
225
207
  /// Holds canonical data for all paths pointing to the same entity.
226
208
  ///
227
209
  /// Both fields can be populated if the file is both imported and read().
228
- struct PathSlot {
210
+ struct FileSlot {
229
211
  /// The slot's canonical file id.
230
212
  id: FileId,
231
- /// The slot's path on the system.
232
- path: PathBuf,
233
213
  /// The lazily loaded and incrementally updated source file.
234
214
  source: SlotCell<Source>,
235
215
  /// The lazily loaded raw byte buffer.
236
216
  file: SlotCell<Bytes>,
237
217
  }
238
218
 
239
- impl PathSlot {
219
+ impl FileSlot {
240
220
  /// Create a new path slot.
241
- fn new(id: FileId, path: PathBuf) -> Self {
221
+ fn new(id: FileId) -> Self {
242
222
  Self {
243
223
  id,
244
- path,
245
224
  file: SlotCell::new(),
246
225
  source: SlotCell::new(),
247
226
  }
@@ -249,92 +228,107 @@ impl PathSlot {
249
228
 
250
229
  /// Marks the file as not yet accessed in preparation of the next
251
230
  /// compilation.
252
- fn reset(&self) {
231
+ fn reset(&mut self) {
253
232
  self.source.reset();
254
233
  self.file.reset();
255
234
  }
256
235
 
257
- fn source(&self) -> FileResult<Source> {
258
- self.source.get_or_init(&self.path, |data, prev| {
259
- let text = decode_utf8(&data)?;
260
- if let Some(mut prev) = prev {
261
- prev.replace(text);
262
- Ok(prev)
263
- } else {
264
- Ok(Source::new(self.id, text.into()))
265
- }
266
- })
236
+ fn source(&mut self, root: &Path, package_storage: &PackageStorage) -> FileResult<Source> {
237
+ let id = self.id;
238
+ self.source.get_or_init(
239
+ || system_path(root, id, package_storage),
240
+ |data, prev| {
241
+ let text = decode_utf8(&data)?;
242
+ if let Some(mut prev) = prev {
243
+ prev.replace(text);
244
+ Ok(prev)
245
+ } else {
246
+ Ok(Source::new(self.id, text.into()))
247
+ }
248
+ },
249
+ )
250
+ }
251
+
252
+ fn file(&mut self, root: &Path, package_storage: &PackageStorage) -> FileResult<Bytes> {
253
+ let id = self.id;
254
+ self.file.get_or_init(
255
+ || system_path(root, id, package_storage),
256
+ |data, _| Ok(Bytes::new(data)),
257
+ )
267
258
  }
259
+ }
268
260
 
269
- fn file(&self) -> FileResult<Bytes> {
270
- self.file.get_or_init(&self.path, |data, _| Ok(data.into()))
261
+ /// The path of the slot on the system.
262
+ fn system_path(root: &Path, id: FileId, package_storage: &PackageStorage) -> FileResult<PathBuf> {
263
+ // Determine the root path relative to which the file path
264
+ // will be resolved.
265
+ let buf;
266
+ let mut root = root;
267
+ if let Some(spec) = id.package() {
268
+ buf = package_storage.prepare_package(spec, &mut SlientDownload(&spec))?;
269
+ root = &buf;
271
270
  }
271
+
272
+ // Join the path to the root. If it tries to escape, deny
273
+ // access. Note: It can still escape via symlinks.
274
+ id.vpath().resolve(root).ok_or(FileError::AccessDenied)
272
275
  }
273
276
 
274
277
  /// Lazily processes data for a file.
275
278
  struct SlotCell<T> {
276
- data: RefCell<Option<FileResult<T>>>,
277
- refreshed: Cell<FileTime>,
278
- accessed: Cell<bool>,
279
+ /// The processed data.
280
+ data: Option<FileResult<T>>,
281
+ /// A hash of the raw file contents / access error.
282
+ fingerprint: u128,
283
+ /// Whether the slot has been accessed in the current compilation.
284
+ accessed: bool,
279
285
  }
280
286
 
281
287
  impl<T: Clone> SlotCell<T> {
282
288
  /// Creates a new, empty cell.
283
289
  fn new() -> Self {
284
290
  Self {
285
- data: RefCell::new(None),
286
- refreshed: Cell::new(FileTime::zero()),
287
- accessed: Cell::new(false),
291
+ data: None,
292
+ fingerprint: 0,
293
+ accessed: false,
288
294
  }
289
295
  }
290
296
 
291
297
  /// Marks the cell as not yet accessed in preparation of the next
292
298
  /// compilation.
293
- fn reset(&self) {
294
- self.accessed.set(false);
299
+ fn reset(&mut self) {
300
+ self.accessed = false;
295
301
  }
296
302
 
297
303
  /// Gets the contents of the cell or initialize them.
298
304
  fn get_or_init(
299
- &self,
300
- path: &Path,
305
+ &mut self,
306
+ path: impl FnOnce() -> FileResult<PathBuf>,
301
307
  f: impl FnOnce(Vec<u8>, Option<T>) -> FileResult<T>,
302
308
  ) -> FileResult<T> {
303
- let mut borrow = self.data.borrow_mut();
304
- if let Some(data) = &*borrow {
305
- if self.accessed.replace(true) || self.current(path) {
309
+ // If we accessed the file already in this compilation, retrieve it.
310
+ if std::mem::replace(&mut self.accessed, true) {
311
+ if let Some(data) = &self.data {
306
312
  return data.clone();
307
313
  }
308
314
  }
309
315
 
310
- self.accessed.set(true);
311
- self.refreshed.set(FileTime::now());
312
- let prev = borrow.take().and_then(Result::ok);
313
- let value = read(path).and_then(|data| f(data, prev));
314
- *borrow = Some(value.clone());
315
- value
316
- }
316
+ // Read and hash the file.
317
+ let result = path().and_then(|p| read(&p));
318
+ let fingerprint = typst::utils::hash128(&result);
317
319
 
318
- /// Whether the cell contents are still up to date with the file system.
319
- fn current(&self, path: &Path) -> bool {
320
- fs::metadata(path).map_or(false, |meta| {
321
- let modified = FileTime::from_last_modification_time(&meta);
322
- modified < self.refreshed.get()
323
- })
324
- }
325
- }
320
+ // If the file contents didn't change, yield the old processed data.
321
+ if std::mem::replace(&mut self.fingerprint, fingerprint) == fingerprint {
322
+ if let Some(data) = &self.data {
323
+ return data.clone();
324
+ }
325
+ }
326
+
327
+ let prev = self.data.take().and_then(Result::ok);
328
+ let value = result.and_then(|data| f(data, prev));
329
+ self.data = Some(value.clone());
326
330
 
327
- /// A hash that is the same for all paths pointing to the same entity.
328
- #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
329
- struct PathHash(u128);
330
-
331
- impl PathHash {
332
- fn new(path: &Path) -> FileResult<Self> {
333
- let f = |e| FileError::from_io(e, path);
334
- let handle = Handle::from_path(path).map_err(f)?;
335
- let mut state = SipHasher13::new();
336
- handle.hash(&mut state);
337
- Ok(Self(state.finish128().as_u128()))
331
+ value
338
332
  }
339
333
  }
340
334
 
data/lib/typst.rb CHANGED
@@ -78,7 +78,7 @@ module Typst
78
78
 
79
79
  def initialize(input, root: ".", font_paths: [])
80
80
  super(input, root: root, font_paths: font_paths)
81
- @bytes = Typst::_to_pdf(self.input, self.root, self.font_paths, File.dirname(__FILE__))
81
+ @bytes = Typst::_to_pdf(self.input, self.root, self.font_paths, File.dirname(__FILE__), false, {})[0]
82
82
  end
83
83
 
84
84
  def document
@@ -91,7 +91,33 @@ module Typst
91
91
 
92
92
  def initialize(input, root: ".", font_paths: [])
93
93
  super(input, root: root, font_paths: font_paths)
94
- @pages = Typst::_to_svg(self.input, self.root, self.font_paths, File.dirname(__FILE__))
94
+ @pages = Typst::_to_svg(self.input, self.root, self.font_paths, File.dirname(__FILE__), false, {}).collect{ |page| page.pack("C*").to_s }
95
+ end
96
+
97
+ def write(output)
98
+ if pages.size > 1
99
+ pages.each_with_index do |page, i|
100
+ if output.include?("{{n}}")
101
+ file_name = output.gsub("{{n}}", (i+1).to_s)
102
+ else
103
+ file_name = File.basename(output, File.extname(output)) + "_" + i.to_s
104
+ file_name = file_name + File.extname(output)
105
+ end
106
+ File.open(file_name, "w"){ |f| f.write(page) }
107
+ end
108
+ elsif pages.size == 1
109
+ File.open(output, "w"){ |f| f.write(pages[0]) }
110
+ else
111
+ end
112
+ end
113
+ end
114
+
115
+ class Png < Base
116
+ attr_accessor :pages
117
+
118
+ def initialize(input, root: ".", font_paths: [])
119
+ super(input, root: root, font_paths: font_paths)
120
+ @pages = Typst::_to_png(self.input, self.root, self.font_paths, File.dirname(__FILE__), false, {}).collect{ |page| page.pack("C*").to_s }
95
121
  end
96
122
 
97
123
  def write(output)
@@ -139,4 +165,37 @@ module Typst
139
165
  end
140
166
  alias_method :document, :markup
141
167
  end
168
+
169
+ class HtmlExperimental < Base
170
+ attr_accessor :bytes
171
+
172
+ def initialize(input, root: ".", font_paths: [])
173
+ super(input, root: root, font_paths: font_paths)
174
+ @bytes = Typst::_to_html(self.input, self.root, self.font_paths, File.dirname(__FILE__), false, {})[0]
175
+ end
176
+
177
+ def document
178
+ bytes.pack("C*").to_s
179
+ end
180
+ alias_method :markup, :document
181
+ end
182
+
183
+ class Query < Base
184
+ attr_accessor :format
185
+ attr_accessor :result
186
+
187
+ def initialize(selector, input, field: nil, one: false, format: "json", root: ".", font_paths: [])
188
+ super(input, root: root, font_paths: font_paths)
189
+ self.format = format
190
+ self.result = Typst::_query(selector, field, one, format, self.input, self.root, self.font_paths, File.dirname(__FILE__), false, {})
191
+ end
192
+
193
+ def result(raw: false)
194
+ case raw || format
195
+ when "json" then JSON(@result)
196
+ when "yaml" then YAML::safe_load(@result)
197
+ else @result
198
+ end
199
+ end
200
+ end
142
201
  end