mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Live reloading HTTP server for typst watch
and HTML export (#5524)
This commit is contained in:
parent
4f3ba7f8ca
commit
79a7a6bf77
36
Cargo.lock
generated
36
Cargo.lock
generated
@ -123,6 +123,12 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "ascii"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
@ -282,6 +288,12 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chunked_transfer"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
@ -909,6 +921,12 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hypher"
|
||||
version = "0.1.5"
|
||||
@ -1296,13 +1314,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.7"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
|
||||
checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2581,6 +2598,18 @@ dependencies = [
|
||||
"strict-num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny_http"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"
|
||||
dependencies = [
|
||||
"ascii",
|
||||
"chunked_transfer",
|
||||
"httpdate",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.7.6"
|
||||
@ -2718,6 +2747,7 @@ dependencies = [
|
||||
"sigpipe",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"tiny_http",
|
||||
"toml",
|
||||
"typst",
|
||||
"typst-eval",
|
||||
|
@ -117,6 +117,7 @@ tar = "0.4"
|
||||
tempfile = "3.7.0"
|
||||
thin-vec = "0.2.13"
|
||||
time = { version = "0.3.20", features = ["formatting", "macros", "parsing"] }
|
||||
tiny_http = "0.12"
|
||||
tiny-skia = "0.11"
|
||||
toml = { version = "0.8", default-features = false, features = ["parse", "display"] }
|
||||
ttf-parser = "0.24.1"
|
||||
|
@ -50,6 +50,7 @@ shell-escape = { workspace = true }
|
||||
sigpipe = { workspace = true }
|
||||
tar = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tiny_http = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
ureq = { workspace = true }
|
||||
xz2 = { workspace = true, optional = true }
|
||||
|
@ -97,6 +97,21 @@ pub struct WatchCommand {
|
||||
/// Arguments for compilation.
|
||||
#[clap(flatten)]
|
||||
pub args: CompileArgs,
|
||||
|
||||
/// Disables the built-in HTTP server for HTML export.
|
||||
#[clap(long)]
|
||||
pub no_serve: bool,
|
||||
|
||||
/// Disables the injected live reload script for HTML export. The HTML that
|
||||
/// is written to disk isn't affected either way.
|
||||
#[clap(long)]
|
||||
pub no_reload: bool,
|
||||
|
||||
/// The port where HTML is served.
|
||||
///
|
||||
/// Defaults to the first free port in the range 3000-3005.
|
||||
#[clap(long)]
|
||||
pub port: Option<u16>,
|
||||
}
|
||||
|
||||
/// Initializes a new project from a template.
|
||||
|
@ -1,3 +1,4 @@
|
||||
use std::ffi::OsString;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
@ -20,8 +21,9 @@ use typst_pdf::{PdfOptions, PdfStandards};
|
||||
|
||||
use crate::args::{
|
||||
CompileArgs, CompileCommand, DiagnosticFormat, Input, Output, OutputFormat,
|
||||
PdfStandard,
|
||||
PdfStandard, WatchCommand,
|
||||
};
|
||||
use crate::server::HtmlServer;
|
||||
use crate::timings::Timer;
|
||||
|
||||
use crate::watch::Status;
|
||||
@ -33,15 +35,17 @@ type CodespanError = codespan_reporting::files::Error;
|
||||
|
||||
/// Execute a compilation command.
|
||||
pub fn compile(timer: &mut Timer, command: &CompileCommand) -> StrResult<()> {
|
||||
let mut config = CompileConfig::new(&command.args)?;
|
||||
let mut config = CompileConfig::new(command)?;
|
||||
let mut world =
|
||||
SystemWorld::new(&command.args.input, &command.args.world, &command.args.process)
|
||||
.map_err(|err| eco_format!("{err}"))?;
|
||||
timer.record(&mut world, |world| compile_once(world, &mut config, false))?
|
||||
timer.record(&mut world, |world| compile_once(world, &mut config))?
|
||||
}
|
||||
|
||||
/// A preprocessed `CompileCommand`.
|
||||
pub struct CompileConfig {
|
||||
/// Whether we are watching.
|
||||
pub watching: bool,
|
||||
/// Path to input Typst file or stdin.
|
||||
pub input: Input,
|
||||
/// Path to output file (PDF, PNG, SVG, or HTML).
|
||||
@ -64,11 +68,27 @@ pub struct CompileConfig {
|
||||
pub make_deps: Option<PathBuf>,
|
||||
/// The PPI (pixels per inch) to use for PNG export.
|
||||
pub ppi: f32,
|
||||
/// The export cache for images, used for caching output files in `typst
|
||||
/// watch` sessions with images.
|
||||
pub export_cache: ExportCache,
|
||||
/// Server for `typst watch` to HTML.
|
||||
pub server: Option<HtmlServer>,
|
||||
}
|
||||
|
||||
impl CompileConfig {
|
||||
/// Preprocess a `CompileCommand`, producing a compilation config.
|
||||
pub fn new(args: &CompileArgs) -> StrResult<Self> {
|
||||
pub fn new(command: &CompileCommand) -> StrResult<Self> {
|
||||
Self::new_impl(&command.args, None)
|
||||
}
|
||||
|
||||
/// Preprocess a `WatchCommand`, producing a compilation config.
|
||||
pub fn watching(command: &WatchCommand) -> StrResult<Self> {
|
||||
Self::new_impl(&command.args, Some(command))
|
||||
}
|
||||
|
||||
/// The shared implementation of [`CompileConfig::new`] and
|
||||
/// [`CompileConfig::watching`].
|
||||
fn new_impl(args: &CompileArgs, watch: Option<&WatchCommand>) -> StrResult<Self> {
|
||||
let input = args.input.clone();
|
||||
|
||||
let output_format = if let Some(specified) = args.format {
|
||||
@ -119,7 +139,17 @@ impl CompileConfig {
|
||||
PdfStandards::new(&list)?
|
||||
};
|
||||
|
||||
let mut server = None;
|
||||
let mut watching = false;
|
||||
if let Some(command) = watch {
|
||||
watching = true;
|
||||
if output_format == OutputFormat::Html && !command.no_serve {
|
||||
server = Some(HtmlServer::new(&input, command.port, !command.no_reload)?);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
watching,
|
||||
input,
|
||||
output,
|
||||
output_format,
|
||||
@ -130,6 +160,8 @@ impl CompileConfig {
|
||||
ppi: args.ppi,
|
||||
diagnostic_format: args.process.diagnostic_format,
|
||||
open: args.open.clone(),
|
||||
export_cache: ExportCache::new(),
|
||||
server,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -141,21 +173,20 @@ impl CompileConfig {
|
||||
pub fn compile_once(
|
||||
world: &mut SystemWorld,
|
||||
config: &mut CompileConfig,
|
||||
watching: bool,
|
||||
) -> StrResult<()> {
|
||||
let start = std::time::Instant::now();
|
||||
if watching {
|
||||
if config.watching {
|
||||
Status::Compiling.print(config).unwrap();
|
||||
}
|
||||
|
||||
let Warned { output, warnings } = compile_and_export(world, config, watching);
|
||||
let Warned { output, warnings } = compile_and_export(world, config);
|
||||
|
||||
match output {
|
||||
// Export the PDF / PNG.
|
||||
Ok(()) => {
|
||||
let duration = start.elapsed();
|
||||
|
||||
if watching {
|
||||
if config.watching {
|
||||
if warnings.is_empty() {
|
||||
Status::Success(duration).print(config).unwrap();
|
||||
} else {
|
||||
@ -167,19 +198,14 @@ pub fn compile_once(
|
||||
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
|
||||
|
||||
write_make_deps(world, config)?;
|
||||
|
||||
if let Some(open) = config.open.take() {
|
||||
if let Output::Path(file) = &config.output {
|
||||
open_file(open.as_deref(), file)?;
|
||||
}
|
||||
}
|
||||
open_output(config)?;
|
||||
}
|
||||
|
||||
// Print diagnostics.
|
||||
Err(errors) => {
|
||||
set_failed();
|
||||
|
||||
if watching {
|
||||
if config.watching {
|
||||
Status::Error.print(config).unwrap();
|
||||
}
|
||||
|
||||
@ -191,48 +217,48 @@ pub fn compile_once(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compile and then export the document.
|
||||
fn compile_and_export(
|
||||
world: &mut SystemWorld,
|
||||
config: &mut CompileConfig,
|
||||
watching: bool,
|
||||
) -> Warned<SourceResult<()>> {
|
||||
match config.output_format {
|
||||
OutputFormat::Html => {
|
||||
let Warned { output, warnings } = typst::compile::<HtmlDocument>(world);
|
||||
let result = output.and_then(|document| {
|
||||
config
|
||||
.output
|
||||
.write(typst_html::html(&document)?.as_bytes())
|
||||
.map_err(|err| eco_format!("failed to write HTML file ({err})"))
|
||||
.at(Span::detached())
|
||||
});
|
||||
let result = output.and_then(|document| export_html(&document, config));
|
||||
Warned { output: result, warnings }
|
||||
}
|
||||
_ => {
|
||||
let Warned { output, warnings } = typst::compile::<PagedDocument>(world);
|
||||
let result = output
|
||||
.and_then(|document| export_paged(world, &document, config, watching));
|
||||
let result = output.and_then(|document| export_paged(&document, config));
|
||||
Warned { output: result, warnings }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Export into the target format.
|
||||
fn export_paged(
|
||||
world: &mut SystemWorld,
|
||||
document: &PagedDocument,
|
||||
config: &CompileConfig,
|
||||
watching: bool,
|
||||
) -> SourceResult<()> {
|
||||
/// Export to HTML.
|
||||
fn export_html(document: &HtmlDocument, config: &CompileConfig) -> SourceResult<()> {
|
||||
let html = typst_html::html(document)?;
|
||||
let result = config.output.write(html.as_bytes());
|
||||
|
||||
if let Some(server) = &config.server {
|
||||
server.update(html);
|
||||
}
|
||||
|
||||
result
|
||||
.map_err(|err| eco_format!("failed to write HTML file ({err})"))
|
||||
.at(Span::detached())
|
||||
}
|
||||
|
||||
/// Export to a paged target format.
|
||||
fn export_paged(document: &PagedDocument, config: &CompileConfig) -> SourceResult<()> {
|
||||
match config.output_format {
|
||||
OutputFormat::Pdf => export_pdf(document, config),
|
||||
OutputFormat::Png => {
|
||||
export_image(world, document, config, watching, ImageExportFormat::Png)
|
||||
.at(Span::detached())
|
||||
export_image(document, config, ImageExportFormat::Png).at(Span::detached())
|
||||
}
|
||||
OutputFormat::Svg => {
|
||||
export_image(world, document, config, watching, ImageExportFormat::Svg)
|
||||
.at(Span::detached())
|
||||
export_image(document, config, ImageExportFormat::Svg).at(Span::detached())
|
||||
}
|
||||
OutputFormat::Html => unreachable!(),
|
||||
}
|
||||
@ -278,10 +304,8 @@ enum ImageExportFormat {
|
||||
|
||||
/// Export to one or multiple images.
|
||||
fn export_image(
|
||||
world: &mut SystemWorld,
|
||||
document: &PagedDocument,
|
||||
config: &CompileConfig,
|
||||
watching: bool,
|
||||
fmt: ImageExportFormat,
|
||||
) -> StrResult<()> {
|
||||
// Determine whether we have indexable templates in output
|
||||
@ -313,8 +337,6 @@ fn export_image(
|
||||
bail!("cannot export multiple images {err}");
|
||||
}
|
||||
|
||||
let cache = world.export_cache();
|
||||
|
||||
// The results are collected in a `Vec<()>` which does not allocate.
|
||||
exported_pages
|
||||
.par_iter()
|
||||
@ -337,7 +359,10 @@ fn export_image(
|
||||
// If we are not watching, don't use the cache.
|
||||
// If the frame is in the cache, skip it.
|
||||
// If the file does not exist, always create it.
|
||||
if watching && cache.is_cached(*i, &page.frame) && path.exists() {
|
||||
if config.watching
|
||||
&& config.export_cache.is_cached(*i, &page.frame)
|
||||
&& path.exists()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@ -531,17 +556,26 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult
|
||||
})
|
||||
}
|
||||
|
||||
/// Opens the given file using:
|
||||
/// Opens the output if desired, with:
|
||||
/// - The default file viewer if `open` is `None`.
|
||||
/// - The given viewer provided by `open` if it is `Some`.
|
||||
///
|
||||
/// If the file could not be opened, an error is returned.
|
||||
fn open_file(open: Option<&str>, path: &Path) -> StrResult<()> {
|
||||
// Some resource openers require the path to be canonicalized.
|
||||
let path = path
|
||||
.canonicalize()
|
||||
.map_err(|err| eco_format!("failed to canonicalize path ({err})"))?;
|
||||
if let Some(app) = open {
|
||||
fn open_output(config: &mut CompileConfig) -> StrResult<()> {
|
||||
let Some(open) = config.open.take() else { return Ok(()) };
|
||||
|
||||
let path = if let Some(server) = &config.server {
|
||||
OsString::from(format!("http://{}", server.addr()))
|
||||
} else if let Output::Path(path) = &config.output {
|
||||
// Some resource openers require the path to be canonicalized.
|
||||
path.canonicalize()
|
||||
.map_err(|err| eco_format!("failed to canonicalize path ({err})"))?
|
||||
.into_os_string()
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Some(app) = &open {
|
||||
open::with_detached(&path, app)
|
||||
.map_err(|err| eco_format!("failed to open file with {} ({})", app, err))
|
||||
} else {
|
||||
|
@ -6,6 +6,7 @@ mod greet;
|
||||
mod init;
|
||||
mod package;
|
||||
mod query;
|
||||
mod server;
|
||||
mod terminal;
|
||||
mod timings;
|
||||
#[cfg(feature = "self-update")]
|
||||
|
217
crates/typst-cli/src/server.rs
Normal file
217
crates/typst-cli/src/server.rs
Normal file
@ -0,0 +1,217 @@
|
||||
use std::io::{self, Write};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener};
|
||||
use std::sync::Arc;
|
||||
|
||||
use ecow::eco_format;
|
||||
use parking_lot::{Condvar, Mutex, MutexGuard};
|
||||
use tiny_http::{Header, Request, Response, StatusCode};
|
||||
use typst::diag::{bail, StrResult};
|
||||
|
||||
use crate::args::Input;
|
||||
|
||||
/// Serves HTML with live reload.
|
||||
pub struct HtmlServer {
|
||||
addr: SocketAddr,
|
||||
bucket: Arc<Bucket<String>>,
|
||||
}
|
||||
|
||||
impl HtmlServer {
|
||||
/// Create a new HTTP server that serves live HTML.
|
||||
pub fn new(input: &Input, port: Option<u16>, reload: bool) -> StrResult<Self> {
|
||||
let (addr, server) = start_server(port)?;
|
||||
|
||||
let placeholder = PLACEHOLDER_HTML.replace("{INPUT}", &input.to_string());
|
||||
let bucket = Arc::new(Bucket::new(placeholder));
|
||||
let bucket2 = bucket.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
for req in server.incoming_requests() {
|
||||
let _ = handle(req, reload, &bucket2);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Self { addr, bucket })
|
||||
}
|
||||
|
||||
/// The address that we serve the HTML on.
|
||||
pub fn addr(&self) -> SocketAddr {
|
||||
self.addr
|
||||
}
|
||||
|
||||
/// Updates the HTML, triggering a reload all connected browsers.
|
||||
pub fn update(&self, html: String) {
|
||||
self.bucket.put(html);
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts a local HTTP server.
|
||||
///
|
||||
/// Uses the specified port or tries to find a free port in the range
|
||||
/// `3000..=3005`.
|
||||
fn start_server(port: Option<u16>) -> StrResult<(SocketAddr, tiny_http::Server)> {
|
||||
const BASE_PORT: u16 = 3000;
|
||||
|
||||
let mut addr;
|
||||
let mut retries = 0;
|
||||
|
||||
let listener = loop {
|
||||
addr = SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::LOCALHOST),
|
||||
port.unwrap_or(BASE_PORT + retries),
|
||||
);
|
||||
|
||||
match TcpListener::bind(addr) {
|
||||
Ok(listener) => break listener,
|
||||
Err(err) if err.kind() == io::ErrorKind::AddrInUse => {
|
||||
if let Some(port) = port {
|
||||
bail!("port {port} is already in use")
|
||||
} else if retries < 5 {
|
||||
// If the port is in use, try the next one.
|
||||
retries += 1;
|
||||
} else {
|
||||
bail!("could not find free port for HTTP server");
|
||||
}
|
||||
}
|
||||
Err(err) => bail!("failed to start TCP server: {err}"),
|
||||
}
|
||||
};
|
||||
|
||||
let server = tiny_http::Server::from_listener(listener, None)
|
||||
.map_err(|err| eco_format!("failed to start HTTP server: {err}"))?;
|
||||
|
||||
Ok((addr, server))
|
||||
}
|
||||
|
||||
/// Handles a request.
|
||||
fn handle(req: Request, reload: bool, bucket: &Arc<Bucket<String>>) -> io::Result<()> {
|
||||
let path = req.url();
|
||||
match path {
|
||||
"/" => handle_root(req, reload, bucket),
|
||||
"/events" => handle_events(req, bucket.clone()),
|
||||
_ => req.respond(Response::new_empty(StatusCode(404))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles for the `/` route. Serves the compiled HTML.
|
||||
fn handle_root(req: Request, reload: bool, bucket: &Bucket<String>) -> io::Result<()> {
|
||||
let mut html = bucket.get().clone();
|
||||
if reload {
|
||||
inject_live_reload_script(&mut html);
|
||||
}
|
||||
req.respond(Response::new(
|
||||
StatusCode(200),
|
||||
vec![Header::from_bytes("Content-Type", "text/html").unwrap()],
|
||||
html.as_bytes(),
|
||||
Some(html.len()),
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
/// Handler for the `/events` route.
|
||||
fn handle_events(req: Request, bucket: Arc<Bucket<String>>) -> io::Result<()> {
|
||||
std::thread::spawn(move || {
|
||||
// When this returns an error, the client is disconnected and we can
|
||||
// terminate the thread.
|
||||
let _ = handle_events_blocking(req, &bucket);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Event stream for the `/events` route.
|
||||
fn handle_events_blocking(req: Request, bucket: &Bucket<String>) -> io::Result<()> {
|
||||
let mut writer = req.into_writer();
|
||||
let writer: &mut dyn Write = &mut *writer;
|
||||
|
||||
// We need to write the header manually because `tiny-http` defaults to
|
||||
// `Transfer-Encoding: chunked` when no `Content-Length` is provided, which
|
||||
// Chrome & Safari dislike for `Content-Type: text/event-stream`.
|
||||
write!(writer, "HTTP/1.1 200 OK\r\n")?;
|
||||
write!(writer, "Content-Type: text/event-stream\r\n")?;
|
||||
write!(writer, "Cache-Control: no-cache\r\n")?;
|
||||
write!(writer, "\r\n")?;
|
||||
writer.flush()?;
|
||||
|
||||
// If the user closes the browser tab, this loop will terminate once it
|
||||
// tries to write to the dead socket for the first time.
|
||||
loop {
|
||||
bucket.wait();
|
||||
// Trigger a server-sent event. The browser is listening to it via
|
||||
// an `EventSource` listener` (see `inject_script`).
|
||||
write!(writer, "event: reload\ndata:\n\n")?;
|
||||
writer.flush()?;
|
||||
}
|
||||
}
|
||||
|
||||
/// Injects the live reload script into a string of HTML.
|
||||
fn inject_live_reload_script(html: &mut String) {
|
||||
let pos = html.rfind("</html>").unwrap_or(html.len());
|
||||
html.insert_str(pos, LIVE_RELOAD_SCRIPT);
|
||||
}
|
||||
|
||||
/// Holds data and notifies consumers when it's updated.
|
||||
struct Bucket<T> {
|
||||
mutex: Mutex<T>,
|
||||
condvar: Condvar,
|
||||
}
|
||||
|
||||
impl<T> Bucket<T> {
|
||||
/// Creates a new bucket with initial data.
|
||||
fn new(init: T) -> Self {
|
||||
Self { mutex: Mutex::new(init), condvar: Condvar::new() }
|
||||
}
|
||||
|
||||
/// Retrieves the current data in the bucket.
|
||||
fn get(&self) -> MutexGuard<T> {
|
||||
self.mutex.lock()
|
||||
}
|
||||
|
||||
/// Puts new data into the bucket and notifies everyone who's currently
|
||||
/// [waiting](Self::wait).
|
||||
fn put(&self, data: T) {
|
||||
*self.mutex.lock() = data;
|
||||
self.condvar.notify_all();
|
||||
}
|
||||
|
||||
/// Waits for new data in the bucket.
|
||||
fn wait(&self) {
|
||||
self.condvar.wait(&mut self.mutex.lock());
|
||||
}
|
||||
}
|
||||
|
||||
/// The initial HTML before compilation is finished.
|
||||
const PLACEHOLDER_HTML: &str = "\
|
||||
<html>
|
||||
<head>
|
||||
<title>Waiting for {INPUT}</title>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #565565;
|
||||
background: #eff0f3;
|
||||
}
|
||||
|
||||
body > main > div {
|
||||
margin-block: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div>Waiting for output ...</div>
|
||||
<div><code>typst watch {INPUT}</code></div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
";
|
||||
|
||||
/// Reloads the page whenever it receives a "reload" server-sent event
|
||||
/// on the `/events` route.
|
||||
const LIVE_RELOAD_SCRIPT: &str = "\
|
||||
<script>\
|
||||
new EventSource(\"/events\")\
|
||||
.addEventListener(\"reload\", () => location.reload())\
|
||||
</script>\
|
||||
";
|
@ -21,7 +21,7 @@ use crate::{print_error, terminal};
|
||||
|
||||
/// Execute a watching compilation command.
|
||||
pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
|
||||
let mut config = CompileConfig::new(&command.args)?;
|
||||
let mut config = CompileConfig::watching(command)?;
|
||||
|
||||
let Output::Path(output) = &config.output else {
|
||||
bail!("cannot write document to stdout in watch mode");
|
||||
@ -53,7 +53,7 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
|
||||
};
|
||||
|
||||
// Perform initial compilation.
|
||||
timer.record(&mut world, |world| compile_once(world, &mut config, true))??;
|
||||
timer.record(&mut world, |world| compile_once(world, &mut config))??;
|
||||
|
||||
// Watch all dependencies of the initial compilation.
|
||||
watcher.update(world.dependencies())?;
|
||||
@ -67,7 +67,7 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
|
||||
world.reset();
|
||||
|
||||
// Recompile.
|
||||
timer.record(&mut world, |world| compile_once(world, &mut config, true))??;
|
||||
timer.record(&mut world, |world| compile_once(world, &mut config))??;
|
||||
|
||||
// Evict the cache.
|
||||
comemo::evict(10);
|
||||
@ -293,6 +293,13 @@ impl Status {
|
||||
out.reset()?;
|
||||
writeln!(out, " {}", config.output)?;
|
||||
|
||||
if let Some(server) = &config.server {
|
||||
out.set_color(&color)?;
|
||||
write!(out, "serving at")?;
|
||||
out.reset()?;
|
||||
writeln!(out, " http://{}", server.addr())?;
|
||||
}
|
||||
|
||||
writeln!(out)?;
|
||||
writeln!(out, "[{timestamp}] {}", self.message())?;
|
||||
writeln!(out)?;
|
||||
|
@ -18,7 +18,6 @@ use typst_kit::package::PackageStorage;
|
||||
use typst_timing::timed;
|
||||
|
||||
use crate::args::{Feature, Input, ProcessArgs, WorldArgs};
|
||||
use crate::compile::ExportCache;
|
||||
use crate::download::PrintDownload;
|
||||
use crate::package;
|
||||
|
||||
@ -49,9 +48,6 @@ pub struct SystemWorld {
|
||||
/// always the same within one compilation.
|
||||
/// Reset between compilations if not [`Now::Fixed`].
|
||||
now: Now,
|
||||
/// The export cache, used for caching output files in `typst watch`
|
||||
/// sessions.
|
||||
export_cache: ExportCache,
|
||||
}
|
||||
|
||||
impl SystemWorld {
|
||||
@ -146,7 +142,6 @@ impl SystemWorld {
|
||||
slots: Mutex::new(HashMap::new()),
|
||||
package_storage: package::storage(&world_args.package),
|
||||
now,
|
||||
export_cache: ExportCache::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -191,11 +186,6 @@ impl SystemWorld {
|
||||
pub fn lookup(&self, id: FileId) -> Source {
|
||||
self.source(id).expect("file id does not point to any source file")
|
||||
}
|
||||
|
||||
/// Gets access to the export cache.
|
||||
pub fn export_cache(&self) -> &ExportCache {
|
||||
&self.export_cache
|
||||
}
|
||||
}
|
||||
|
||||
impl World for SystemWorld {
|
||||
|
Loading…
x
Reference in New Issue
Block a user