typst-rb 0.15.0.0.3
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.lock +4136 -0
- data/Cargo.toml +3 -0
- data/LICENSE +176 -0
- data/README.md +200 -0
- data/README.typ +208 -0
- data/Rakefile +43 -0
- data/ext/typst/Cargo.toml +55 -0
- data/ext/typst/extconf.rb +4 -0
- data/ext/typst/src/compiler.rs +257 -0
- data/ext/typst/src/download.rs +7 -0
- data/ext/typst/src/lib.rs +296 -0
- data/ext/typst/src/package.rs +64 -0
- data/ext/typst/src/query.rs +138 -0
- data/ext/typst/src/world.rs +400 -0
- data/lib/base.rb +142 -0
- data/lib/document.rb +37 -0
- data/lib/formats/html_experimental.rb +13 -0
- data/lib/formats/pdf.rb +13 -0
- data/lib/formats/png.rb +13 -0
- data/lib/formats/svg.rb +13 -0
- data/lib/query.rb +22 -0
- data/lib/typst-rb.rb +6 -0
- data/lib/typst.rb +101 -0
- metadata +126 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
use chrono::{Datelike, Timelike};
|
|
2
|
+
use codespan_reporting::diagnostic::{Diagnostic, Label};
|
|
3
|
+
use codespan_reporting::term::{self, termcolor};
|
|
4
|
+
use ecow::{eco_format, EcoString};
|
|
5
|
+
use typst::diag::{At, Severity, SourceDiagnostic, StrResult, Warned};
|
|
6
|
+
use typst::foundations::Datetime;
|
|
7
|
+
use typst_html::HtmlDocument;
|
|
8
|
+
use typst_layout::PagedDocument;
|
|
9
|
+
use typst::syntax::{DiagSpan, FileId, Lines, Span};
|
|
10
|
+
use typst::{World, WorldExt};
|
|
11
|
+
|
|
12
|
+
use crate::world::SystemWorld;
|
|
13
|
+
|
|
14
|
+
type CodespanResult<T> = Result<T, CodespanError>;
|
|
15
|
+
type CodespanError = codespan_reporting::files::Error;
|
|
16
|
+
|
|
17
|
+
impl SystemWorld {
|
|
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>>> {
|
|
24
|
+
// Reset everything and ensure that the main file is present.
|
|
25
|
+
self.reset();
|
|
26
|
+
self.source(self.main()).map_err(|err| err.to_string())?;
|
|
27
|
+
|
|
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
|
+
.map_err(|e| eco_format!("PDF standards error: {:?}", e))
|
|
50
|
+
.at(Span::detached()).unwrap()
|
|
51
|
+
)?]),
|
|
52
|
+
"png" => Ok(export_image(&document, ImageExportFormat::Png, ppi)?),
|
|
53
|
+
"svg" => Ok(export_image(&document, ImageExportFormat::Svg, ppi)?),
|
|
54
|
+
fmt => Err(eco_format!("unknown format: {fmt}")),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
Err(errors) => Err(format_diagnostics(self, &errors, &warnings).unwrap().into()),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Export to a PDF.
|
|
65
|
+
#[inline]
|
|
66
|
+
fn export_html(
|
|
67
|
+
document: &HtmlDocument,
|
|
68
|
+
world: &SystemWorld,
|
|
69
|
+
) -> StrResult<Vec<u8>> {
|
|
70
|
+
let html = typst_html::html(document, &typst_html::HtmlOptions::default())
|
|
71
|
+
.map_err(|e| match format_diagnostics(world, &e, &[]) {
|
|
72
|
+
Ok(e) => EcoString::from(e),
|
|
73
|
+
Err(err) => eco_format!("failed to print diagnostics ({err})"),
|
|
74
|
+
})?;
|
|
75
|
+
let buffer = html.as_bytes().to_vec();
|
|
76
|
+
Ok(buffer)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Export to a PDF.
|
|
80
|
+
#[inline]
|
|
81
|
+
fn export_pdf(
|
|
82
|
+
document: &PagedDocument,
|
|
83
|
+
world: &SystemWorld,
|
|
84
|
+
standards: typst_pdf::PdfStandards,
|
|
85
|
+
) -> StrResult<Vec<u8>> {
|
|
86
|
+
let buffer = typst_pdf::pdf(
|
|
87
|
+
document,
|
|
88
|
+
&typst_pdf::PdfOptions {
|
|
89
|
+
ident: typst::foundations::Smart::Auto,
|
|
90
|
+
timestamp: now().map(typst_pdf::Timestamp::new_utc),
|
|
91
|
+
standards,
|
|
92
|
+
..Default::default()
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
.map_err(|e| match format_diagnostics(world, &e, &[]) {
|
|
96
|
+
Ok(e) => EcoString::from(e),
|
|
97
|
+
Err(err) => eco_format!("failed to print diagnostics ({err})"),
|
|
98
|
+
})?;
|
|
99
|
+
Ok(buffer.into())
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Get the current date and time in UTC.
|
|
103
|
+
fn now() -> Option<Datetime> {
|
|
104
|
+
let now = chrono::Local::now().naive_utc();
|
|
105
|
+
Datetime::from_ymd_hms(
|
|
106
|
+
now.year(),
|
|
107
|
+
now.month().try_into().ok()?,
|
|
108
|
+
now.day().try_into().ok()?,
|
|
109
|
+
now.hour().try_into().ok()?,
|
|
110
|
+
now.minute().try_into().ok()?,
|
|
111
|
+
now.second().try_into().ok()?,
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// An image format to export in.
|
|
116
|
+
enum ImageExportFormat {
|
|
117
|
+
Png,
|
|
118
|
+
Svg,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Export the frames to PNGs or SVGs.
|
|
122
|
+
fn export_image(
|
|
123
|
+
document: &PagedDocument,
|
|
124
|
+
fmt: ImageExportFormat,
|
|
125
|
+
ppi: Option<f32>,
|
|
126
|
+
) -> StrResult<Vec<Vec<u8>>> {
|
|
127
|
+
let mut buffers = Vec::new();
|
|
128
|
+
for page in document.pages() {
|
|
129
|
+
let buffer = match fmt {
|
|
130
|
+
ImageExportFormat::Png => typst_render::render(
|
|
131
|
+
page,
|
|
132
|
+
&typst_render::RenderOptions {
|
|
133
|
+
pixel_per_pt: typst::utils::Scalar::new(f64::from(ppi.unwrap_or(144.0) / 72.0)),
|
|
134
|
+
render_bleed: false,
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
.encode_png()
|
|
138
|
+
.map_err(|err| eco_format!("failed to write PNG file ({err})"))?,
|
|
139
|
+
ImageExportFormat::Svg => {
|
|
140
|
+
let svg = typst_svg::svg(page, &typst_svg::SvgOptions::default());
|
|
141
|
+
svg.as_bytes().to_vec()
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
buffers.push(buffer);
|
|
145
|
+
}
|
|
146
|
+
Ok(buffers)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// Format diagnostic messages.\
|
|
150
|
+
pub fn format_diagnostics(
|
|
151
|
+
world: &SystemWorld,
|
|
152
|
+
errors: &[SourceDiagnostic],
|
|
153
|
+
warnings: &[SourceDiagnostic],
|
|
154
|
+
) -> Result<String, codespan_reporting::files::Error> {
|
|
155
|
+
let mut w = termcolor::Buffer::no_color();
|
|
156
|
+
|
|
157
|
+
let config = term::Config {
|
|
158
|
+
tab_width: 2,
|
|
159
|
+
..Default::default()
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
for diagnostic in warnings.iter().chain(errors.iter()) {
|
|
163
|
+
let diag = match diagnostic.severity {
|
|
164
|
+
Severity::Error => Diagnostic::error(),
|
|
165
|
+
Severity::Warning => Diagnostic::warning(),
|
|
166
|
+
}
|
|
167
|
+
.with_message(diagnostic.message.clone())
|
|
168
|
+
.with_notes(
|
|
169
|
+
diagnostic
|
|
170
|
+
.hints
|
|
171
|
+
.iter()
|
|
172
|
+
.map(|e| (eco_format!("hint: {}", e.v)).into())
|
|
173
|
+
.collect(),
|
|
174
|
+
)
|
|
175
|
+
.with_labels(label(world, diagnostic.span).into_iter().collect());
|
|
176
|
+
|
|
177
|
+
term::emit_to_write_style(&mut w, &config, world, &diag)?;
|
|
178
|
+
|
|
179
|
+
// Stacktrace-like helper diagnostics.
|
|
180
|
+
for point in &diagnostic.trace {
|
|
181
|
+
let message = point.v.to_string();
|
|
182
|
+
let help = Diagnostic::help()
|
|
183
|
+
.with_message(message)
|
|
184
|
+
.with_labels(label(world, point.span).into_iter().collect());
|
|
185
|
+
|
|
186
|
+
term::emit_to_write_style(&mut w, &config, world, &help)?;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let s = String::from_utf8(w.into_inner()).unwrap();
|
|
191
|
+
Ok(s)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// Create a label for a span.
|
|
195
|
+
fn label(world: &SystemWorld, span: impl Into<DiagSpan>) -> Option<Label<FileId>> {
|
|
196
|
+
let span = span.into();
|
|
197
|
+
Some(Label::primary(span.id()?, world.range(span)?))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
impl<'a> codespan_reporting::files::Files<'a> for SystemWorld {
|
|
201
|
+
type FileId = FileId;
|
|
202
|
+
type Name = String;
|
|
203
|
+
type Source = Lines<String>;
|
|
204
|
+
|
|
205
|
+
fn name(&'a self, id: FileId) -> CodespanResult<Self::Name> {
|
|
206
|
+
let vpath = id.vpath();
|
|
207
|
+
Ok(if let typst::syntax::VirtualRoot::Package(package) = id.root() {
|
|
208
|
+
format!("{package}{}", vpath.get_with_slash())
|
|
209
|
+
} else {
|
|
210
|
+
// Try to express the path relative to the working directory.
|
|
211
|
+
vpath
|
|
212
|
+
.realize(self.root())
|
|
213
|
+
.ok()
|
|
214
|
+
.and_then(|abs| pathdiff::diff_paths(abs, self.workdir()))
|
|
215
|
+
.as_deref()
|
|
216
|
+
.unwrap_or_else(|| std::path::Path::new(vpath.get_without_slash()))
|
|
217
|
+
.to_string_lossy()
|
|
218
|
+
.into()
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fn source(&'a self, id: FileId) -> CodespanResult<Self::Source> {
|
|
223
|
+
Ok(self.lookup(id))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fn line_index(&'a self, id: FileId, given: usize) -> CodespanResult<usize> {
|
|
227
|
+
let source = self.lookup(id);
|
|
228
|
+
source
|
|
229
|
+
.byte_to_line(given)
|
|
230
|
+
.ok_or_else(|| CodespanError::IndexTooLarge {
|
|
231
|
+
given,
|
|
232
|
+
max: source.len_bytes(),
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fn line_range(&'a self, id: FileId, given: usize) -> CodespanResult<std::ops::Range<usize>> {
|
|
237
|
+
let source = self.lookup(id);
|
|
238
|
+
source
|
|
239
|
+
.line_to_range(given)
|
|
240
|
+
.ok_or_else(|| CodespanError::LineTooLarge {
|
|
241
|
+
given,
|
|
242
|
+
max: source.len_lines(),
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
fn column_number(&'a self, id: FileId, _: usize, given: usize) -> CodespanResult<usize> {
|
|
247
|
+
let source = self.lookup(id);
|
|
248
|
+
source.byte_to_column(given).ok_or_else(|| {
|
|
249
|
+
let max = source.len_bytes();
|
|
250
|
+
if given <= max {
|
|
251
|
+
CodespanError::InvalidCharBoundary { given }
|
|
252
|
+
} else {
|
|
253
|
+
CodespanError::IndexTooLarge { given, max }
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
use std::path::PathBuf;
|
|
2
|
+
|
|
3
|
+
use magnus::{function, prelude::*, Error, Ruby};
|
|
4
|
+
|
|
5
|
+
use std::collections::HashMap;
|
|
6
|
+
use query::{query as typst_query, QueryCommand, SerializationFormat};
|
|
7
|
+
use typst::foundations::{Dict, Value};
|
|
8
|
+
use typst_library::Feature;
|
|
9
|
+
use typst_pdf::PdfStandard;
|
|
10
|
+
use world::SystemWorld;
|
|
11
|
+
|
|
12
|
+
mod compiler;
|
|
13
|
+
mod download;
|
|
14
|
+
mod query;
|
|
15
|
+
mod world;
|
|
16
|
+
|
|
17
|
+
fn to_html(
|
|
18
|
+
ruby: &Ruby,
|
|
19
|
+
input: PathBuf,
|
|
20
|
+
root: Option<PathBuf>,
|
|
21
|
+
font_paths: Vec<PathBuf>,
|
|
22
|
+
ignore_system_fonts: bool,
|
|
23
|
+
ignore_embedded_fonts: bool,
|
|
24
|
+
sys_inputs: HashMap<String, String>,
|
|
25
|
+
) -> Result<Vec<Vec<u8>>, Error> {
|
|
26
|
+
let input = input.canonicalize()
|
|
27
|
+
.map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?;
|
|
28
|
+
|
|
29
|
+
let root = if let Some(root) = root {
|
|
30
|
+
root.canonicalize()
|
|
31
|
+
.map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?
|
|
32
|
+
} else if let Some(dir) = input.parent() {
|
|
33
|
+
dir.into()
|
|
34
|
+
} else {
|
|
35
|
+
PathBuf::new()
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let mut features = Vec::new();
|
|
39
|
+
features.push(Feature::Html);
|
|
40
|
+
|
|
41
|
+
let feat = features.iter()
|
|
42
|
+
.map(|&feature|
|
|
43
|
+
match feature {
|
|
44
|
+
Feature::Html => typst::Feature::Html,
|
|
45
|
+
_ => typst::Feature::Html // TODO: fix this hack
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
.collect();
|
|
49
|
+
|
|
50
|
+
let mut world = SystemWorld::builder(root, input)
|
|
51
|
+
.inputs(Dict::from_iter(
|
|
52
|
+
sys_inputs
|
|
53
|
+
.into_iter()
|
|
54
|
+
.map(|(k, v)| (k.into(), Value::Str(v.into()))),
|
|
55
|
+
))
|
|
56
|
+
.features(feat)
|
|
57
|
+
.font_paths(font_paths)
|
|
58
|
+
.ignore_system_fonts(ignore_system_fonts)
|
|
59
|
+
.ignore_embedded_fonts(ignore_embedded_fonts)
|
|
60
|
+
.build()
|
|
61
|
+
.map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
|
|
62
|
+
|
|
63
|
+
let bytes = world
|
|
64
|
+
.compile(Some("html"), None, &Vec::new())
|
|
65
|
+
.map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
|
|
66
|
+
|
|
67
|
+
Ok(bytes)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn to_svg(
|
|
71
|
+
ruby: &Ruby,
|
|
72
|
+
input: PathBuf,
|
|
73
|
+
root: Option<PathBuf>,
|
|
74
|
+
font_paths: Vec<PathBuf>,
|
|
75
|
+
ignore_system_fonts: bool,
|
|
76
|
+
ignore_embedded_fonts: bool,
|
|
77
|
+
sys_inputs: HashMap<String, String>,
|
|
78
|
+
) -> Result<Vec<Vec<u8>>, Error> {
|
|
79
|
+
let input = input.canonicalize()
|
|
80
|
+
.map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?;
|
|
81
|
+
|
|
82
|
+
let root = if let Some(root) = root {
|
|
83
|
+
root.canonicalize()
|
|
84
|
+
.map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?
|
|
85
|
+
} else if let Some(dir) = input.parent() {
|
|
86
|
+
dir.into()
|
|
87
|
+
} else {
|
|
88
|
+
PathBuf::new()
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
let mut world = SystemWorld::builder(root, input)
|
|
92
|
+
.inputs(Dict::from_iter(
|
|
93
|
+
sys_inputs
|
|
94
|
+
.into_iter()
|
|
95
|
+
.map(|(k, v)| (k.into(), Value::Str(v.into()))),
|
|
96
|
+
))
|
|
97
|
+
.font_paths(font_paths)
|
|
98
|
+
.ignore_system_fonts(ignore_system_fonts)
|
|
99
|
+
.ignore_embedded_fonts(ignore_embedded_fonts)
|
|
100
|
+
.build()
|
|
101
|
+
.map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
|
|
102
|
+
|
|
103
|
+
let svg_bytes = world
|
|
104
|
+
.compile(Some("svg"), None, &Vec::new())
|
|
105
|
+
.map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
|
|
106
|
+
|
|
107
|
+
Ok(svg_bytes)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fn to_png(
|
|
111
|
+
ruby: &Ruby,
|
|
112
|
+
input: PathBuf,
|
|
113
|
+
root: Option<PathBuf>,
|
|
114
|
+
font_paths: Vec<PathBuf>,
|
|
115
|
+
ignore_system_fonts: bool,
|
|
116
|
+
ignore_embedded_fonts: bool,
|
|
117
|
+
sys_inputs: HashMap<String, String>,
|
|
118
|
+
ppi: Option<f32>,
|
|
119
|
+
) -> Result<Vec<Vec<u8>>, Error> {
|
|
120
|
+
let input = input.canonicalize()
|
|
121
|
+
.map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?;
|
|
122
|
+
|
|
123
|
+
let root = if let Some(root) = root {
|
|
124
|
+
root.canonicalize()
|
|
125
|
+
.map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?
|
|
126
|
+
} else if let Some(dir) = input.parent() {
|
|
127
|
+
dir.into()
|
|
128
|
+
} else {
|
|
129
|
+
PathBuf::new()
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
let mut world = SystemWorld::builder(root, input)
|
|
133
|
+
.inputs(Dict::from_iter(
|
|
134
|
+
sys_inputs
|
|
135
|
+
.into_iter()
|
|
136
|
+
.map(|(k, v)| (k.into(), Value::Str(v.into()))),
|
|
137
|
+
))
|
|
138
|
+
.font_paths(font_paths)
|
|
139
|
+
.ignore_system_fonts(ignore_system_fonts)
|
|
140
|
+
.ignore_embedded_fonts(ignore_embedded_fonts)
|
|
141
|
+
.build()
|
|
142
|
+
.map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
|
|
143
|
+
|
|
144
|
+
let bytes = world
|
|
145
|
+
.compile(Some("png"), ppi, &Vec::new())
|
|
146
|
+
.map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
|
|
147
|
+
|
|
148
|
+
Ok(bytes)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fn to_pdf(
|
|
152
|
+
ruby: &Ruby,
|
|
153
|
+
input: PathBuf,
|
|
154
|
+
root: Option<PathBuf>,
|
|
155
|
+
font_paths: Vec<PathBuf>,
|
|
156
|
+
ignore_system_fonts: bool,
|
|
157
|
+
ignore_embedded_fonts: bool,
|
|
158
|
+
sys_inputs: HashMap<String, String>,
|
|
159
|
+
pdf_standards: Vec<String>,
|
|
160
|
+
) -> Result<Vec<Vec<u8>>, Error> {
|
|
161
|
+
let input = input.canonicalize()
|
|
162
|
+
.map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?;
|
|
163
|
+
|
|
164
|
+
let root = if let Some(root) = root {
|
|
165
|
+
root.canonicalize()
|
|
166
|
+
.map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?
|
|
167
|
+
} else if let Some(dir) = input.parent() {
|
|
168
|
+
dir.into()
|
|
169
|
+
} else {
|
|
170
|
+
PathBuf::new()
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
let mut world = SystemWorld::builder(root, input)
|
|
174
|
+
.inputs(Dict::from_iter(
|
|
175
|
+
sys_inputs
|
|
176
|
+
.into_iter()
|
|
177
|
+
.map(|(k, v)| (k.into(), Value::Str(v.into()))),
|
|
178
|
+
))
|
|
179
|
+
.font_paths(font_paths)
|
|
180
|
+
.ignore_system_fonts(ignore_system_fonts)
|
|
181
|
+
.ignore_embedded_fonts(ignore_embedded_fonts)
|
|
182
|
+
.build()
|
|
183
|
+
.map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
|
|
184
|
+
|
|
185
|
+
let pdf_standards_lookup: HashMap::<&str, PdfStandard> = HashMap::from([
|
|
186
|
+
("1.4", PdfStandard::V_1_4),
|
|
187
|
+
("1.5", PdfStandard::V_1_5),
|
|
188
|
+
("1.6", PdfStandard::V_1_6),
|
|
189
|
+
("1.7", PdfStandard::V_1_7),
|
|
190
|
+
("2.0", PdfStandard::V_2_0),
|
|
191
|
+
("a-1a", PdfStandard::A_1a),
|
|
192
|
+
("a-1b", PdfStandard::A_1b),
|
|
193
|
+
("a-2a", PdfStandard::A_2a),
|
|
194
|
+
("a-2b", PdfStandard::A_2b),
|
|
195
|
+
("a-2u", PdfStandard::A_2u),
|
|
196
|
+
("a-3a", PdfStandard::A_3a),
|
|
197
|
+
("a-3b", PdfStandard::A_3b),
|
|
198
|
+
("a-3u", PdfStandard::A_3u),
|
|
199
|
+
("a-4", PdfStandard::A_4),
|
|
200
|
+
("a-4e", PdfStandard::A_4e),
|
|
201
|
+
("a-4f", PdfStandard::A_4f),
|
|
202
|
+
("ua-1", PdfStandard::Ua_1),
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
let mut pdf_standards_vec = Vec::<PdfStandard>::new();
|
|
206
|
+
for pdf_standard in pdf_standards.iter() {
|
|
207
|
+
let result = pdf_standards_lookup.get(pdf_standard.as_str());
|
|
208
|
+
match result {
|
|
209
|
+
Some(value) => pdf_standards_vec.push(*value),
|
|
210
|
+
_ => return Err(magnus::Error::new(ruby.exception_arg_error(), "Unknown PdfStandard")),
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let pdf_bytes = world
|
|
215
|
+
.compile(Some("pdf"), None, &pdf_standards_vec)
|
|
216
|
+
.map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
|
|
217
|
+
|
|
218
|
+
Ok(pdf_bytes)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fn query(
|
|
222
|
+
ruby: &Ruby,
|
|
223
|
+
selector: String,
|
|
224
|
+
field: Option<String>,
|
|
225
|
+
one: bool,
|
|
226
|
+
format: Option<String>,
|
|
227
|
+
input: PathBuf,
|
|
228
|
+
root: Option<PathBuf>,
|
|
229
|
+
font_paths: Vec<PathBuf>,
|
|
230
|
+
ignore_system_fonts: bool,
|
|
231
|
+
ignore_embedded_fonts: bool,
|
|
232
|
+
sys_inputs: HashMap<String, String>,
|
|
233
|
+
) -> Result<String, Error> {
|
|
234
|
+
let format = match format.unwrap().to_ascii_lowercase().as_str() {
|
|
235
|
+
"json" => SerializationFormat::Json,
|
|
236
|
+
"yaml" => SerializationFormat::Yaml,
|
|
237
|
+
_ => return Err(magnus::Error::new(ruby.exception_arg_error(), "unsupported serialization format"))?,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
let input = input.canonicalize()
|
|
241
|
+
.map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?;
|
|
242
|
+
|
|
243
|
+
let root = if let Some(root) = root {
|
|
244
|
+
root.canonicalize()
|
|
245
|
+
.map_err(|err| magnus::Error::new(ruby.exception_arg_error(), err.to_string()))?
|
|
246
|
+
} else if let Some(dir) = input.parent() {
|
|
247
|
+
dir.into()
|
|
248
|
+
} else {
|
|
249
|
+
PathBuf::new()
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
let mut world = SystemWorld::builder(root, input)
|
|
253
|
+
.inputs(Dict::from_iter(
|
|
254
|
+
sys_inputs
|
|
255
|
+
.into_iter()
|
|
256
|
+
.map(|(k, v)| (k.into(), Value::Str(v.into()))),
|
|
257
|
+
))
|
|
258
|
+
.font_paths(font_paths)
|
|
259
|
+
.ignore_system_fonts(ignore_system_fonts)
|
|
260
|
+
.ignore_embedded_fonts(ignore_embedded_fonts)
|
|
261
|
+
.build()
|
|
262
|
+
.map_err(|msg| magnus::Error::new(ruby.exception_arg_error(), msg.to_string()))?;
|
|
263
|
+
|
|
264
|
+
let result = typst_query(
|
|
265
|
+
&mut world,
|
|
266
|
+
&QueryCommand {
|
|
267
|
+
selector: selector.into(),
|
|
268
|
+
field: field.map(Into::into),
|
|
269
|
+
one,
|
|
270
|
+
format,
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
match result {
|
|
275
|
+
Ok(data) => Ok(data),
|
|
276
|
+
Err(msg) => Err(magnus::Error::new(ruby.exception_arg_error(), msg.to_string())),
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
fn clear_cache(_ruby: &Ruby, max_age: usize) {
|
|
281
|
+
comemo::evict(max_age);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#[magnus::init]
|
|
285
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
286
|
+
env_logger::init();
|
|
287
|
+
|
|
288
|
+
let module = ruby.define_module("Typst")?;
|
|
289
|
+
module.define_singleton_method("_to_pdf", function!(to_pdf, 7))?;
|
|
290
|
+
module.define_singleton_method("_to_svg", function!(to_svg, 6))?;
|
|
291
|
+
module.define_singleton_method("_to_png", function!(to_png, 7))?;
|
|
292
|
+
module.define_singleton_method("_to_html", function!(to_html, 6))?;
|
|
293
|
+
module.define_singleton_method("_query", function!(query, 10))?;
|
|
294
|
+
module.define_singleton_method("_clear_cache", function!(clear_cache, 1))?;
|
|
295
|
+
Ok(())
|
|
296
|
+
}
|
|
@@ -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
|
+
}
|