typst 0.0.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.
- 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
|
+
}
|