typst 0.13.3-x86_64-darwin-21
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.
- checksums.yaml +7 -0
- data/Cargo.toml +3 -0
- data/README.md +133 -0
- data/README.typ +136 -0
- data/Rakefile +31 -0
- data/ext/typst/Cargo.toml +54 -0
- data/ext/typst/extconf.rb +4 -0
- data/ext/typst/src/compiler.rs +248 -0
- data/ext/typst/src/download.rs +19 -0
- data/ext/typst/src/fonts.rs +86 -0
- data/ext/typst/src/lib.rs +330 -0
- data/ext/typst/src/package.rs +64 -0
- data/ext/typst/src/query.rs +131 -0
- data/ext/typst/src/world.rs +351 -0
- data/lib/base.rb +169 -0
- data/lib/document.rb +29 -0
- data/lib/fonts/DejaVuSansMono-Bold.ttf +0 -0
- data/lib/fonts/DejaVuSansMono-BoldOblique.ttf +0 -0
- data/lib/fonts/DejaVuSansMono-Oblique.ttf +0 -0
- data/lib/fonts/DejaVuSansMono.ttf +0 -0
- data/lib/fonts/LinLibertine_R.ttf +0 -0
- data/lib/fonts/LinLibertine_RB.ttf +0 -0
- data/lib/fonts/LinLibertine_RBI.ttf +0 -0
- data/lib/fonts/LinLibertine_RI.ttf +0 -0
- data/lib/fonts/NewCM10-Bold.otf +0 -0
- data/lib/fonts/NewCM10-BoldItalic.otf +0 -0
- data/lib/fonts/NewCM10-Italic.otf +0 -0
- data/lib/fonts/NewCM10-Regular.otf +0 -0
- data/lib/fonts/NewCMMath-Book.otf +0 -0
- data/lib/fonts/NewCMMath-Regular.otf +0 -0
- data/lib/formats/html.rb +35 -0
- data/lib/formats/html_experimental.rb +11 -0
- data/lib/formats/pdf.rb +11 -0
- data/lib/formats/png.rb +11 -0
- data/lib/formats/svg.rb +11 -0
- data/lib/query.rb +19 -0
- data/lib/typst/typst.bundle +0 -0
- data/lib/typst.rb +31 -0
- metadata +111 -0
@@ -0,0 +1,86 @@
|
|
1
|
+
use std::cell::OnceCell;
|
2
|
+
use std::fs::{self};
|
3
|
+
use std::path::PathBuf;
|
4
|
+
|
5
|
+
use fontdb::{Database, Source};
|
6
|
+
use typst::font::{Font, FontBook, FontInfo};
|
7
|
+
|
8
|
+
/// Searches for fonts.
|
9
|
+
pub struct FontSearcher {
|
10
|
+
// Metadata about all discovered fonts.
|
11
|
+
pub book: FontBook,
|
12
|
+
/// Slots that the fonts are loaded into.
|
13
|
+
pub fonts: Vec<FontSlot>,
|
14
|
+
}
|
15
|
+
|
16
|
+
/// Holds details about the location of a font and lazily the font itself.
|
17
|
+
pub struct FontSlot {
|
18
|
+
/// The path at which the font can be found on the system.
|
19
|
+
path: PathBuf,
|
20
|
+
/// The index of the font in its collection. Zero if the path does not point
|
21
|
+
/// to a collection.
|
22
|
+
index: u32,
|
23
|
+
/// The lazily loaded font.
|
24
|
+
font: OnceCell<Option<Font>>,
|
25
|
+
}
|
26
|
+
|
27
|
+
impl FontSlot {
|
28
|
+
/// Get the font for this slot.
|
29
|
+
pub fn get(&self) -> Option<Font> {
|
30
|
+
self.font
|
31
|
+
.get_or_init(|| {
|
32
|
+
let data = fs::read(&self.path).ok()?.into();
|
33
|
+
Font::new(data, self.index)
|
34
|
+
})
|
35
|
+
.clone()
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
impl FontSearcher {
|
40
|
+
/// Create a new, empty system searcher.
|
41
|
+
pub fn new() -> Self {
|
42
|
+
Self {
|
43
|
+
book: FontBook::new(),
|
44
|
+
fonts: vec![],
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
/// Search everything that is available.
|
49
|
+
pub fn search(&mut self, font_dirs: &[PathBuf], font_files: &[PathBuf]) {
|
50
|
+
let mut db = Database::new();
|
51
|
+
|
52
|
+
// Font paths have highest priority.
|
53
|
+
for path in font_dirs {
|
54
|
+
db.load_fonts_dir(path);
|
55
|
+
}
|
56
|
+
|
57
|
+
// System fonts have second priority.
|
58
|
+
db.load_system_fonts();
|
59
|
+
|
60
|
+
for path in font_files {
|
61
|
+
let _ret = db.load_font_file(path).ok();
|
62
|
+
}
|
63
|
+
|
64
|
+
for face in db.faces() {
|
65
|
+
let path = match &face.source {
|
66
|
+
Source::File(path) | Source::SharedFile(path, _) => path,
|
67
|
+
// We never add binary sources to the database, so there
|
68
|
+
// shouln't be any.
|
69
|
+
Source::Binary(_) => continue,
|
70
|
+
};
|
71
|
+
|
72
|
+
let info = db
|
73
|
+
.with_face_data(face.id, FontInfo::new)
|
74
|
+
.expect("database must contain this font");
|
75
|
+
|
76
|
+
if let Some(info) = info {
|
77
|
+
self.book.push(info);
|
78
|
+
self.fonts.push(FontSlot {
|
79
|
+
path: path.clone(),
|
80
|
+
index: face.index,
|
81
|
+
font: OnceCell::new(),
|
82
|
+
});
|
83
|
+
}
|
84
|
+
}
|
85
|
+
}
|
86
|
+
}
|
@@ -0,0 +1,330 @@
|
|
1
|
+
use std::path::PathBuf;
|
2
|
+
|
3
|
+
use magnus::{define_module, function, exception, Error }; //, IntoValue};
|
4
|
+
use magnus::{prelude::*};
|
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;
|
10
|
+
use world::SystemWorld;
|
11
|
+
|
12
|
+
mod compiler;
|
13
|
+
mod download;
|
14
|
+
mod query;
|
15
|
+
mod world;
|
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
|
+
|
84
|
+
fn to_svg(
|
85
|
+
input: PathBuf,
|
86
|
+
root: Option<PathBuf>,
|
87
|
+
font_paths: Vec<PathBuf>,
|
88
|
+
resource_path: PathBuf,
|
89
|
+
ignore_system_fonts: bool,
|
90
|
+
sys_inputs: HashMap<String, String>,
|
91
|
+
) -> Result<Vec<Vec<u8>>, Error> {
|
92
|
+
let input = input.canonicalize()
|
93
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
|
94
|
+
|
95
|
+
let root = if let Some(root) = root {
|
96
|
+
root.canonicalize()
|
97
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
|
98
|
+
} else if let Some(dir) = input.parent() {
|
99
|
+
dir.into()
|
100
|
+
} else {
|
101
|
+
PathBuf::new()
|
102
|
+
};
|
103
|
+
|
104
|
+
let resource_path = resource_path.canonicalize()
|
105
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
|
106
|
+
|
107
|
+
let mut default_fonts = Vec::new();
|
108
|
+
for entry in walkdir::WalkDir::new(resource_path.join("fonts")) {
|
109
|
+
let path = entry
|
110
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
|
111
|
+
.into_path();
|
112
|
+
let Some(extension) = path.extension() else {
|
113
|
+
continue;
|
114
|
+
};
|
115
|
+
if extension == "ttf" || extension == "otf" {
|
116
|
+
default_fonts.push(path);
|
117
|
+
}
|
118
|
+
}
|
119
|
+
|
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
|
+
))
|
126
|
+
.font_paths(font_paths)
|
127
|
+
.ignore_system_fonts(ignore_system_fonts)
|
128
|
+
.build()
|
129
|
+
.map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
|
130
|
+
|
131
|
+
let svg_bytes = world
|
132
|
+
.compile(Some("svg"), None, &Vec::new())
|
133
|
+
.map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
|
134
|
+
|
135
|
+
Ok(svg_bytes)
|
136
|
+
}
|
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
|
+
|
192
|
+
fn to_pdf(
|
193
|
+
input: PathBuf,
|
194
|
+
root: Option<PathBuf>,
|
195
|
+
font_paths: Vec<PathBuf>,
|
196
|
+
resource_path: PathBuf,
|
197
|
+
ignore_system_fonts: bool,
|
198
|
+
sys_inputs: HashMap<String, String>,
|
199
|
+
) -> Result<Vec<Vec<u8>>, Error> {
|
200
|
+
let input = input.canonicalize()
|
201
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
|
202
|
+
|
203
|
+
let root = if let Some(root) = root {
|
204
|
+
root.canonicalize()
|
205
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
|
206
|
+
} else if let Some(dir) = input.parent() {
|
207
|
+
dir.into()
|
208
|
+
} else {
|
209
|
+
PathBuf::new()
|
210
|
+
};
|
211
|
+
|
212
|
+
let resource_path = resource_path.canonicalize()
|
213
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
|
214
|
+
|
215
|
+
let mut default_fonts = Vec::new();
|
216
|
+
for entry in walkdir::WalkDir::new(resource_path.join("fonts")) {
|
217
|
+
let path = entry
|
218
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
|
219
|
+
.into_path();
|
220
|
+
let Some(extension) = path.extension() else {
|
221
|
+
continue;
|
222
|
+
};
|
223
|
+
if extension == "ttf" || extension == "otf" {
|
224
|
+
default_fonts.push(path);
|
225
|
+
}
|
226
|
+
}
|
227
|
+
|
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
|
+
))
|
234
|
+
.font_paths(font_paths)
|
235
|
+
.ignore_system_fonts(ignore_system_fonts)
|
236
|
+
.build()
|
237
|
+
.map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
|
238
|
+
|
239
|
+
let pdf_bytes = world
|
240
|
+
.compile(Some("pdf"), None, &Vec::new())
|
241
|
+
.map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
|
242
|
+
|
243
|
+
Ok(pdf_bytes)
|
244
|
+
}
|
245
|
+
|
246
|
+
fn query(
|
247
|
+
selector: String,
|
248
|
+
field: Option<String>,
|
249
|
+
one: bool,
|
250
|
+
format: Option<String>,
|
251
|
+
input: PathBuf,
|
252
|
+
root: Option<PathBuf>,
|
253
|
+
font_paths: Vec<PathBuf>,
|
254
|
+
resource_path: PathBuf,
|
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
|
+
};
|
263
|
+
|
264
|
+
let input = input.canonicalize()
|
265
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
|
266
|
+
|
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
|
+
}
|
318
|
+
|
319
|
+
#[magnus::init]
|
320
|
+
fn init() -> Result<(), Error> {
|
321
|
+
env_logger::init();
|
322
|
+
|
323
|
+
let module = define_module("Typst")?;
|
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))?;
|
329
|
+
Ok(())
|
330
|
+
}
|
@@ -0,0 +1,64 @@
|
|
1
|
+
use std::fs;
|
2
|
+
use std::path::{Path, PathBuf};
|
3
|
+
|
4
|
+
use ecow::eco_format;
|
5
|
+
use typst::diag::{PackageError, PackageResult};
|
6
|
+
use typst::syntax::PackageSpec;
|
7
|
+
|
8
|
+
use crate::download::download;
|
9
|
+
|
10
|
+
/// Make a package available in the on-disk cache.
|
11
|
+
pub fn prepare_package(spec: &PackageSpec) -> PackageResult<PathBuf> {
|
12
|
+
let subdir = format!(
|
13
|
+
"typst/packages/{}/{}/{}",
|
14
|
+
spec.namespace, spec.name, spec.version
|
15
|
+
);
|
16
|
+
|
17
|
+
if let Some(data_dir) = dirs::data_dir() {
|
18
|
+
let dir = data_dir.join(&subdir);
|
19
|
+
if dir.exists() {
|
20
|
+
return Ok(dir);
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
if let Some(cache_dir) = dirs::cache_dir() {
|
25
|
+
let dir = cache_dir.join(&subdir);
|
26
|
+
|
27
|
+
// Download from network if it doesn't exist yet.
|
28
|
+
if spec.namespace == "preview" && !dir.exists() {
|
29
|
+
download_package(spec, &dir)?;
|
30
|
+
}
|
31
|
+
|
32
|
+
if dir.exists() {
|
33
|
+
return Ok(dir);
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
Err(PackageError::NotFound(spec.clone()))
|
38
|
+
}
|
39
|
+
|
40
|
+
/// Download a package over the network.
|
41
|
+
fn download_package(spec: &PackageSpec, package_dir: &Path) -> PackageResult<()> {
|
42
|
+
// The `@preview` namespace is the only namespace that supports on-demand
|
43
|
+
// fetching.
|
44
|
+
assert_eq!(spec.namespace, "preview");
|
45
|
+
|
46
|
+
let url = format!(
|
47
|
+
"https://packages.typst.org/preview/{}-{}.tar.gz",
|
48
|
+
spec.name, spec.version
|
49
|
+
);
|
50
|
+
|
51
|
+
let data = match download(&url) {
|
52
|
+
Ok(data) => data,
|
53
|
+
Err(ureq::Error::Status(404, _)) => return Err(PackageError::NotFound(spec.clone())),
|
54
|
+
Err(err) => return Err(PackageError::NetworkFailed(Some(eco_format!("{err}")))),
|
55
|
+
};
|
56
|
+
|
57
|
+
let decompressed = flate2::read::GzDecoder::new(data.as_slice());
|
58
|
+
tar::Archive::new(decompressed)
|
59
|
+
.unpack(package_dir)
|
60
|
+
.map_err(|err| {
|
61
|
+
fs::remove_dir_all(package_dir).ok();
|
62
|
+
PackageError::MalformedArchive(Some(eco_format!("{err}")))
|
63
|
+
})
|
64
|
+
}
|
@@ -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
|
+
}
|