typst 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,4 @@
1
+ require "mkmf"
2
+ require "rb_sys/mkmf"
3
+
4
+ create_rust_makefile("typst/typst")
@@ -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
+ }