typst 0.13.3-x86_64-linux-musl

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,351 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+ use std::sync::{Mutex, OnceLock};
4
+
5
+ use chrono::{DateTime, Datelike, Local};
6
+ use ecow::eco_format;
7
+ use typst::diag::{FileError, FileResult, StrResult};
8
+ use typst::foundations::{Bytes, Datetime, Dict};
9
+ use typst::syntax::{FileId, Source, VirtualPath};
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
+
20
+ use crate::download::SlientDownload;
21
+
22
+ /// A world that provides access to the operating system.
23
+ pub struct SystemWorld {
24
+ /// The working directory.
25
+ workdir: Option<PathBuf>,
26
+ /// The canonical path to the input file.
27
+ input: PathBuf,
28
+ /// The root relative to which absolute paths are resolved.
29
+ root: PathBuf,
30
+ /// The input path.
31
+ main: FileId,
32
+ /// Typst's standard library.
33
+ library: LazyHash<Library>,
34
+ /// Metadata about discovered fonts.
35
+ book: LazyHash<FontBook>,
36
+ /// Locations of and storage for lazily loaded fonts.
37
+ fonts: Vec<FontSlot>,
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,
42
+ /// The current datetime if requested. This is stored here to ensure it is
43
+ /// always the same within one compilation. Reset between compilations.
44
+ now: OnceLock<DateTime<Local>>,
45
+ }
46
+
47
+ impl World for SystemWorld {
48
+ fn library(&self) -> &LazyHash<Library> {
49
+ &self.library
50
+ }
51
+
52
+ fn book(&self) -> &LazyHash<FontBook> {
53
+ &self.book
54
+ }
55
+
56
+ fn main(&self) -> FileId {
57
+ self.main
58
+ }
59
+
60
+ fn source(&self, id: FileId) -> FileResult<Source> {
61
+ self.slot(id, |slot| slot.source(&self.root, &self.package_storage))
62
+ }
63
+
64
+ fn file(&self, id: FileId) -> FileResult<Bytes> {
65
+ self.slot(id, |slot| slot.file(&self.root, &self.package_storage))
66
+ }
67
+
68
+ fn font(&self, index: usize) -> Option<Font> {
69
+ self.fonts[index].get()
70
+ }
71
+
72
+ fn today(&self, offset: Option<i64>) -> Option<Datetime> {
73
+ let now = self.now.get_or_init(chrono::Local::now);
74
+
75
+ let naive = match offset {
76
+ None => now.naive_local(),
77
+ Some(o) => now.naive_utc() + chrono::Duration::hours(o),
78
+ };
79
+
80
+ Datetime::from_ymd(
81
+ naive.year(),
82
+ naive.month().try_into().ok()?,
83
+ naive.day().try_into().ok()?,
84
+ )
85
+ }
86
+ }
87
+
88
+ impl SystemWorld {
89
+ pub fn builder(root: PathBuf, main: PathBuf) -> SystemWorldBuilder {
90
+ SystemWorldBuilder::new(root, main)
91
+ }
92
+
93
+ /// Access the canonical slot for the given file id.
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)))
100
+ }
101
+
102
+ /// The id of the main source file.
103
+ pub fn main(&self) -> FileId {
104
+ self.main
105
+ }
106
+
107
+ /// The root relative to which absolute paths are resolved.
108
+ pub fn root(&self) -> &Path {
109
+ &self.root
110
+ }
111
+
112
+ /// The current working directory.
113
+ pub fn workdir(&self) -> &Path {
114
+ self.workdir.as_deref().unwrap_or(Path::new("."))
115
+ }
116
+
117
+ /// Reset the compilation state in preparation of a new compilation.
118
+ pub fn reset(&mut self) {
119
+ for slot in self.slots.lock().unwrap().values_mut() {
120
+ slot.reset();
121
+ }
122
+ self.now.take();
123
+ }
124
+
125
+ /// Return the canonical path to the input file.
126
+ pub fn input(&self) -> &PathBuf {
127
+ &self.input
128
+ }
129
+
130
+ /// Lookup a source file by id.
131
+ #[track_caller]
132
+ pub fn lookup(&self, id: FileId) -> Source {
133
+ self.source(id)
134
+ .expect("file id does not point to any source file")
135
+ }
136
+ }
137
+
138
+ pub struct SystemWorldBuilder {
139
+ root: PathBuf,
140
+ main: PathBuf,
141
+ font_paths: Vec<PathBuf>,
142
+ ignore_system_fonts: bool,
143
+ inputs: Dict,
144
+ features: Features,
145
+ }
146
+
147
+ impl SystemWorldBuilder {
148
+ pub fn new(root: PathBuf, main: PathBuf) -> Self {
149
+ Self {
150
+ root,
151
+ main,
152
+ font_paths: Vec::new(),
153
+ ignore_system_fonts: false,
154
+ inputs: Dict::default(),
155
+ features: Features::default(),
156
+ }
157
+ }
158
+
159
+ pub fn font_paths(mut self, font_paths: Vec<PathBuf>) -> Self {
160
+ self.font_paths = font_paths;
161
+ self
162
+ }
163
+
164
+ pub fn ignore_system_fonts(mut self, ignore: bool) -> Self {
165
+ self.ignore_system_fonts = ignore;
166
+ self
167
+ }
168
+
169
+ pub fn inputs(mut self, inputs: Dict) -> Self {
170
+ self.inputs = inputs;
171
+ self
172
+ }
173
+
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);
183
+
184
+ let input = self.main.canonicalize().map_err(|_| {
185
+ eco_format!("input file not found (searched at {})", self.main.display())
186
+ })?;
187
+ // Resolve the virtual path of the main file within the project root.
188
+ let main_path = VirtualPath::within_root(&self.main, &self.root)
189
+ .ok_or("input file must be contained in project root")?;
190
+
191
+ let world = SystemWorld {
192
+ workdir: std::env::current_dir().ok(),
193
+ input,
194
+ root: self.root,
195
+ main: FileId::new(None, main_path),
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(),
202
+ };
203
+ Ok(world)
204
+ }
205
+ }
206
+
207
+ /// Holds canonical data for all paths pointing to the same entity.
208
+ ///
209
+ /// Both fields can be populated if the file is both imported and read().
210
+ struct FileSlot {
211
+ /// The slot's canonical file id.
212
+ id: FileId,
213
+ /// The lazily loaded and incrementally updated source file.
214
+ source: SlotCell<Source>,
215
+ /// The lazily loaded raw byte buffer.
216
+ file: SlotCell<Bytes>,
217
+ }
218
+
219
+ impl FileSlot {
220
+ /// Create a new path slot.
221
+ fn new(id: FileId) -> Self {
222
+ Self {
223
+ id,
224
+ file: SlotCell::new(),
225
+ source: SlotCell::new(),
226
+ }
227
+ }
228
+
229
+ /// Marks the file as not yet accessed in preparation of the next
230
+ /// compilation.
231
+ fn reset(&mut self) {
232
+ self.source.reset();
233
+ self.file.reset();
234
+ }
235
+
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
+ )
258
+ }
259
+ }
260
+
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;
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)
275
+ }
276
+
277
+ /// Lazily processes data for a file.
278
+ struct SlotCell<T> {
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,
285
+ }
286
+
287
+ impl<T: Clone> SlotCell<T> {
288
+ /// Creates a new, empty cell.
289
+ fn new() -> Self {
290
+ Self {
291
+ data: None,
292
+ fingerprint: 0,
293
+ accessed: false,
294
+ }
295
+ }
296
+
297
+ /// Marks the cell as not yet accessed in preparation of the next
298
+ /// compilation.
299
+ fn reset(&mut self) {
300
+ self.accessed = false;
301
+ }
302
+
303
+ /// Gets the contents of the cell or initialize them.
304
+ fn get_or_init(
305
+ &mut self,
306
+ path: impl FnOnce() -> FileResult<PathBuf>,
307
+ f: impl FnOnce(Vec<u8>, Option<T>) -> FileResult<T>,
308
+ ) -> FileResult<T> {
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 {
312
+ return data.clone();
313
+ }
314
+ }
315
+
316
+ // Read and hash the file.
317
+ let result = path().and_then(|p| read(&p));
318
+ let fingerprint = typst::utils::hash128(&result);
319
+
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());
330
+
331
+ value
332
+ }
333
+ }
334
+
335
+ /// Read a file.
336
+ fn read(path: &Path) -> FileResult<Vec<u8>> {
337
+ let f = |e| FileError::from_io(e, path);
338
+ if fs::metadata(path).map_err(f)?.is_dir() {
339
+ Err(FileError::IsDirectory)
340
+ } else {
341
+ fs::read(path).map_err(f)
342
+ }
343
+ }
344
+
345
+ /// Decode UTF-8 with an optional BOM.
346
+ fn decode_utf8(buf: &[u8]) -> FileResult<&str> {
347
+ // Remove UTF-8 BOM.
348
+ Ok(std::str::from_utf8(
349
+ buf.strip_prefix(b"\xef\xbb\xbf").unwrap_or(buf),
350
+ )?)
351
+ }
data/lib/base.rb ADDED
@@ -0,0 +1,169 @@
1
+ module Typst
2
+ class Base
3
+ attr_accessor :options
4
+ attr_accessor :compiled
5
+
6
+ def initialize(*options)
7
+ if options.size.zero?
8
+ raise "No options given"
9
+ elsif options.first.is_a?(String)
10
+ file, options = options
11
+ options ||= {}
12
+ options[:file] = file
13
+ elsif options.first.is_a?(Hash)
14
+ options = options.first
15
+ end
16
+
17
+ if options.has_key?(:file)
18
+ raise "Can't find file" unless File.exist?(options[:file])
19
+ elsif options.has_key?(:body)
20
+ raise "Empty body" if options[:body].to_s.empty?
21
+ elsif options.has_key?(:zip)
22
+ raise "Can't find zip" unless File.exist?(options[:zip])
23
+ else
24
+ raise "No input given"
25
+ end
26
+
27
+ root = Pathname.new(options[:root] || ".").expand_path
28
+ raise "Invalid path for root" unless root.exist?
29
+ options[:root] = root.to_s
30
+
31
+ font_paths = (options[:font_paths] || []).collect{ |fp| Pathname.new(fp).expand_path }
32
+ options[:font_paths] = font_paths.collect(&:to_s)
33
+
34
+ options[:dependencies] ||= {}
35
+ options[:fonts] ||= {}
36
+ options[:sys_inputs] ||= {}
37
+
38
+ self.options = options
39
+ end
40
+
41
+ def typst_args
42
+ [options[:file], options[:root], options[:font_paths], File.dirname(__FILE__), false, options[:sys_inputs].map{ |k,v| [k.to_s,v.to_s] }.to_h]
43
+ end
44
+
45
+ def self.from_s(main_source, **options)
46
+ dependencies = options[:dependencies] ||= {}
47
+ fonts = options[:fonts] ||= {}
48
+
49
+ Dir.mktmpdir do |tmp_dir|
50
+ tmp_main_file = Pathname.new(tmp_dir).join("main.typ")
51
+ File.write(tmp_main_file, main_source)
52
+
53
+ dependencies.each do |dep_name, dep_source|
54
+ tmp_dep_file = Pathname.new(tmp_dir).join(dep_name)
55
+ File.write(tmp_dep_file, dep_source)
56
+ end
57
+
58
+ relative_font_path = Pathname.new(tmp_dir).join("fonts")
59
+ fonts.each do |font_name, font_bytes|
60
+ Pathname.new(relative_font_path).mkpath
61
+ tmp_font_file = relative_font_path.join(font_name)
62
+ File.write(tmp_font_file, font_bytes)
63
+ end
64
+
65
+ options[:file] = tmp_main_file
66
+ options[:root] = tmp_dir
67
+ options[:font_paths] = [relative_font_path]
68
+
69
+ if options[:format]
70
+ Typst::formats[options[:format]].new(**options)
71
+ else
72
+ new(**options)
73
+ end
74
+ end
75
+ end
76
+
77
+ def self.from_zip(zip_file_path, main_file = nil, **options)
78
+ options[:dependencies] ||= {}
79
+ options[:fonts] ||= {}
80
+
81
+ Zip::File.open(zip_file_path) do |zipfile|
82
+ file_names = zipfile.dir.glob("*").collect{ |f| f.name }
83
+ case
84
+ when file_names.include?(main_file) then tmp_main_file = main_file
85
+ when file_names.include?("main.typ") then tmp_main_file = "main.typ"
86
+ when file_names.size == 1 then tmp_main_file = file_names.first
87
+ else raise "no main file found"
88
+ end
89
+ main_source = zipfile.file.read(tmp_main_file)
90
+ file_names.delete(tmp_main_file)
91
+ file_names.delete("fonts/")
92
+
93
+ file_names.each do |dep_name|
94
+ options[:dependencies][dep_name] = zipfile.file.read(dep_name)
95
+ end
96
+
97
+ font_file_names = zipfile.dir.glob("fonts/*").collect{ |f| f.name }
98
+ font_file_names.each do |font_name|
99
+ options[:fonts][Pathname.new(font_name).basename.to_s] = zipfile.file.read(font_name)
100
+ end
101
+
102
+ options[:main_file] = tmp_main_file
103
+
104
+ from_s(main_source, **options)
105
+ end
106
+ end
107
+
108
+ def with_dependencies(dependencies)
109
+ self.options[:dependencies] = self.options[:dependencies].merge(dependencies)
110
+ self
111
+ end
112
+
113
+ def with_fonts(fonts)
114
+ self.options[:fonts] = self.options[:fonts].merge(fonts)
115
+ self
116
+ end
117
+
118
+ def with_inputs(inputs)
119
+ self.options[:sys_inputs] = self.options[:sys_inputs].merge(inputs)
120
+ self
121
+ end
122
+
123
+ def with_font_paths(font_paths)
124
+ self.options[:font_paths] = self.options[:font_paths] + font_paths
125
+ self
126
+ end
127
+
128
+ def with_root(root)
129
+ self.options[:root] = root
130
+ self
131
+ end
132
+
133
+ def compile(format, **options)
134
+ raise "Invalid format" if Typst::formats[format].nil?
135
+
136
+ options = self.options.merge(options)
137
+
138
+ if options.has_key?(:file)
139
+ Typst::formats[format].new(**options).compiled
140
+ elsif options.has_key?(:body)
141
+ Typst::formats[format].from_s(options[:body], **options).compiled
142
+ elsif options.has_key?(:zip)
143
+ Typst::formats[format].from_zip(options[:zip], options[:main_file], **options).compiled
144
+ else
145
+ raise "No input given"
146
+ end
147
+ end
148
+
149
+ def write(output)
150
+ STDERR.puts "DEPRECATION WARNING: this method will go away in a future version"
151
+ compiled.write(output)
152
+ end
153
+
154
+ def document
155
+ STDERR.puts "DEPRECATION WARNING: this method will go away in a future version"
156
+ compiled.document
157
+ end
158
+
159
+ def bytes
160
+ STDERR.puts "DEPRECATION WARNING: this method will go away in a future version"
161
+ compiled.bytes
162
+ end
163
+
164
+ def pages
165
+ STDERR.puts "DEPRECATION WARNING: this method will go away in a future version"
166
+ compiled.pages
167
+ end
168
+ end
169
+ end
data/lib/document.rb ADDED
@@ -0,0 +1,29 @@
1
+ module Typst
2
+ class Document
3
+ attr_accessor :bytes
4
+
5
+ def initialize(bytes)
6
+ @bytes = bytes
7
+ end
8
+
9
+ def write(out)
10
+ if pages.size == 1
11
+ File.write(out, pages.first, mode: "wb")
12
+ else
13
+ pages.each_with_index do |page, i|
14
+ fn = File.basename(out, ".*") + "_{{n}}" + File.extname(out) unless out.include?("{{n}}")
15
+ fn = fn.gsub("{{n}}", (i+1).to_s)
16
+ File.write(fn, page, mode: "wb")
17
+ end
18
+ end
19
+ end
20
+
21
+ def pages
22
+ bytes.collect{ |page| page.pack("C*").to_s }
23
+ end
24
+
25
+ def document
26
+ pages.size == 1 ? pages.first : pages
27
+ end
28
+ end
29
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,35 @@
1
+ module Typst
2
+ class Html < Base
3
+ def initialize(*options)
4
+ super(*options)
5
+ title = CGI::escapeHTML(@options[:title] || File.basename(@options[:file], ".*"))
6
+ @compiled = HtmlDocument.new(Typst::_to_svg(*self.typst_args), title)
7
+ end
8
+ end
9
+
10
+ class HtmlDocument < Document
11
+ attr_accessor :title
12
+
13
+ def initialize(bytes, title)
14
+ super(bytes)
15
+ self.title = title
16
+ end
17
+
18
+ def markup
19
+ %{
20
+ <!DOCTYPE html>
21
+ <html>
22
+ <head>
23
+ <title>#{title}</title>
24
+ </head>
25
+ <body>
26
+ #{pages.join("<br />")}
27
+ </body>
28
+ </html>
29
+ }
30
+ end
31
+ alias_method :document, :markup
32
+ end
33
+
34
+ register_format(html: Html)
35
+ end
@@ -0,0 +1,11 @@
1
+ module Typst
2
+ class HtmlExperimental < Base
3
+ def initialize(*options)
4
+ super(*options)
5
+ @compiled = HtmlExperimentalDocument.new(Typst::_to_html(*self.typst_args))
6
+ end
7
+ end
8
+ class HtmlExperimentalDocument < Document; end
9
+
10
+ register_format(html_experimental: HtmlExperimental)
11
+ end
@@ -0,0 +1,11 @@
1
+ module Typst
2
+ class Pdf < Base
3
+ def initialize(*options)
4
+ super(*options)
5
+ @compiled = PdfDocument.new(Typst::_to_pdf(*self.typst_args))
6
+ end
7
+ end
8
+ class PdfDocument < Document; end
9
+
10
+ register_format(pdf: Pdf)
11
+ end
@@ -0,0 +1,11 @@
1
+ module Typst
2
+ class Png < Base
3
+ def initialize(*options)
4
+ super(*options)
5
+ @compiled = PngDocument.new(Typst::_to_png(*self.typst_args))
6
+ end
7
+ end
8
+ class PngDocument < Document; end
9
+
10
+ register_format(png: Png)
11
+ end
@@ -0,0 +1,11 @@
1
+ module Typst
2
+ class Svg < Base
3
+ def initialize(*options)
4
+ super(*options)
5
+ @compiled = SvgDocument.new(Typst::_to_svg(*self.typst_args))
6
+ end
7
+ end
8
+ class SvgDocument < Document; end
9
+
10
+ register_format(svg: Svg)
11
+ end
data/lib/query.rb ADDED
@@ -0,0 +1,19 @@
1
+ module Typst
2
+ class Query < Base
3
+ attr_accessor :format
4
+
5
+ def initialize(selector, input, field: nil, one: false, format: "json", root: ".", font_paths: [], sys_inputs: {})
6
+ super(input, root: root, font_paths: font_paths, sys_inputs: sys_inputs)
7
+ self.format = format
8
+ @result = Typst::_query(selector, field, one, format, input, root, font_paths, File.dirname(__FILE__), false, sys_inputs)
9
+ end
10
+
11
+ def result(raw: false)
12
+ case raw || format
13
+ when "json" then JSON(@result)
14
+ when "yaml" then YAML::safe_load(@result)
15
+ else @result
16
+ end
17
+ end
18
+ end
19
+ end
Binary file