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.
@@ -1,9 +1,11 @@
1
1
  use chrono::{Datelike, Timelike};
2
2
  use codespan_reporting::diagnostic::{Diagnostic, Label};
3
3
  use codespan_reporting::term::{self, termcolor};
4
- use typst::diag::{Severity, SourceDiagnostic, StrResult};
5
- use typst::doc::Document;
6
- use typst::eval::{eco_format, Datetime, Tracer};
4
+ use ecow::{eco_format, EcoString};
5
+ use typst::diag::{Severity, SourceDiagnostic, StrResult, Warned};
6
+ use typst::foundations::Datetime;
7
+ use typst::html::HtmlDocument;
8
+ use typst::layout::PagedDocument;
7
9
  use typst::syntax::{FileId, Source, Span};
8
10
  use typst::{World, WorldExt};
9
11
 
@@ -13,55 +15,86 @@ type CodespanResult<T> = Result<T, CodespanError>;
13
15
  type CodespanError = codespan_reporting::files::Error;
14
16
 
15
17
  impl SystemWorld {
16
- pub fn compile_pdf(&mut self) -> StrResult<Vec<u8>> {
18
+ pub fn compile(
19
+ &mut self,
20
+ format: Option<&str>,
21
+ ppi: Option<f32>,
22
+ pdf_standards: &[typst_pdf::PdfStandard],
23
+ ) -> StrResult<Vec<Vec<u8>>> {
17
24
  // Reset everything and ensure that the main file is present.
18
25
  self.reset();
19
26
  self.source(self.main()).map_err(|err| err.to_string())?;
20
27
 
21
- let mut tracer = Tracer::default();
22
- let result = typst::compile(self, &mut tracer);
23
- let warnings = tracer.warnings();
24
-
25
- match result {
26
- // Export the PDF / PNG.
27
- Ok(document) => Ok(export_pdf(&document, self)?),
28
- Err(errors) => Err(format_diagnostics(self, &errors, &warnings).unwrap().into()),
29
- }
30
- }
31
-
32
- pub fn compile_svg(&mut self) -> StrResult<Vec<String>> {
33
- // Reset everything and ensure that the main file is present.
34
- self.reset();
35
- self.source(self.main()).map_err(|err| err.to_string())?;
36
-
37
- let mut tracer = Tracer::default();
38
- let result = typst::compile(self, &mut tracer);
39
- let warnings = tracer.warnings();
40
-
41
- match result {
42
- // Export the PDF / PNG.
43
- Ok(document) => Ok(export_svg(&document)?),
44
- Err(errors) => Err(format_diagnostics(self, &errors, &warnings).unwrap().into()),
28
+ match format.unwrap_or_else(|| "pdf").to_ascii_lowercase().as_str() {
29
+ "html" => {
30
+ let Warned { output, warnings } = typst::compile::<HtmlDocument>(self);
31
+ match output {
32
+ Ok(document) => {
33
+ Ok(vec![export_html(&document, self)?])
34
+ }
35
+ Err(errors) => Err(format_diagnostics(self, &errors, &warnings).unwrap().into()),
36
+ }
37
+ }
38
+ _ => {
39
+ let Warned { output, warnings } = typst::compile::<PagedDocument>(self);
40
+ match output {
41
+ // Export the PDF / PNG.
42
+ Ok(document) => {
43
+ // Assert format is "pdf" or "png" or "svg"
44
+ match format.unwrap_or("pdf").to_ascii_lowercase().as_str() {
45
+ "pdf" => Ok(vec![export_pdf(
46
+ &document,
47
+ self,
48
+ typst_pdf::PdfStandards::new(pdf_standards)?,
49
+ )?]),
50
+ "png" => Ok(export_image(&document, ImageExportFormat::Png, ppi)?),
51
+ "svg" => Ok(export_image(&document, ImageExportFormat::Svg, ppi)?),
52
+ fmt => Err(eco_format!("unknown format: {fmt}")),
53
+ }
54
+ }
55
+ Err(errors) => Err(format_diagnostics(self, &errors, &warnings).unwrap().into()),
56
+ }
57
+ }
45
58
  }
46
59
  }
47
60
  }
48
61
 
49
62
  /// Export to a PDF.
50
63
  #[inline]
51
- fn export_pdf(document: &Document, world: &SystemWorld) -> StrResult<Vec<u8>> {
52
- let ident = world.input().to_string_lossy();
53
- let buffer = typst::export::pdf(document, Some(&ident), now());
64
+ fn export_html(
65
+ document: &HtmlDocument,
66
+ world: &SystemWorld,
67
+ ) -> StrResult<Vec<u8>> {
68
+ let html = typst_html::html(document)
69
+ .map_err(|e| match format_diagnostics(world, &e, &[]) {
70
+ Ok(e) => EcoString::from(e),
71
+ Err(err) => eco_format!("failed to print diagnostics ({err})"),
72
+ })?;
73
+ let buffer = html.as_bytes().to_vec();
54
74
  Ok(buffer)
55
75
  }
56
76
 
57
- /// Export to one or multiple SVGs.
77
+ /// Export to a PDF.
58
78
  #[inline]
59
- fn export_svg(document: &Document) -> StrResult<Vec<String>> {
60
- let mut buffer: Vec<String> = Vec::new();
61
- for (_i, frame) in document.pages.iter().enumerate() {
62
- let svg = typst::export::svg(frame);
63
- buffer.push(svg.to_string())
64
- }
79
+ fn export_pdf(
80
+ document: &PagedDocument,
81
+ world: &SystemWorld,
82
+ standards: typst_pdf::PdfStandards,
83
+ ) -> StrResult<Vec<u8>> {
84
+ let ident = world.input().to_string_lossy();
85
+ let buffer = typst_pdf::pdf(
86
+ document,
87
+ &typst_pdf::PdfOptions {
88
+ ident: typst::foundations::Smart::Custom(&ident),
89
+ timestamp: now().map(typst_pdf::Timestamp::new_utc),
90
+ standards,
91
+ ..Default::default()
92
+ },
93
+ )
94
+ .map_err(|e| match format_diagnostics(world, &e, &[]) {
95
+ Ok(e) => EcoString::from(e),
96
+ Err(err) => eco_format!("failed to print diagnostics ({err})"),
97
+ })?;
65
98
  Ok(buffer)
66
99
  }
67
100
 
@@ -78,6 +111,34 @@ fn now() -> Option<Datetime> {
78
111
  )
79
112
  }
80
113
 
114
+ /// An image format to export in.
115
+ enum ImageExportFormat {
116
+ Png,
117
+ Svg,
118
+ }
119
+
120
+ /// Export the frames to PNGs or SVGs.
121
+ fn export_image(
122
+ document: &PagedDocument,
123
+ fmt: ImageExportFormat,
124
+ ppi: Option<f32>,
125
+ ) -> StrResult<Vec<Vec<u8>>> {
126
+ let mut buffers = Vec::new();
127
+ for page in &document.pages {
128
+ let buffer = match fmt {
129
+ ImageExportFormat::Png => typst_render::render(page, ppi.unwrap_or(144.0) / 72.0)
130
+ .encode_png()
131
+ .map_err(|err| eco_format!("failed to write PNG file ({err})"))?,
132
+ ImageExportFormat::Svg => {
133
+ let svg = typst_svg::svg(page);
134
+ svg.as_bytes().to_vec()
135
+ }
136
+ };
137
+ buffers.push(buffer);
138
+ }
139
+ Ok(buffers)
140
+ }
141
+
81
142
  /// Format diagnostic messages.\
82
143
  pub fn format_diagnostics(
83
144
  world: &SystemWorld,
@@ -141,7 +202,7 @@ impl<'a> codespan_reporting::files::Files<'a> for SystemWorld {
141
202
  // Try to express the path relative to the working directory.
142
203
  vpath
143
204
  .resolve(self.root())
144
- .and_then(|abs| pathdiff::diff_paths(&abs, self.workdir()))
205
+ .and_then(|abs| pathdiff::diff_paths(abs, self.workdir()))
145
206
  .as_deref()
146
207
  .unwrap_or_else(|| vpath.as_rootless_path())
147
208
  .to_string_lossy()
@@ -1,79 +1,19 @@
1
- use std::io::{self, ErrorKind, Read};
1
+ use std::fmt::Display;
2
2
 
3
- use ureq::Response;
3
+ use typst_kit::download::{DownloadState, Downloader, Progress};
4
4
 
5
- /// Download binary data and display its progress.
6
- #[allow(clippy::result_large_err)]
7
- pub fn download(url: &str) -> Result<Vec<u8>, ureq::Error> {
8
- let mut builder =
9
- ureq::AgentBuilder::new().user_agent(concat!("typst/{}", env!("CARGO_PKG_VERSION")));
5
+ pub struct SlientDownload<T>(pub T);
10
6
 
11
- // Get the network proxy config from the environment.
12
- if let Some(proxy) = env_proxy::for_url_str(url)
13
- .to_url()
14
- .and_then(|url| ureq::Proxy::new(url).ok())
15
- {
16
- builder = builder.proxy(proxy);
17
- }
7
+ impl<T: Display> Progress for SlientDownload<T> {
8
+ fn print_start(&mut self) {}
18
9
 
19
- let agent = builder.build();
20
- let response = agent.get(url).call()?;
21
- Ok(RemoteReader::from_response(response).download()?)
22
- }
10
+ fn print_progress(&mut self, _state: &DownloadState) {}
23
11
 
24
- /// A wrapper around [`ureq::Response`] that reads the response body in chunks
25
- /// over a websocket and displays statistics about its progress.
26
- ///
27
- /// Downloads will _never_ fail due to statistics failing to print, print errors
28
- /// are silently ignored.
29
- struct RemoteReader {
30
- reader: Box<dyn Read + Send + Sync + 'static>,
31
- content_len: Option<usize>,
12
+ fn print_finish(&mut self, _state: &DownloadState) {}
32
13
  }
33
14
 
34
- impl RemoteReader {
35
- /// Wraps a [`ureq::Response`] and prepares it for downloading.
36
- ///
37
- /// The 'Content-Length' header is used as a size hint for read
38
- /// optimization, if present.
39
- pub fn from_response(response: Response) -> Self {
40
- let content_len: Option<usize> = response
41
- .header("Content-Length")
42
- .and_then(|header| header.parse().ok());
43
-
44
- Self {
45
- reader: response.into_reader(),
46
- content_len,
47
- }
48
- }
49
-
50
- /// Download the bodies content as raw bytes while attempting to print
51
- /// download statistics to standard error. Download progress gets displayed
52
- /// and updated every second.
53
- ///
54
- /// These statistics will never prevent a download from completing, errors
55
- /// are silently ignored.
56
- pub fn download(mut self) -> io::Result<Vec<u8>> {
57
- let mut buffer = vec![0; 8192];
58
- let mut data = match self.content_len {
59
- Some(content_len) => Vec::with_capacity(content_len),
60
- None => Vec::with_capacity(8192),
61
- };
62
-
63
- loop {
64
- let read = match self.reader.read(&mut buffer) {
65
- Ok(0) => break,
66
- Ok(n) => n,
67
- // If the data is not yet ready but will be available eventually
68
- // keep trying until we either get an actual error, receive data
69
- // or an Ok(0).
70
- Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
71
- Err(e) => return Err(e),
72
- };
73
-
74
- data.extend(&buffer[..read]);
75
- }
76
-
77
- Ok(data)
78
- }
15
+ /// Returns a new downloader.
16
+ pub fn downloader() -> Downloader {
17
+ let user_agent = concat!("typst-py/", env!("CARGO_PKG_VERSION"));
18
+ Downloader::new(user_agent)
79
19
  }
data/ext/typst/src/lib.rs CHANGED
@@ -1,22 +1,94 @@
1
1
  use std::path::PathBuf;
2
2
 
3
- use magnus::{define_module, function, exception, Error, IntoValue};
3
+ use magnus::{define_module, function, exception, Error }; //, IntoValue};
4
4
  use magnus::{prelude::*};
5
5
 
6
+ use std::collections::HashMap;
7
+ use query::{query as typst_query, QueryCommand, SerializationFormat};
8
+ use typst::foundations::{Dict, Value};
9
+ use typst_library::Feature;
6
10
  use world::SystemWorld;
7
11
 
8
12
  mod compiler;
9
13
  mod download;
10
- mod fonts;
11
- mod package;
14
+ mod query;
12
15
  mod world;
13
16
 
17
+ fn to_html(
18
+ input: PathBuf,
19
+ root: Option<PathBuf>,
20
+ font_paths: Vec<PathBuf>,
21
+ resource_path: PathBuf,
22
+ ignore_system_fonts: bool,
23
+ sys_inputs: HashMap<String, String>,
24
+ ) -> Result<Vec<Vec<u8>>, Error> {
25
+ let input = input.canonicalize()
26
+ .map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
27
+
28
+ let root = if let Some(root) = root {
29
+ root.canonicalize()
30
+ .map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
31
+ } else if let Some(dir) = input.parent() {
32
+ dir.into()
33
+ } else {
34
+ PathBuf::new()
35
+ };
36
+
37
+ let resource_path = resource_path.canonicalize()
38
+ .map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
39
+
40
+ let mut default_fonts = Vec::new();
41
+ for entry in walkdir::WalkDir::new(resource_path.join("fonts")) {
42
+ let path = entry
43
+ .map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
44
+ .into_path();
45
+ let Some(extension) = path.extension() else {
46
+ continue;
47
+ };
48
+ if extension == "ttf" || extension == "otf" {
49
+ default_fonts.push(path);
50
+ }
51
+ }
52
+
53
+ let mut features = Vec::new();
54
+ features.push(Feature::Html);
55
+
56
+ let feat = features.iter()
57
+ .map(|&feature|
58
+ match feature {
59
+ Feature::Html => typst::Feature::Html,
60
+ _ => typst::Feature::Html // TODO: fix this hack
61
+ }
62
+ )
63
+ .collect();
64
+
65
+ let mut world = SystemWorld::builder(root, input)
66
+ .inputs(Dict::from_iter(
67
+ sys_inputs
68
+ .into_iter()
69
+ .map(|(k, v)| (k.into(), Value::Str(v.into()))),
70
+ ))
71
+ .features(feat)
72
+ .font_paths(font_paths)
73
+ .ignore_system_fonts(ignore_system_fonts)
74
+ .build()
75
+ .map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
76
+
77
+ let bytes = world
78
+ .compile(Some("html"), None, &Vec::new())
79
+ .map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
80
+
81
+ Ok(bytes)
82
+ }
83
+
14
84
  fn to_svg(
15
85
  input: PathBuf,
16
86
  root: Option<PathBuf>,
17
87
  font_paths: Vec<PathBuf>,
18
88
  resource_path: PathBuf,
19
- ) -> Result<Vec<String>, Error> {
89
+ ignore_system_fonts: bool,
90
+ sys_inputs: HashMap<String, String>,
91
+ ) -> Result<Vec<Vec<u8>>, Error> {
20
92
  let input = input.canonicalize()
21
93
  .map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
22
94
 
@@ -46,24 +118,85 @@ fn to_svg(
46
118
  }
47
119
 
48
120
  let mut world = SystemWorld::builder(root, input)
121
+ .inputs(Dict::from_iter(
122
+ sys_inputs
123
+ .into_iter()
124
+ .map(|(k, v)| (k.into(), Value::Str(v.into()))),
125
+ ))
49
126
  .font_paths(font_paths)
50
- .font_files(default_fonts)
127
+ .ignore_system_fonts(ignore_system_fonts)
51
128
  .build()
52
129
  .map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
53
130
 
54
131
  let svg_bytes = world
55
- .compile_svg()
132
+ .compile(Some("svg"), None, &Vec::new())
56
133
  .map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
57
134
 
58
135
  Ok(svg_bytes)
59
136
  }
60
137
 
138
+ fn to_png(
139
+ input: PathBuf,
140
+ root: Option<PathBuf>,
141
+ font_paths: Vec<PathBuf>,
142
+ resource_path: PathBuf,
143
+ ignore_system_fonts: bool,
144
+ sys_inputs: HashMap<String, String>,
145
+ ) -> Result<Vec<Vec<u8>>, Error> {
146
+ let input = input.canonicalize()
147
+ .map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
148
+
149
+ let root = if let Some(root) = root {
150
+ root.canonicalize()
151
+ .map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
152
+ } else if let Some(dir) = input.parent() {
153
+ dir.into()
154
+ } else {
155
+ PathBuf::new()
156
+ };
157
+
158
+ let resource_path = resource_path.canonicalize()
159
+ .map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
160
+
161
+ let mut default_fonts = Vec::new();
162
+ for entry in walkdir::WalkDir::new(resource_path.join("fonts")) {
163
+ let path = entry
164
+ .map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
165
+ .into_path();
166
+ let Some(extension) = path.extension() else {
167
+ continue;
168
+ };
169
+ if extension == "ttf" || extension == "otf" {
170
+ default_fonts.push(path);
171
+ }
172
+ }
173
+
174
+ let mut world = SystemWorld::builder(root, input)
175
+ .inputs(Dict::from_iter(
176
+ sys_inputs
177
+ .into_iter()
178
+ .map(|(k, v)| (k.into(), Value::Str(v.into()))),
179
+ ))
180
+ .font_paths(font_paths)
181
+ .ignore_system_fonts(ignore_system_fonts)
182
+ .build()
183
+ .map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
184
+
185
+ let bytes = world
186
+ .compile(Some("png"), None, &Vec::new())
187
+ .map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
188
+
189
+ Ok(bytes)
190
+ }
191
+
61
192
  fn to_pdf(
62
193
  input: PathBuf,
63
194
  root: Option<PathBuf>,
64
195
  font_paths: Vec<PathBuf>,
65
196
  resource_path: PathBuf,
66
- ) -> Result<Vec<u8>, Error> {
197
+ ignore_system_fonts: bool,
198
+ sys_inputs: HashMap<String, String>,
199
+ ) -> Result<Vec<Vec<u8>>, Error> {
67
200
  let input = input.canonicalize()
68
201
  .map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
69
202
 
@@ -93,41 +226,105 @@ fn to_pdf(
93
226
  }
94
227
 
95
228
  let mut world = SystemWorld::builder(root, input)
229
+ .inputs(Dict::from_iter(
230
+ sys_inputs
231
+ .into_iter()
232
+ .map(|(k, v)| (k.into(), Value::Str(v.into()))),
233
+ ))
96
234
  .font_paths(font_paths)
97
- .font_files(default_fonts)
235
+ .ignore_system_fonts(ignore_system_fonts)
98
236
  .build()
99
237
  .map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
100
238
 
101
239
  let pdf_bytes = world
102
- .compile_pdf()
240
+ .compile(Some("pdf"), None, &Vec::new())
103
241
  .map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
104
242
 
105
243
  Ok(pdf_bytes)
106
244
  }
107
245
 
108
- fn write_pdf(
246
+ fn query(
247
+ selector: String,
248
+ field: Option<String>,
249
+ one: bool,
250
+ format: Option<String>,
109
251
  input: PathBuf,
110
- output: PathBuf,
111
252
  root: Option<PathBuf>,
112
253
  font_paths: Vec<PathBuf>,
113
254
  resource_path: PathBuf,
114
- ) -> Result<magnus::Value, Error> {
115
- let pdf_bytes = to_pdf(input, root, font_paths, resource_path)?;
255
+ ignore_system_fonts: bool,
256
+ sys_inputs: HashMap<String, String>,
257
+ ) -> Result<String, Error> {
258
+ let format = match format.unwrap().to_ascii_lowercase().as_str() {
259
+ "json" => SerializationFormat::Json,
260
+ "yaml" => SerializationFormat::Yaml,
261
+ _ => return Err(magnus::Error::new(exception::arg_error(), "unsupported serialization format"))?,
262
+ };
116
263
 
117
- std::fs::write(output, pdf_bytes)
118
- .map_err(|_| magnus::Error::new(exception::arg_error(), "error"))?;
264
+ let input = input.canonicalize()
265
+ .map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
119
266
 
120
- let value = true.into_value();
121
- Ok(value)
122
- }
267
+ let root = if let Some(root) = root {
268
+ root.canonicalize()
269
+ .map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
270
+ } else if let Some(dir) = input.parent() {
271
+ dir.into()
272
+ } else {
273
+ PathBuf::new()
274
+ };
275
+
276
+ let resource_path = resource_path.canonicalize()
277
+ .map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
278
+
279
+ let mut default_fonts = Vec::new();
280
+ for entry in walkdir::WalkDir::new(resource_path.join("fonts")) {
281
+ let path = entry
282
+ .map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
283
+ .into_path();
284
+ let Some(extension) = path.extension() else {
285
+ continue;
286
+ };
287
+ if extension == "ttf" || extension == "otf" {
288
+ default_fonts.push(path);
289
+ }
290
+ }
291
+
292
+ let mut world = SystemWorld::builder(root, input)
293
+ .inputs(Dict::from_iter(
294
+ sys_inputs
295
+ .into_iter()
296
+ .map(|(k, v)| (k.into(), Value::Str(v.into()))),
297
+ ))
298
+ .font_paths(font_paths)
299
+ .ignore_system_fonts(ignore_system_fonts)
300
+ .build()
301
+ .map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
302
+
303
+ let result = typst_query(
304
+ &mut world,
305
+ &QueryCommand {
306
+ selector: selector.into(),
307
+ field: field.map(Into::into),
308
+ one,
309
+ format,
310
+ },
311
+ );
312
+
313
+ match result {
314
+ Ok(data) => Ok(data),
315
+ Err(msg) => Err(magnus::Error::new(exception::arg_error(), msg.to_string())),
316
+ }
317
+ }
123
318
 
124
319
  #[magnus::init]
125
320
  fn init() -> Result<(), Error> {
126
321
  env_logger::init();
127
322
 
128
323
  let module = define_module("Typst")?;
129
- module.define_singleton_method("_to_pdf", function!(to_pdf, 4))?;
130
- module.define_singleton_method("_write_pdf", function!(write_pdf, 5))?;
131
- module.define_singleton_method("_to_svg", function!(to_svg, 4))?;
324
+ module.define_singleton_method("_to_pdf", function!(to_pdf, 6))?;
325
+ module.define_singleton_method("_to_svg", function!(to_svg, 6))?;
326
+ module.define_singleton_method("_to_png", function!(to_png, 6))?;
327
+ module.define_singleton_method("_to_html", function!(to_html, 6))?;
328
+ module.define_singleton_method("_query", function!(query, 10))?;
132
329
  Ok(())
133
330
  }