typst 0.13.5 → 0.14.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e4b47eadd66459fc03839fddfb0d95a2757a63c5ed6475bf0000ac6a8edf72b
4
- data.tar.gz: 2bfe1a0c9e7d542d6f64d59de0c777a86a3c56f11962c997f6a0e378e5a348d2
3
+ metadata.gz: 5e9d8a961dbfb1f5c4b2abcc506c2a4b8d51aa65bc628837f853c1d1cf27b769
4
+ data.tar.gz: c9268f5475181810a42292ae8f85df0d8943462e48912568ba37e1896071b689
5
5
  SHA512:
6
- metadata.gz: e5626d390a3e6f3d844b0e91e3df609d9a167d2b1b496634b50133110b57ed7a2756db3c6ee23de327849d22ddc5d45a9088af576a26512d49afef31e0b0aae6
7
- data.tar.gz: ff150b69ed67845d4a4a48f1b1bda2421905dc7a035270f1ae6a5ec8ad2911523499bb23256b1e2aa10f0c5ab51f74f27201e33b2f6827c3db4f4adb1e630870
6
+ metadata.gz: bb05e491212ca31f0d4b9dd61fd42338117abc3b0f692c18767763705c592ebcbbc2b55592c3f3f9465183e9979390e462d635c070cbd548b48ac47c72dd4c8d
7
+ data.tar.gz: 0e7efcc12d9b6ad8720441b604ec6c31053341ec5094544d7dcb2313577afd344c5bbf386d37105f06988cd80fcea6b7c324a19f699ef580b0d18a9208db6d89
data/Rakefile CHANGED
@@ -40,8 +40,8 @@ task 'gem:native' do |t|
40
40
  end
41
41
 
42
42
  task 'gem:native:push' do |t|
43
+ sh "gem signin"
43
44
  CROSS_PLATFORMS.each do |platform|
44
- sh "gem signin"
45
45
  sh "gem push pkg/typst-#{spec.version}-#{platform}.gem"
46
46
  end
47
47
  end
data/ext/typst/Cargo.toml CHANGED
@@ -1,18 +1,18 @@
1
1
  [package]
2
2
  name = "typst"
3
- version = "0.13.5"
3
+ version = "0.14.2"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
7
7
  crate-type = ["cdylib"]
8
8
 
9
9
  [dependencies]
10
- chrono = { version = "0.4.38", default-features = false, features = [
10
+ chrono = { version = "0.4.42", default-features = false, features = [
11
11
  "clock",
12
12
  "std",
13
13
  ] }
14
- codespan-reporting = "0.11"
15
- comemo = "0.4"
14
+ codespan-reporting = "0.13"
15
+ comemo = "0.5.0"
16
16
  dirs = "5" #
17
17
  ecow = "0.2"
18
18
  env_logger = "0.10.1" #
@@ -28,21 +28,21 @@ siphasher = "1.0" #
28
28
  tar = "0.4" #
29
29
  #typst = { git = "https://github.com/typst/typst.git", tag = "v0.13.0" }
30
30
  #typst-library = { git = "https://github.com/typst/typst.git", tag = "v0.13.0" }
31
- serde = { version = "1.0.217", features = ["derive"] }
31
+ serde = { version = "1.0.228", features = ["derive"] }
32
32
  serde_json = "1"
33
33
  serde_yaml = "0.9"
34
- typst = "0.13.1"
35
- typst-library = "0.13.1"
36
- typst-kit = { version = "0.13.1", features = [
34
+ typst = "0.14.2"
35
+ typst-library = "0.14.2"
36
+ typst-kit = { version = "0.14.2", features = [
37
37
  "downloads",
38
38
  "embed-fonts",
39
39
  "vendor-openssl",
40
40
  ] }
41
- typst-pdf = "0.13.1"
42
- typst-svg = "0.13.1"
43
- typst-html = "0.13.1"
44
- typst-render = "0.13.1"
45
- typst-eval = "0.13.1"
41
+ typst-pdf = "0.14.2"
42
+ typst-svg = "0.14.2"
43
+ typst-html = "0.14.2"
44
+ typst-render = "0.14.2"
45
+ typst-eval = "0.14.2"
46
46
  ureq = { version = "2", default-features = false, features = [
47
47
  "gzip",
48
48
  "socks-proxy",
@@ -51,4 +51,4 @@ walkdir = "2.4.0"
51
51
 
52
52
  # enable rb-sys feature to test against Ruby head. This is only needed if you
53
53
  # want to work with the unreleased, in-development, next version of Ruby
54
- rb-sys = { version = "0.9.116", default-features = false, features = ["stable-api-compiled-fallback"] }
54
+ rb-sys = { version = "0.9.119", default-features = false, features = ["stable-api-compiled-fallback"] }
@@ -4,9 +4,10 @@ use codespan_reporting::term::{self, termcolor};
4
4
  use ecow::{eco_format, EcoString};
5
5
  use typst::diag::{Severity, SourceDiagnostic, StrResult, Warned};
6
6
  use typst::foundations::Datetime;
7
- use typst::html::HtmlDocument;
7
+ use typst_html::HtmlDocument;
8
8
  use typst::layout::PagedDocument;
9
- use typst::syntax::{FileId, Source, Span};
9
+ //use typst::syntax::{FileId, Source, Span};
10
+ use typst::syntax::{FileId, Lines, Span};
10
11
  use typst::{World, WorldExt};
11
12
 
12
13
  use crate::world::SystemWorld;
@@ -167,7 +168,7 @@ pub fn format_diagnostics(
167
168
  )
168
169
  .with_labels(label(world, diagnostic.span).into_iter().collect());
169
170
 
170
- term::emit(&mut w, &config, world, &diag)?;
171
+ term::emit_to_write_style(&mut w, &config, world, &diag)?;
171
172
 
172
173
  // Stacktrace-like helper diagnostics.
173
174
  for point in &diagnostic.trace {
@@ -176,7 +177,7 @@ pub fn format_diagnostics(
176
177
  .with_message(message)
177
178
  .with_labels(label(world, point.span).into_iter().collect());
178
179
 
179
- term::emit(&mut w, &config, world, &help)?;
180
+ term::emit_to_write_style(&mut w, &config, world, &help)?;
180
181
  }
181
182
  }
182
183
 
@@ -192,7 +193,7 @@ fn label(world: &SystemWorld, span: Span) -> Option<Label<FileId>> {
192
193
  impl<'a> codespan_reporting::files::Files<'a> for SystemWorld {
193
194
  type FileId = FileId;
194
195
  type Name = String;
195
- type Source = Source;
196
+ type Source = Lines<String>;
196
197
 
197
198
  fn name(&'a self, id: FileId) -> CodespanResult<Self::Name> {
198
199
  let vpath = id.vpath();
@@ -2,11 +2,13 @@ use comemo::Track;
2
2
  use ecow::{eco_format, EcoString};
3
3
  use serde::Serialize;
4
4
  use typst::diag::{bail, StrResult, Warned};
5
+ use typst::engine::Sink;
5
6
  use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
6
7
  use typst::layout::PagedDocument;
7
8
  use typst::syntax::Span;
9
+ use typst::syntax::SyntaxMode;
8
10
  use typst::World;
9
- use typst_eval::{eval_string, EvalMode};
11
+ use typst_eval::eval_string; //{eval_string, EvalMode};
10
12
 
11
13
  use crate::world::SystemWorld;
12
14
 
@@ -73,9 +75,10 @@ fn retrieve(
73
75
  let selector = eval_string(
74
76
  &typst::ROUTINES,
75
77
  world.track(),
78
+ Sink::new().track_mut(),
76
79
  &command.selector,
77
80
  Span::detached(),
78
- EvalMode::Code,
81
+ SyntaxMode::Code,
79
82
  Scope::default(),
80
83
  )
81
84
  .map_err(|errors| {
@@ -3,13 +3,14 @@ use std::path::{Path, PathBuf};
3
3
  use std::sync::{Mutex, OnceLock};
4
4
 
5
5
  use chrono::{DateTime, Datelike, Local};
6
+ //use rustc_hash::FxHashMap;
6
7
  use ecow::eco_format;
7
8
  use typst::diag::{FileError, FileResult, StrResult};
8
9
  use typst::foundations::{Bytes, Datetime, Dict};
9
- use typst::syntax::{FileId, Source, VirtualPath};
10
+ use typst::syntax::{FileId, Lines, Source, VirtualPath};
10
11
  use typst::text::{Font, FontBook};
11
12
  use typst::utils::LazyHash;
12
- use typst::{Features, Library, LibraryBuilder, World};
13
+ use typst::{Features, Library, LibraryExt, World};
13
14
  use typst_kit::{
14
15
  fonts::{FontSearcher, FontSlot},
15
16
  package::PackageStorage,
@@ -129,9 +130,20 @@ impl SystemWorld {
129
130
 
130
131
  /// Lookup a source file by id.
131
132
  #[track_caller]
132
- pub fn lookup(&self, id: FileId) -> Source {
133
- self.source(id)
134
- .expect("file id does not point to any source file")
133
+ pub fn lookup(&self, id: FileId) -> Lines<String> {
134
+ // self.source(id)
135
+ // .expect("file id does not point to any source file")
136
+ self.slot(id, |slot| {
137
+ if let Some(source) = slot.source.get() {
138
+ let source = source.as_ref().expect("file is not valid");
139
+ source.lines().clone()
140
+ } else if let Some(bytes) = slot.file.get() {
141
+ let bytes = bytes.as_ref().expect("file is not valid");
142
+ Lines::try_from(bytes).expect("file is not valid utf-8")
143
+ } else {
144
+ panic!("file id does not point to any source file");
145
+ }
146
+ })
135
147
  }
136
148
  }
137
149
 
@@ -193,7 +205,7 @@ impl SystemWorldBuilder {
193
205
  input,
194
206
  root: self.root,
195
207
  main: FileId::new(None, main_path),
196
- library: LazyHash::new(LibraryBuilder::default().with_inputs(self.inputs).with_features(self.features).build()),
208
+ library: LazyHash::new(Library::builder().with_inputs(self.inputs).with_features(self.features).build()),
197
209
  book: LazyHash::new(fonts.book),
198
210
  fonts: fonts.fonts,
199
211
  slots: Mutex::default(),
@@ -300,6 +312,11 @@ impl<T: Clone> SlotCell<T> {
300
312
  self.accessed = false;
301
313
  }
302
314
 
315
+ /// Gets the contents of the cell.
316
+ fn get(&self) -> Option<&FileResult<T>> {
317
+ self.data.as_ref()
318
+ }
319
+
303
320
  /// Gets the contents of the cell or initialize them.
304
321
  fn get_or_init(
305
322
  &mut self,
Binary file
Binary file
metadata CHANGED
@@ -1,57 +1,62 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: typst
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.5
4
+ version: 0.14.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Flinn
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-06-30 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rb_sys
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.9'
17
19
  - - ">="
18
20
  - !ruby/object:Gem::Version
19
- version: 0.9.116
21
+ version: 0.9.119
20
22
  type: :runtime
21
23
  prerelease: false
22
24
  version_requirements: !ruby/object:Gem::Requirement
23
25
  requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '0.9'
24
29
  - - ">="
25
30
  - !ruby/object:Gem::Version
26
- version: 0.9.116
31
+ version: 0.9.119
27
32
  - !ruby/object:Gem::Dependency
28
33
  name: rubyzip
29
34
  requirement: !ruby/object:Gem::Requirement
30
35
  requirements:
31
36
  - - "~>"
32
37
  - !ruby/object:Gem::Version
33
- version: '2.4'
38
+ version: '3.2'
34
39
  type: :runtime
35
40
  prerelease: false
36
41
  version_requirements: !ruby/object:Gem::Requirement
37
42
  requirements:
38
43
  - - "~>"
39
44
  - !ruby/object:Gem::Version
40
- version: '2.4'
45
+ version: '3.2'
41
46
  - !ruby/object:Gem::Dependency
42
47
  name: hexapdf
43
48
  requirement: !ruby/object:Gem::Requirement
44
49
  requirements:
45
50
  - - "~>"
46
51
  - !ruby/object:Gem::Version
47
- version: '1.3'
52
+ version: '1.5'
48
53
  type: :development
49
54
  prerelease: false
50
55
  version_requirements: !ruby/object:Gem::Requirement
51
56
  requirements:
52
57
  - - "~>"
53
58
  - !ruby/object:Gem::Version
54
- version: '1.3'
59
+ version: '1.5'
55
60
  - !ruby/object:Gem::Dependency
56
61
  name: test-unit
57
62
  requirement: !ruby/object:Gem::Requirement
@@ -66,7 +71,6 @@ dependencies:
66
71
  - - "~>"
67
72
  - !ruby/object:Gem::Version
68
73
  version: '3.6'
69
- description:
70
74
  email: flinn@actsasflinn.com
71
75
  executables: []
72
76
  extensions:
@@ -79,7 +83,6 @@ files:
79
83
  - Rakefile
80
84
  - ext/typst/Cargo.toml
81
85
  - ext/typst/extconf.rb
82
- - ext/typst/src/compiler-new.rs
83
86
  - ext/typst/src/compiler.rs
84
87
  - ext/typst/src/download.rs
85
88
  - ext/typst/src/fonts.rs
@@ -110,11 +113,12 @@ files:
110
113
  - lib/formats/svg.rb
111
114
  - lib/query.rb
112
115
  - lib/typst.rb
116
+ - lib/typst/typst.bundle
117
+ - lib/typst/typst.so
113
118
  homepage: https://github.com/actsasflinn/typst-rb
114
119
  licenses:
115
120
  - Apache-2.0
116
121
  metadata: {}
117
- post_install_message:
118
122
  rdoc_options: []
119
123
  require_paths:
120
124
  - lib
@@ -129,8 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
129
133
  - !ruby/object:Gem::Version
130
134
  version: '0'
131
135
  requirements: []
132
- rubygems_version: 3.3.26
133
- signing_key:
136
+ rubygems_version: 3.6.7
134
137
  specification_version: 4
135
138
  summary: Ruby binding to typst, a new markup-based typesetting system that is powerful
136
139
  and easy to learn.
@@ -1,771 +0,0 @@
1
- use std::ffi::OsStr;
2
- use std::fs::{self, File};
3
- use std::io::{self, Write};
4
- use std::path::{Path, PathBuf};
5
-
6
- use chrono::{DateTime, Datelike, Timelike, Utc};
7
- use codespan_reporting::diagnostic::{Diagnostic, Label};
8
- use codespan_reporting::term;
9
- use ecow::eco_format;
10
- //use parking_lot::RwLock;
11
- use pathdiff::diff_paths;
12
- //use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
13
- use typst::diag::{
14
- bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
15
- };
16
- use typst::foundations::{Datetime, Smart};
17
- use typst::html::HtmlDocument;
18
- use typst::layout::{Frame, Page, PageRanges, PagedDocument};
19
- use typst::syntax::{FileId, Source, Span};
20
- use typst::WorldExt;
21
- use typst_pdf::{PdfOptions, PdfStandards, Timestamp};
22
-
23
- use typst_cli::args::{
24
- CompileArgs, CompileCommand, DiagnosticFormat, Input, Output, OutputFormat,
25
- PdfStandard, WatchCommand,
26
- };
27
- //#[cfg(feature = "http-server")]
28
- //use crate::server::HtmlServer;
29
- //use crate::timings::Timer;
30
-
31
- //use crate::watch::Status;
32
- use crate::world::SystemWorld;
33
- //use crate::{set_failed, terminal};
34
-
35
- type CodespanResult<T> = Result<T, CodespanError>;
36
- type CodespanError = codespan_reporting::files::Error;
37
-
38
- /*
39
- /// Execute a compilation command.
40
- pub fn compile(timer: &mut Timer, command: &CompileCommand) -> StrResult<()> {
41
- let mut config = CompileConfig::new(command)?;
42
- let mut world =
43
- SystemWorld::new(&command.args.input, &command.args.world, &command.args.process)
44
- .map_err(|err| eco_format!("{err}"))?;
45
- timer.record(&mut world, |world| compile_once(world, &mut config))?
46
- }
47
- */
48
-
49
- /// A preprocessed `CompileCommand`.
50
- pub struct CompileConfig {
51
- /// Whether we are watching.
52
- pub watching: bool,
53
- /// Path to input Typst file or stdin.
54
- pub input: Input,
55
- /// Path to output file (PDF, PNG, SVG, or HTML).
56
- pub output: Output,
57
- /// The format of the output file.
58
- pub output_format: OutputFormat,
59
- /// Which pages to export.
60
- pub pages: Option<PageRanges>,
61
- /// The document's creation date formatted as a UNIX timestamp, with UTC suffix.
62
- pub creation_timestamp: Option<DateTime<Utc>>,
63
- /// The format to emit diagnostics in.
64
- pub diagnostic_format: DiagnosticFormat,
65
- /// Opens the output file with the default viewer or a specific program after
66
- /// compilation.
67
- pub open: Option<Option<String>>,
68
- /// One (or multiple comma-separated) PDF standards that Typst will enforce
69
- /// conformance with.
70
- pub pdf_standards: PdfStandards,
71
- /// A path to write a Makefile rule describing the current compilation.
72
- pub make_deps: Option<PathBuf>,
73
- /// The PPI (pixels per inch) to use for PNG export.
74
- pub ppi: f32,
75
- /// The export cache for images, used for caching output files in `typst
76
- /// watch` sessions with images.
77
- pub export_cache: ExportCache,
78
- // /// Server for `typst watch` to HTML.
79
- // #[cfg(feature = "http-server")]
80
- // pub server: Option<HtmlServer>,
81
- }
82
-
83
- impl CompileConfig {
84
- /// Preprocess a `CompileCommand`, producing a compilation config.
85
- pub fn new(command: &CompileCommand) -> StrResult<Self> {
86
- Self::new_impl(&command.args, None)
87
- }
88
-
89
- /// Preprocess a `WatchCommand`, producing a compilation config.
90
- pub fn watching(command: &WatchCommand) -> StrResult<Self> {
91
- Self::new_impl(&command.args, Some(command))
92
- }
93
-
94
- /// The shared implementation of [`CompileConfig::new`] and
95
- /// [`CompileConfig::watching`].
96
- fn new_impl(args: &CompileArgs, watch: Option<&WatchCommand>) -> StrResult<Self> {
97
- let input = args.input.clone();
98
-
99
- let output_format = if let Some(specified) = args.format {
100
- specified
101
- } else if let Some(Output::Path(output)) = &args.output {
102
- match output.extension() {
103
- Some(ext) if ext.eq_ignore_ascii_case("pdf") => OutputFormat::Pdf,
104
- Some(ext) if ext.eq_ignore_ascii_case("png") => OutputFormat::Png,
105
- Some(ext) if ext.eq_ignore_ascii_case("svg") => OutputFormat::Svg,
106
- Some(ext) if ext.eq_ignore_ascii_case("html") => OutputFormat::Html,
107
- _ => bail!(
108
- "could not infer output format for path {}.\n\
109
- consider providing the format manually with `--format/-f`",
110
- output.display()
111
- ),
112
- }
113
- } else {
114
- OutputFormat::Pdf
115
- };
116
-
117
- let output = args.output.clone().unwrap_or_else(|| {
118
- let Input::Path(path) = &input else {
119
- panic!("output must be specified when input is from stdin, as guarded by the CLI");
120
- };
121
- Output::Path(path.with_extension(
122
- match output_format {
123
- OutputFormat::Pdf => "pdf",
124
- OutputFormat::Png => "png",
125
- OutputFormat::Svg => "svg",
126
- OutputFormat::Html => "html",
127
- },
128
- ))
129
- });
130
-
131
- let pages = args.pages.as_ref().map(|export_ranges| {
132
- PageRanges::new(export_ranges.iter().map(|r| r.0.clone()).collect())
133
- });
134
-
135
- let pdf_standards = {
136
- let list = args
137
- .pdf_standard
138
- .iter()
139
- .map(|standard| match standard {
140
- PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7,
141
- PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b,
142
- PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b,
143
- })
144
- .collect::<Vec<_>>();
145
- PdfStandards::new(&list)?
146
- };
147
-
148
- /*
149
- #[cfg(feature = "http-server")]
150
- let server = match watch {
151
- Some(command)
152
- if output_format == OutputFormat::Html && !command.server.no_serve =>
153
- {
154
- Some(HtmlServer::new(&input, &command.server)?)
155
- }
156
- _ => None,
157
- };
158
- */
159
-
160
- Ok(Self {
161
- watching: watch.is_some(),
162
- input,
163
- output,
164
- output_format,
165
- pages,
166
- pdf_standards,
167
- creation_timestamp: args.world.creation_timestamp,
168
- make_deps: args.make_deps.clone(),
169
- ppi: args.ppi,
170
- diagnostic_format: args.process.diagnostic_format,
171
- open: args.open.clone(),
172
- export_cache: ExportCache::new(),
173
- //#[cfg(feature = "http-server")]
174
- //server,
175
- })
176
- }
177
- }
178
-
179
- /// Compile a single time.
180
- ///
181
- /// Returns whether it compiled without errors.
182
- #[typst_macros::time(name = "compile once")]
183
- pub fn compile_once(
184
- world: &mut SystemWorld,
185
- config: &mut CompileConfig,
186
- ) -> StrResult<()> {
187
- let start = std::time::Instant::now();
188
- if config.watching {
189
- Status::Compiling.print(config).unwrap();
190
- }
191
-
192
- let Warned { output, warnings } = compile_and_export(world, config);
193
-
194
- match output {
195
- // Export the PDF / PNG.
196
- Ok(outputs) => {
197
- let duration = start.elapsed();
198
-
199
- if config.watching {
200
- if warnings.is_empty() {
201
- Status::Success(duration).print(config).unwrap();
202
- } else {
203
- Status::PartialSuccess(duration).print(config).unwrap();
204
- }
205
- }
206
-
207
- print_diagnostics(world, &[], &warnings, config.diagnostic_format)
208
- .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
209
-
210
- write_make_deps(world, config, outputs)?;
211
- open_output(config)?;
212
- }
213
-
214
- // Print diagnostics.
215
- Err(errors) => {
216
- set_failed();
217
-
218
- if config.watching {
219
- Status::Error.print(config).unwrap();
220
- }
221
-
222
- print_diagnostics(world, &errors, &warnings, config.diagnostic_format)
223
- .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
224
- }
225
- }
226
-
227
- Ok(())
228
- }
229
-
230
- /// Compile and then export the document.
231
- fn compile_and_export(
232
- world: &mut SystemWorld,
233
- config: &mut CompileConfig,
234
- ) -> Warned<SourceResult<Vec<Output>>> {
235
- match config.output_format {
236
- OutputFormat::Html => {
237
- let Warned { output, warnings } = typst::compile::<HtmlDocument>(world);
238
- let result = output.and_then(|document| export_html(&document, config));
239
- Warned {
240
- output: result.map(|()| vec![config.output.clone()]),
241
- warnings,
242
- }
243
- }
244
- _ => {
245
- let Warned { output, warnings } = typst::compile::<PagedDocument>(world);
246
- let result = output.and_then(|document| export_paged(&document, config));
247
- Warned { output: result, warnings }
248
- }
249
- }
250
- }
251
-
252
- /// Export to HTML.
253
- fn export_html(document: &HtmlDocument, config: &CompileConfig) -> SourceResult<()> {
254
- let html = typst_html::html(document)?;
255
- let result = config.output.write(html.as_bytes());
256
-
257
- // #[cfg(feature = "http-server")]
258
- // if let Some(server) = &config.server {
259
- // server.update(html);
260
- // }
261
-
262
- result
263
- .map_err(|err| eco_format!("failed to write HTML file ({err})"))
264
- .at(Span::detached())
265
- }
266
-
267
- /// Export to a paged target format.
268
- fn export_paged(
269
- document: &PagedDocument,
270
- config: &CompileConfig,
271
- ) -> SourceResult<Vec<Output>> {
272
- match config.output_format {
273
- OutputFormat::Pdf => {
274
- export_pdf(document, config).map(|()| vec![config.output.clone()])
275
- }
276
- OutputFormat::Png => {
277
- export_image(document, config, ImageExportFormat::Png).at(Span::detached())
278
- }
279
- OutputFormat::Svg => {
280
- export_image(document, config, ImageExportFormat::Svg).at(Span::detached())
281
- }
282
- OutputFormat::Html => unreachable!(),
283
- }
284
- }
285
-
286
- /// Export to a PDF.
287
- fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult<()> {
288
- // If the timestamp is provided through the CLI, use UTC suffix,
289
- // else, use the current local time and timezone.
290
- let timestamp = match config.creation_timestamp {
291
- Some(timestamp) => convert_datetime(timestamp).map(Timestamp::new_utc),
292
- None => {
293
- let local_datetime = chrono::Local::now();
294
- convert_datetime(local_datetime).and_then(|datetime| {
295
- Timestamp::new_local(
296
- datetime,
297
- local_datetime.offset().local_minus_utc() / 60,
298
- )
299
- })
300
- }
301
- };
302
- let options = PdfOptions {
303
- ident: Smart::Auto,
304
- timestamp,
305
- page_ranges: config.pages.clone(),
306
- standards: config.pdf_standards.clone(),
307
- };
308
- let buffer = typst_pdf::pdf(document, &options)?;
309
- config
310
- .output
311
- .write(&buffer)
312
- .map_err(|err| eco_format!("failed to write PDF file ({err})"))
313
- .at(Span::detached())?;
314
- Ok(())
315
- }
316
-
317
- /// Convert [`chrono::DateTime`] to [`Datetime`]
318
- fn convert_datetime<Tz: chrono::TimeZone>(
319
- date_time: chrono::DateTime<Tz>,
320
- ) -> Option<Datetime> {
321
- Datetime::from_ymd_hms(
322
- date_time.year(),
323
- date_time.month().try_into().ok()?,
324
- date_time.day().try_into().ok()?,
325
- date_time.hour().try_into().ok()?,
326
- date_time.minute().try_into().ok()?,
327
- date_time.second().try_into().ok()?,
328
- )
329
- }
330
-
331
- /// An image format to export in.
332
- #[derive(Clone, Copy)]
333
- enum ImageExportFormat {
334
- Png,
335
- Svg,
336
- }
337
-
338
- /// Export to one or multiple images.
339
- fn export_image(
340
- document: &PagedDocument,
341
- config: &CompileConfig,
342
- fmt: ImageExportFormat,
343
- ) -> StrResult<Vec<Output>> {
344
- // Determine whether we have indexable templates in output
345
- let can_handle_multiple = match config.output {
346
- Output::Stdout => false,
347
- Output::Path(ref output) => {
348
- output_template::has_indexable_template(output.to_str().unwrap_or_default())
349
- }
350
- };
351
-
352
- let exported_pages = document
353
- .pages
354
- .iter()
355
- .enumerate()
356
- .filter(|(i, _)| {
357
- config.pages.as_ref().map_or(true, |exported_page_ranges| {
358
- exported_page_ranges.includes_page_index(*i)
359
- })
360
- })
361
- .collect::<Vec<_>>();
362
-
363
- if !can_handle_multiple && exported_pages.len() > 1 {
364
- let err = match config.output {
365
- Output::Stdout => "to stdout",
366
- Output::Path(_) => {
367
- "without a page number template ({p}, {0p}) in the output path"
368
- }
369
- };
370
- bail!("cannot export multiple images {err}");
371
- }
372
-
373
- // The results are collected in a `Vec<()>` which does not allocate.
374
- exported_pages
375
- .par_iter()
376
- .map(|(i, page)| {
377
- // Use output with converted path.
378
- let output = match &config.output {
379
- Output::Path(path) => {
380
- let storage;
381
- let path = if can_handle_multiple {
382
- storage = output_template::format(
383
- path.to_str().unwrap_or_default(),
384
- i + 1,
385
- document.pages.len(),
386
- );
387
- Path::new(&storage)
388
- } else {
389
- path
390
- };
391
-
392
- // If we are not watching, don't use the cache.
393
- // If the frame is in the cache, skip it.
394
- // If the file does not exist, always create it.
395
- if config.watching
396
- && config.export_cache.is_cached(*i, &page.frame)
397
- && path.exists()
398
- {
399
- return Ok(Output::Path(path.to_path_buf()));
400
- }
401
-
402
- Output::Path(path.to_owned())
403
- }
404
- Output::Stdout => Output::Stdout,
405
- };
406
-
407
- export_image_page(config, page, &output, fmt)?;
408
- Ok(output)
409
- })
410
- .collect::<StrResult<Vec<Output>>>()
411
- }
412
-
413
- mod output_template {
414
- const INDEXABLE: [&str; 3] = ["{p}", "{0p}", "{n}"];
415
-
416
- pub fn has_indexable_template(output: &str) -> bool {
417
- INDEXABLE.iter().any(|template| output.contains(template))
418
- }
419
-
420
- pub fn format(output: &str, this_page: usize, total_pages: usize) -> String {
421
- // Find the base 10 width of number `i`
422
- fn width(i: usize) -> usize {
423
- 1 + i.checked_ilog10().unwrap_or(0) as usize
424
- }
425
-
426
- let other_templates = ["{t}"];
427
- INDEXABLE.iter().chain(other_templates.iter()).fold(
428
- output.to_string(),
429
- |out, template| {
430
- let replacement = match *template {
431
- "{p}" => format!("{this_page}"),
432
- "{0p}" | "{n}" => format!("{:01$}", this_page, width(total_pages)),
433
- "{t}" => format!("{total_pages}"),
434
- _ => unreachable!("unhandled template placeholder {template}"),
435
- };
436
- out.replace(template, replacement.as_str())
437
- },
438
- )
439
- }
440
- }
441
-
442
- /// Export single image.
443
- fn export_image_page(
444
- config: &CompileConfig,
445
- page: &Page,
446
- output: &Output,
447
- fmt: ImageExportFormat,
448
- ) -> StrResult<()> {
449
- match fmt {
450
- ImageExportFormat::Png => {
451
- let pixmap = typst_render::render(page, config.ppi / 72.0);
452
- let buf = pixmap
453
- .encode_png()
454
- .map_err(|err| eco_format!("failed to encode PNG file ({err})"))?;
455
- output
456
- .write(&buf)
457
- .map_err(|err| eco_format!("failed to write PNG file ({err})"))?;
458
- }
459
- ImageExportFormat::Svg => {
460
- let svg = typst_svg::svg(page);
461
- output
462
- .write(svg.as_bytes())
463
- .map_err(|err| eco_format!("failed to write SVG file ({err})"))?;
464
- }
465
- }
466
- Ok(())
467
- }
468
-
469
- impl Output {
470
- fn write(&self, buffer: &[u8]) -> StrResult<()> {
471
- match self {
472
- Output::Stdout => std::io::stdout().write_all(buffer),
473
- Output::Path(path) => fs::write(path, buffer),
474
- }
475
- .map_err(|err| eco_format!("{err}"))
476
- }
477
- }
478
-
479
- /// Caches exported files so that we can avoid re-exporting them if they haven't
480
- /// changed.
481
- ///
482
- /// This is done by having a list of size `files.len()` that contains the hashes
483
- /// of the last rendered frame in each file. If a new frame is inserted, this
484
- /// will invalidate the rest of the cache, this is deliberate as to decrease the
485
- /// complexity and memory usage of such a cache.
486
- pub struct ExportCache {
487
- /// The hashes of last compilation's frames.
488
- pub cache: RwLock<Vec<u128>>,
489
- }
490
-
491
- impl ExportCache {
492
- /// Creates a new export cache.
493
- pub fn new() -> Self {
494
- Self { cache: RwLock::new(Vec::with_capacity(32)) }
495
- }
496
-
497
- /// Returns true if the entry is cached and appends the new hash to the
498
- /// cache (for the next compilation).
499
- pub fn is_cached(&self, i: usize, frame: &Frame) -> bool {
500
- let hash = typst::utils::hash128(frame);
501
-
502
- let mut cache = self.cache.upgradable_read();
503
- if i >= cache.len() {
504
- cache.with_upgraded(|cache| cache.push(hash));
505
- return false;
506
- }
507
-
508
- cache.with_upgraded(|cache| std::mem::replace(&mut cache[i], hash) == hash)
509
- }
510
- }
511
-
512
- /// Writes a Makefile rule describing the relationship between the output and
513
- /// its dependencies to the path specified by the --make-deps argument, if it
514
- /// was provided.
515
- fn write_make_deps(
516
- world: &mut SystemWorld,
517
- config: &CompileConfig,
518
- outputs: Vec<Output>,
519
- ) -> StrResult<()> {
520
- let Some(ref make_deps_path) = config.make_deps else { return Ok(()) };
521
- let Ok(output_paths) = outputs
522
- .into_iter()
523
- .filter_map(|o| match o {
524
- Output::Path(path) => Some(path.into_os_string().into_string()),
525
- Output::Stdout => None,
526
- })
527
- .collect::<Result<Vec<_>, _>>()
528
- else {
529
- bail!("failed to create make dependencies file because output path was not valid unicode")
530
- };
531
- if output_paths.is_empty() {
532
- bail!("failed to create make dependencies file because output was stdout")
533
- }
534
-
535
- // Based on `munge` in libcpp/mkdeps.cc from the GCC source code. This isn't
536
- // perfect as some special characters can't be escaped.
537
- fn munge(s: &str) -> String {
538
- let mut res = String::with_capacity(s.len());
539
- let mut slashes = 0;
540
- for c in s.chars() {
541
- match c {
542
- '\\' => slashes += 1,
543
- '$' => {
544
- res.push('$');
545
- slashes = 0;
546
- }
547
- ':' => {
548
- res.push('\\');
549
- slashes = 0;
550
- }
551
- ' ' | '\t' => {
552
- // `munge`'s source contains a comment here that says: "A
553
- // space or tab preceded by 2N+1 backslashes represents N
554
- // backslashes followed by space..."
555
- for _ in 0..slashes + 1 {
556
- res.push('\\');
557
- }
558
- slashes = 0;
559
- }
560
- '#' => {
561
- res.push('\\');
562
- slashes = 0;
563
- }
564
- _ => slashes = 0,
565
- };
566
- res.push(c);
567
- }
568
- res
569
- }
570
-
571
- fn write(
572
- make_deps_path: &Path,
573
- output_paths: Vec<String>,
574
- root: PathBuf,
575
- dependencies: impl Iterator<Item = PathBuf>,
576
- ) -> io::Result<()> {
577
- let mut file = File::create(make_deps_path)?;
578
- let current_dir = std::env::current_dir()?;
579
- let relative_root = diff_paths(&root, &current_dir).unwrap_or(root.clone());
580
-
581
- for (i, output_path) in output_paths.into_iter().enumerate() {
582
- if i != 0 {
583
- file.write_all(b" ")?;
584
- }
585
- file.write_all(munge(&output_path).as_bytes())?;
586
- }
587
- file.write_all(b":")?;
588
- for dependency in dependencies {
589
- let relative_dependency = match dependency.strip_prefix(&root) {
590
- Ok(root_relative_dependency) => {
591
- relative_root.join(root_relative_dependency)
592
- }
593
- Err(_) => dependency,
594
- };
595
- let Some(relative_dependency) = relative_dependency.to_str() else {
596
- // Silently skip paths that aren't valid unicode so we still
597
- // produce a rule that will work for the other paths that can be
598
- // processed.
599
- continue;
600
- };
601
-
602
- file.write_all(b" ")?;
603
- file.write_all(munge(relative_dependency).as_bytes())?;
604
- }
605
- file.write_all(b"\n")?;
606
-
607
- Ok(())
608
- }
609
-
610
- write(make_deps_path, output_paths, world.root().to_owned(), world.dependencies())
611
- .map_err(|err| {
612
- eco_format!("failed to create make dependencies file due to IO error ({err})")
613
- })
614
- }
615
-
616
- /// Opens the output if desired.
617
- fn open_output(config: &mut CompileConfig) -> StrResult<()> {
618
- let Some(viewer) = config.open.take() else { return Ok(()) };
619
-
620
- // #[cfg(feature = "http-server")]
621
- // if let Some(server) = &config.server {
622
- // let url = format!("http://{}", server.addr());
623
- // return open_path(OsStr::new(&url), viewer.as_deref());
624
- // }
625
-
626
- // Can't open stdout.
627
- let Output::Path(path) = &config.output else { return Ok(()) };
628
-
629
- // Some resource openers require the path to be canonicalized.
630
- let path = path
631
- .canonicalize()
632
- .map_err(|err| eco_format!("failed to canonicalize path ({err})"))?;
633
-
634
- open_path(path.as_os_str(), viewer.as_deref())
635
- }
636
-
637
- /// Opens the given file using:
638
- ///
639
- /// - The default file viewer if `app` is `None`.
640
- /// - The given viewer provided by `app` if it is `Some`.
641
- fn open_path(path: &OsStr, viewer: Option<&str>) -> StrResult<()> {
642
- if let Some(viewer) = viewer {
643
- open::with_detached(path, viewer)
644
- .map_err(|err| eco_format!("failed to open file with {} ({})", viewer, err))
645
- } else {
646
- open::that_detached(path).map_err(|err| {
647
- let openers = open::commands(path)
648
- .iter()
649
- .map(|command| command.get_program().to_string_lossy())
650
- .collect::<Vec<_>>()
651
- .join(", ");
652
- eco_format!(
653
- "failed to open file with any of these resource openers: {} ({})",
654
- openers,
655
- err,
656
- )
657
- })
658
- }
659
- }
660
-
661
- /// Print diagnostic messages to the terminal.
662
- pub fn print_diagnostics(
663
- world: &SystemWorld,
664
- errors: &[SourceDiagnostic],
665
- warnings: &[SourceDiagnostic],
666
- diagnostic_format: DiagnosticFormat,
667
- ) -> Result<(), codespan_reporting::files::Error> {
668
- let mut config = term::Config { tab_width: 2, ..Default::default() };
669
- if diagnostic_format == DiagnosticFormat::Short {
670
- config.display_style = term::DisplayStyle::Short;
671
- }
672
-
673
- for diagnostic in warnings.iter().chain(errors) {
674
- let diag = match diagnostic.severity {
675
- Severity::Error => Diagnostic::error(),
676
- Severity::Warning => Diagnostic::warning(),
677
- }
678
- .with_message(diagnostic.message.clone())
679
- .with_notes(
680
- diagnostic
681
- .hints
682
- .iter()
683
- .map(|e| (eco_format!("hint: {e}")).into())
684
- .collect(),
685
- )
686
- .with_labels(label(world, diagnostic.span).into_iter().collect());
687
-
688
- term::emit(&mut terminal::out(), &config, world, &diag)?;
689
-
690
- // Stacktrace-like helper diagnostics.
691
- for point in &diagnostic.trace {
692
- let message = point.v.to_string();
693
- let help = Diagnostic::help()
694
- .with_message(message)
695
- .with_labels(label(world, point.span).into_iter().collect());
696
-
697
- term::emit(&mut terminal::out(), &config, world, &help)?;
698
- }
699
- }
700
-
701
- Ok(())
702
- }
703
-
704
- /// Create a label for a span.
705
- fn label(world: &SystemWorld, span: Span) -> Option<Label<FileId>> {
706
- Some(Label::primary(span.id()?, world.range(span)?))
707
- }
708
-
709
- impl<'a> codespan_reporting::files::Files<'a> for SystemWorld {
710
- type FileId = FileId;
711
- type Name = String;
712
- type Source = Source;
713
-
714
- fn name(&'a self, id: FileId) -> CodespanResult<Self::Name> {
715
- let vpath = id.vpath();
716
- Ok(if let Some(package) = id.package() {
717
- format!("{package}{}", vpath.as_rooted_path().display())
718
- } else {
719
- // Try to express the path relative to the working directory.
720
- vpath
721
- .resolve(self.root())
722
- .and_then(|abs| pathdiff::diff_paths(abs, self.workdir()))
723
- .as_deref()
724
- .unwrap_or_else(|| vpath.as_rootless_path())
725
- .to_string_lossy()
726
- .into()
727
- })
728
- }
729
-
730
- fn source(&'a self, id: FileId) -> CodespanResult<Self::Source> {
731
- Ok(self.lookup(id))
732
- }
733
-
734
- fn line_index(&'a self, id: FileId, given: usize) -> CodespanResult<usize> {
735
- let source = self.lookup(id);
736
- source
737
- .byte_to_line(given)
738
- .ok_or_else(|| CodespanError::IndexTooLarge {
739
- given,
740
- max: source.len_bytes(),
741
- })
742
- }
743
-
744
- fn line_range(
745
- &'a self,
746
- id: FileId,
747
- given: usize,
748
- ) -> CodespanResult<std::ops::Range<usize>> {
749
- let source = self.lookup(id);
750
- source
751
- .line_to_range(given)
752
- .ok_or_else(|| CodespanError::LineTooLarge { given, max: source.len_lines() })
753
- }
754
-
755
- fn column_number(
756
- &'a self,
757
- id: FileId,
758
- _: usize,
759
- given: usize,
760
- ) -> CodespanResult<usize> {
761
- let source = self.lookup(id);
762
- source.byte_to_column(given).ok_or_else(|| {
763
- let max = source.len_bytes();
764
- if given <= max {
765
- CodespanError::InvalidCharBoundary { given }
766
- } else {
767
- CodespanError::IndexTooLarge { given, max }
768
- }
769
- })
770
- }
771
- }