typst 0.0.4 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
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
  }
@@ -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
+ }