typst 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Cargo.toml +3 -0
- data/README.md +80 -0
- data/README.typ +83 -0
- data/Rakefile +24 -0
- data/ext/typst/Cargo.lock +2840 -0
- data/ext/typst/Cargo.toml +39 -0
- data/ext/typst/extconf.rb +4 -0
- data/ext/typst/src/compiler.rs +187 -0
- data/ext/typst/src/download.rs +79 -0
- data/ext/typst/src/fonts.rs +86 -0
- data/ext/typst/src/lib.rs +133 -0
- data/ext/typst/src/package.rs +64 -0
- data/ext/typst/src/world.rs +357 -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/typst.rb +115 -0
- metadata +86 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
[package]
|
2
|
+
name = "typst"
|
3
|
+
version = "0.0.1"
|
4
|
+
edition = "2021"
|
5
|
+
|
6
|
+
[lib]
|
7
|
+
crate-type = ["cdylib"]
|
8
|
+
|
9
|
+
[dependencies]
|
10
|
+
chrono = { version = "0.4.24", default-features = false, features = [
|
11
|
+
"clock",
|
12
|
+
"std",
|
13
|
+
] }
|
14
|
+
codespan-reporting = "0.11"
|
15
|
+
comemo = "0.3"
|
16
|
+
dirs = "5"
|
17
|
+
ecow = "0.2"
|
18
|
+
env_logger = "0.10.1"
|
19
|
+
env_proxy = "0.4"
|
20
|
+
filetime = "0.2.22"
|
21
|
+
flate2 = "1"
|
22
|
+
fontdb = "0.15.0"
|
23
|
+
log = "0.4.20"
|
24
|
+
magnus = { version = "0.6" }
|
25
|
+
pathdiff = "0.2"
|
26
|
+
same-file = "1"
|
27
|
+
siphasher = "1.0"
|
28
|
+
tar = "0.4"
|
29
|
+
typst = { git = "https://github.com/typst/typst.git", tag = "v0.9.0" }
|
30
|
+
typst-library = { git = "https://github.com/typst/typst.git", tag = "v0.9.0" }
|
31
|
+
ureq = { version = "2", default-features = false, features = [
|
32
|
+
"gzip",
|
33
|
+
"socks-proxy",
|
34
|
+
] }
|
35
|
+
walkdir = "2.4.0"
|
36
|
+
|
37
|
+
# enable rb-sys feature to test against Ruby head. This is only needed if you
|
38
|
+
# want to work with the unreleased, in-development, next version of Ruby
|
39
|
+
rb-sys = { version = "*", default-features = false, features = ["stable-api-compiled-fallback"] }
|
@@ -0,0 +1,187 @@
|
|
1
|
+
use chrono::{Datelike, Timelike};
|
2
|
+
use codespan_reporting::diagnostic::{Diagnostic, Label};
|
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};
|
7
|
+
use typst::syntax::{FileId, Source, Span};
|
8
|
+
use typst::{World, WorldExt};
|
9
|
+
|
10
|
+
use crate::world::SystemWorld;
|
11
|
+
|
12
|
+
type CodespanResult<T> = Result<T, CodespanError>;
|
13
|
+
type CodespanError = codespan_reporting::files::Error;
|
14
|
+
|
15
|
+
impl SystemWorld {
|
16
|
+
pub fn compile_pdf(&mut self) -> StrResult<Vec<u8>> {
|
17
|
+
// Reset everything and ensure that the main file is present.
|
18
|
+
self.reset();
|
19
|
+
self.source(self.main()).map_err(|err| err.to_string())?;
|
20
|
+
|
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()),
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
/// Export to a PDF.
|
50
|
+
#[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());
|
54
|
+
Ok(buffer)
|
55
|
+
}
|
56
|
+
|
57
|
+
/// Export to one or multiple SVGs.
|
58
|
+
#[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
|
+
}
|
65
|
+
Ok(buffer)
|
66
|
+
}
|
67
|
+
|
68
|
+
/// Get the current date and time in UTC.
|
69
|
+
fn now() -> Option<Datetime> {
|
70
|
+
let now = chrono::Local::now().naive_utc();
|
71
|
+
Datetime::from_ymd_hms(
|
72
|
+
now.year(),
|
73
|
+
now.month().try_into().ok()?,
|
74
|
+
now.day().try_into().ok()?,
|
75
|
+
now.hour().try_into().ok()?,
|
76
|
+
now.minute().try_into().ok()?,
|
77
|
+
now.second().try_into().ok()?,
|
78
|
+
)
|
79
|
+
}
|
80
|
+
|
81
|
+
/// Format diagnostic messages.\
|
82
|
+
pub fn format_diagnostics(
|
83
|
+
world: &SystemWorld,
|
84
|
+
errors: &[SourceDiagnostic],
|
85
|
+
warnings: &[SourceDiagnostic],
|
86
|
+
) -> Result<String, codespan_reporting::files::Error> {
|
87
|
+
let mut w = termcolor::Buffer::no_color();
|
88
|
+
|
89
|
+
let config = term::Config {
|
90
|
+
tab_width: 2,
|
91
|
+
..Default::default()
|
92
|
+
};
|
93
|
+
|
94
|
+
for diagnostic in warnings.iter().chain(errors.iter()) {
|
95
|
+
let diag = match diagnostic.severity {
|
96
|
+
Severity::Error => Diagnostic::error(),
|
97
|
+
Severity::Warning => Diagnostic::warning(),
|
98
|
+
}
|
99
|
+
.with_message(diagnostic.message.clone())
|
100
|
+
.with_notes(
|
101
|
+
diagnostic
|
102
|
+
.hints
|
103
|
+
.iter()
|
104
|
+
.map(|e| (eco_format!("hint: {e}")).into())
|
105
|
+
.collect(),
|
106
|
+
)
|
107
|
+
.with_labels(label(world, diagnostic.span).into_iter().collect());
|
108
|
+
|
109
|
+
term::emit(&mut w, &config, world, &diag)?;
|
110
|
+
|
111
|
+
// Stacktrace-like helper diagnostics.
|
112
|
+
for point in &diagnostic.trace {
|
113
|
+
let message = point.v.to_string();
|
114
|
+
let help = Diagnostic::help()
|
115
|
+
.with_message(message)
|
116
|
+
.with_labels(label(world, point.span).into_iter().collect());
|
117
|
+
|
118
|
+
term::emit(&mut w, &config, world, &help)?;
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
122
|
+
let s = String::from_utf8(w.into_inner()).unwrap();
|
123
|
+
Ok(s)
|
124
|
+
}
|
125
|
+
|
126
|
+
/// Create a label for a span.
|
127
|
+
fn label(world: &SystemWorld, span: Span) -> Option<Label<FileId>> {
|
128
|
+
Some(Label::primary(span.id()?, world.range(span)?))
|
129
|
+
}
|
130
|
+
|
131
|
+
impl<'a> codespan_reporting::files::Files<'a> for SystemWorld {
|
132
|
+
type FileId = FileId;
|
133
|
+
type Name = String;
|
134
|
+
type Source = Source;
|
135
|
+
|
136
|
+
fn name(&'a self, id: FileId) -> CodespanResult<Self::Name> {
|
137
|
+
let vpath = id.vpath();
|
138
|
+
Ok(if let Some(package) = id.package() {
|
139
|
+
format!("{package}{}", vpath.as_rooted_path().display())
|
140
|
+
} else {
|
141
|
+
// Try to express the path relative to the working directory.
|
142
|
+
vpath
|
143
|
+
.resolve(self.root())
|
144
|
+
.and_then(|abs| pathdiff::diff_paths(&abs, self.workdir()))
|
145
|
+
.as_deref()
|
146
|
+
.unwrap_or_else(|| vpath.as_rootless_path())
|
147
|
+
.to_string_lossy()
|
148
|
+
.into()
|
149
|
+
})
|
150
|
+
}
|
151
|
+
|
152
|
+
fn source(&'a self, id: FileId) -> CodespanResult<Self::Source> {
|
153
|
+
Ok(self.lookup(id))
|
154
|
+
}
|
155
|
+
|
156
|
+
fn line_index(&'a self, id: FileId, given: usize) -> CodespanResult<usize> {
|
157
|
+
let source = self.lookup(id);
|
158
|
+
source
|
159
|
+
.byte_to_line(given)
|
160
|
+
.ok_or_else(|| CodespanError::IndexTooLarge {
|
161
|
+
given,
|
162
|
+
max: source.len_bytes(),
|
163
|
+
})
|
164
|
+
}
|
165
|
+
|
166
|
+
fn line_range(&'a self, id: FileId, given: usize) -> CodespanResult<std::ops::Range<usize>> {
|
167
|
+
let source = self.lookup(id);
|
168
|
+
source
|
169
|
+
.line_to_range(given)
|
170
|
+
.ok_or_else(|| CodespanError::LineTooLarge {
|
171
|
+
given,
|
172
|
+
max: source.len_lines(),
|
173
|
+
})
|
174
|
+
}
|
175
|
+
|
176
|
+
fn column_number(&'a self, id: FileId, _: usize, given: usize) -> CodespanResult<usize> {
|
177
|
+
let source = self.lookup(id);
|
178
|
+
source.byte_to_column(given).ok_or_else(|| {
|
179
|
+
let max = source.len_bytes();
|
180
|
+
if given <= max {
|
181
|
+
CodespanError::InvalidCharBoundary { given }
|
182
|
+
} else {
|
183
|
+
CodespanError::IndexTooLarge { given, max }
|
184
|
+
}
|
185
|
+
})
|
186
|
+
}
|
187
|
+
}
|
@@ -0,0 +1,79 @@
|
|
1
|
+
use std::io::{self, ErrorKind, Read};
|
2
|
+
|
3
|
+
use ureq::Response;
|
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")));
|
10
|
+
|
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
|
+
}
|
18
|
+
|
19
|
+
let agent = builder.build();
|
20
|
+
let response = agent.get(url).call()?;
|
21
|
+
Ok(RemoteReader::from_response(response).download()?)
|
22
|
+
}
|
23
|
+
|
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>,
|
32
|
+
}
|
33
|
+
|
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
|
+
}
|
79
|
+
}
|
@@ -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,133 @@
|
|
1
|
+
use std::path::PathBuf;
|
2
|
+
|
3
|
+
use magnus::{define_module, function, exception, Error, IntoValue};
|
4
|
+
use magnus::{prelude::*};
|
5
|
+
|
6
|
+
use world::SystemWorld;
|
7
|
+
|
8
|
+
mod compiler;
|
9
|
+
mod download;
|
10
|
+
mod fonts;
|
11
|
+
mod package;
|
12
|
+
mod world;
|
13
|
+
|
14
|
+
fn to_svg(
|
15
|
+
input: PathBuf,
|
16
|
+
root: Option<PathBuf>,
|
17
|
+
font_paths: Vec<PathBuf>,
|
18
|
+
resource_path: PathBuf,
|
19
|
+
) -> Result<Vec<String>, Error> {
|
20
|
+
let input = input.canonicalize()
|
21
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
|
22
|
+
|
23
|
+
let root = if let Some(root) = root {
|
24
|
+
root.canonicalize()
|
25
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
|
26
|
+
} else if let Some(dir) = input.parent() {
|
27
|
+
dir.into()
|
28
|
+
} else {
|
29
|
+
PathBuf::new()
|
30
|
+
};
|
31
|
+
|
32
|
+
let resource_path = resource_path.canonicalize()
|
33
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
|
34
|
+
|
35
|
+
let mut default_fonts = Vec::new();
|
36
|
+
for entry in walkdir::WalkDir::new(resource_path.join("fonts")) {
|
37
|
+
let path = entry
|
38
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
|
39
|
+
.into_path();
|
40
|
+
let Some(extension) = path.extension() else {
|
41
|
+
continue;
|
42
|
+
};
|
43
|
+
if extension == "ttf" || extension == "otf" {
|
44
|
+
default_fonts.push(path);
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
let mut world = SystemWorld::builder(root, input)
|
49
|
+
.font_paths(font_paths)
|
50
|
+
.font_files(default_fonts)
|
51
|
+
.build()
|
52
|
+
.map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
|
53
|
+
|
54
|
+
let svg_bytes = world
|
55
|
+
.compile_svg()
|
56
|
+
.map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
|
57
|
+
|
58
|
+
Ok(svg_bytes)
|
59
|
+
}
|
60
|
+
|
61
|
+
fn to_pdf(
|
62
|
+
input: PathBuf,
|
63
|
+
root: Option<PathBuf>,
|
64
|
+
font_paths: Vec<PathBuf>,
|
65
|
+
resource_path: PathBuf,
|
66
|
+
) -> Result<Vec<u8>, Error> {
|
67
|
+
let input = input.canonicalize()
|
68
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
|
69
|
+
|
70
|
+
let root = if let Some(root) = root {
|
71
|
+
root.canonicalize()
|
72
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
|
73
|
+
} else if let Some(dir) = input.parent() {
|
74
|
+
dir.into()
|
75
|
+
} else {
|
76
|
+
PathBuf::new()
|
77
|
+
};
|
78
|
+
|
79
|
+
let resource_path = resource_path.canonicalize()
|
80
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?;
|
81
|
+
|
82
|
+
let mut default_fonts = Vec::new();
|
83
|
+
for entry in walkdir::WalkDir::new(resource_path.join("fonts")) {
|
84
|
+
let path = entry
|
85
|
+
.map_err(|err| magnus::Error::new(exception::arg_error(), err.to_string()))?
|
86
|
+
.into_path();
|
87
|
+
let Some(extension) = path.extension() else {
|
88
|
+
continue;
|
89
|
+
};
|
90
|
+
if extension == "ttf" || extension == "otf" {
|
91
|
+
default_fonts.push(path);
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
let mut world = SystemWorld::builder(root, input)
|
96
|
+
.font_paths(font_paths)
|
97
|
+
.font_files(default_fonts)
|
98
|
+
.build()
|
99
|
+
.map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
|
100
|
+
|
101
|
+
let pdf_bytes = world
|
102
|
+
.compile_pdf()
|
103
|
+
.map_err(|msg| magnus::Error::new(exception::arg_error(), msg.to_string()))?;
|
104
|
+
|
105
|
+
Ok(pdf_bytes)
|
106
|
+
}
|
107
|
+
|
108
|
+
fn write_pdf(
|
109
|
+
input: PathBuf,
|
110
|
+
output: PathBuf,
|
111
|
+
root: Option<PathBuf>,
|
112
|
+
font_paths: Vec<PathBuf>,
|
113
|
+
resource_path: PathBuf,
|
114
|
+
) -> Result<magnus::Value, Error> {
|
115
|
+
let pdf_bytes = to_pdf(input, root, font_paths, resource_path)?;
|
116
|
+
|
117
|
+
std::fs::write(output, pdf_bytes)
|
118
|
+
.map_err(|_| magnus::Error::new(exception::arg_error(), "error"))?;
|
119
|
+
|
120
|
+
let value = true.into_value();
|
121
|
+
Ok(value)
|
122
|
+
}
|
123
|
+
|
124
|
+
#[magnus::init]
|
125
|
+
fn init() -> Result<(), Error> {
|
126
|
+
env_logger::init();
|
127
|
+
|
128
|
+
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))?;
|
132
|
+
Ok(())
|
133
|
+
}
|
@@ -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
|
+
}
|