mirror of
https://github.com/typst/typst
synced 2025-05-23 21:45:29 +08:00
Support reading input from stdin (#3339)
This commit is contained in:
parent
302b870321
commit
70b354e887
@ -65,6 +65,7 @@ pub struct CompileCommand {
|
|||||||
pub common: SharedArgs,
|
pub common: SharedArgs,
|
||||||
|
|
||||||
/// Path to output file (PDF, PNG, or SVG)
|
/// Path to output file (PDF, PNG, or SVG)
|
||||||
|
#[clap(required_if_eq("input", "-"))]
|
||||||
pub output: Option<PathBuf>,
|
pub output: Option<PathBuf>,
|
||||||
|
|
||||||
/// The format of the output file, inferred from the extension by default
|
/// The format of the output file, inferred from the extension by default
|
||||||
@ -121,8 +122,9 @@ pub enum SerializationFormat {
|
|||||||
/// Common arguments of compile, watch, and query.
|
/// Common arguments of compile, watch, and query.
|
||||||
#[derive(Debug, Clone, Args)]
|
#[derive(Debug, Clone, Args)]
|
||||||
pub struct SharedArgs {
|
pub struct SharedArgs {
|
||||||
/// Path to input Typst file
|
/// Path to input Typst file, use `-` to read input from stdin
|
||||||
pub input: PathBuf,
|
#[clap(value_parser = input_value_parser)]
|
||||||
|
pub input: Input,
|
||||||
|
|
||||||
/// Configures the project root (for absolute paths)
|
/// Configures the project root (for absolute paths)
|
||||||
#[clap(long = "root", env = "TYPST_ROOT", value_name = "DIR")]
|
#[clap(long = "root", env = "TYPST_ROOT", value_name = "DIR")]
|
||||||
@ -155,6 +157,26 @@ pub struct SharedArgs {
|
|||||||
pub diagnostic_format: DiagnosticFormat,
|
pub diagnostic_format: DiagnosticFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An input that is either stdin or a real path.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Input {
|
||||||
|
/// Stdin, represented by `-`.
|
||||||
|
Stdin,
|
||||||
|
/// A non-empty path.
|
||||||
|
Path(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The clap value parser used by `SharedArgs.input`
|
||||||
|
fn input_value_parser(value: &str) -> Result<Input, clap::error::Error> {
|
||||||
|
if value.is_empty() {
|
||||||
|
Err(clap::Error::new(clap::error::ErrorKind::InvalidValue))
|
||||||
|
} else if value == "-" {
|
||||||
|
Ok(Input::Stdin)
|
||||||
|
} else {
|
||||||
|
Ok(Input::Path(value.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Parses key/value pairs split by the first equal sign.
|
/// Parses key/value pairs split by the first equal sign.
|
||||||
///
|
///
|
||||||
/// This function will return an error if the argument contains no equals sign
|
/// This function will return an error if the argument contains no equals sign
|
||||||
|
@ -16,7 +16,7 @@ use typst::syntax::{FileId, Source, Span};
|
|||||||
use typst::visualize::Color;
|
use typst::visualize::Color;
|
||||||
use typst::{World, WorldExt};
|
use typst::{World, WorldExt};
|
||||||
|
|
||||||
use crate::args::{CompileCommand, DiagnosticFormat, OutputFormat};
|
use crate::args::{CompileCommand, DiagnosticFormat, Input, OutputFormat};
|
||||||
use crate::timings::Timer;
|
use crate::timings::Timer;
|
||||||
use crate::watch::Status;
|
use crate::watch::Status;
|
||||||
use crate::world::SystemWorld;
|
use crate::world::SystemWorld;
|
||||||
@ -29,7 +29,10 @@ impl CompileCommand {
|
|||||||
/// The output path.
|
/// The output path.
|
||||||
pub fn output(&self) -> PathBuf {
|
pub fn output(&self) -> PathBuf {
|
||||||
self.output.clone().unwrap_or_else(|| {
|
self.output.clone().unwrap_or_else(|| {
|
||||||
self.common.input.with_extension(
|
let Input::Path(path) = &self.common.input else {
|
||||||
|
panic!("output must be specified when input is from stdin, as guarded by the CLI");
|
||||||
|
};
|
||||||
|
path.with_extension(
|
||||||
match self.output_format().unwrap_or(OutputFormat::Pdf) {
|
match self.output_format().unwrap_or(OutputFormat::Pdf) {
|
||||||
OutputFormat::Pdf => "pdf",
|
OutputFormat::Pdf => "pdf",
|
||||||
OutputFormat::Png => "png",
|
OutputFormat::Png => "png",
|
||||||
@ -163,8 +166,8 @@ fn export_pdf(
|
|||||||
command: &CompileCommand,
|
command: &CompileCommand,
|
||||||
world: &SystemWorld,
|
world: &SystemWorld,
|
||||||
) -> StrResult<()> {
|
) -> StrResult<()> {
|
||||||
let ident = world.input().to_string_lossy();
|
let ident = world.input().map(|i| i.to_string_lossy());
|
||||||
let buffer = typst_pdf::pdf(document, Some(&ident), now());
|
let buffer = typst_pdf::pdf(document, ident.as_deref(), now());
|
||||||
let output = command.output();
|
let output = command.output();
|
||||||
fs::write(output, buffer)
|
fs::write(output, buffer)
|
||||||
.map_err(|err| eco_format!("failed to write PDF file ({err})"))?;
|
.map_err(|err| eco_format!("failed to write PDF file ({err})"))?;
|
||||||
|
@ -9,7 +9,7 @@ use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
|||||||
use same_file::is_same_file;
|
use same_file::is_same_file;
|
||||||
use typst::diag::StrResult;
|
use typst::diag::StrResult;
|
||||||
|
|
||||||
use crate::args::CompileCommand;
|
use crate::args::{CompileCommand, Input};
|
||||||
use crate::compile::compile_once;
|
use crate::compile::compile_once;
|
||||||
use crate::terminal;
|
use crate::terminal;
|
||||||
use crate::timings::Timer;
|
use crate::timings::Timer;
|
||||||
@ -168,7 +168,10 @@ impl Status {
|
|||||||
term_out.set_color(&color)?;
|
term_out.set_color(&color)?;
|
||||||
write!(term_out, "watching")?;
|
write!(term_out, "watching")?;
|
||||||
term_out.reset()?;
|
term_out.reset()?;
|
||||||
writeln!(term_out, " {}", command.common.input.display())?;
|
match &command.common.input {
|
||||||
|
Input::Stdin => writeln!(term_out, " <stdin>"),
|
||||||
|
Input::Path(path) => writeln!(term_out, " {}", path.display()),
|
||||||
|
}?;
|
||||||
|
|
||||||
term_out.set_color(&color)?;
|
term_out.set_color(&color)?;
|
||||||
write!(term_out, "writing to")?;
|
write!(term_out, "writing to")?;
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::io::Read;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::{fs, mem};
|
use std::{fs, io, mem};
|
||||||
|
|
||||||
use chrono::{DateTime, Datelike, Local};
|
use chrono::{DateTime, Datelike, Local};
|
||||||
use comemo::Prehashed;
|
use comemo::Prehashed;
|
||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use typst::diag::{FileError, FileResult, StrResult};
|
use typst::diag::{FileError, FileResult, StrResult};
|
||||||
use typst::foundations::{Bytes, Datetime, Dict, IntoValue};
|
use typst::foundations::{Bytes, Datetime, Dict, IntoValue};
|
||||||
@ -14,17 +16,22 @@ use typst::text::{Font, FontBook};
|
|||||||
use typst::{Library, World};
|
use typst::{Library, World};
|
||||||
use typst_timing::{timed, TimingScope};
|
use typst_timing::{timed, TimingScope};
|
||||||
|
|
||||||
use crate::args::SharedArgs;
|
use crate::args::{Input, SharedArgs};
|
||||||
use crate::compile::ExportCache;
|
use crate::compile::ExportCache;
|
||||||
use crate::fonts::{FontSearcher, FontSlot};
|
use crate::fonts::{FontSearcher, FontSlot};
|
||||||
use crate::package::prepare_package;
|
use crate::package::prepare_package;
|
||||||
|
|
||||||
|
/// Static `FileId` allocated for stdin.
|
||||||
|
/// This is to ensure that a file is read in the correct way.
|
||||||
|
static STDIN_ID: Lazy<FileId> =
|
||||||
|
Lazy::new(|| FileId::new_fake(VirtualPath::new("<stdin>")));
|
||||||
|
|
||||||
/// A world that provides access to the operating system.
|
/// A world that provides access to the operating system.
|
||||||
pub struct SystemWorld {
|
pub struct SystemWorld {
|
||||||
/// The working directory.
|
/// The working directory.
|
||||||
workdir: Option<PathBuf>,
|
workdir: Option<PathBuf>,
|
||||||
/// The canonical path to the input file.
|
/// The canonical path to the input file.
|
||||||
input: PathBuf,
|
input: Option<PathBuf>,
|
||||||
/// The root relative to which absolute paths are resolved.
|
/// The root relative to which absolute paths are resolved.
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
/// The input path.
|
/// The input path.
|
||||||
@ -52,25 +59,34 @@ impl SystemWorld {
|
|||||||
searcher.search(&command.font_paths);
|
searcher.search(&command.font_paths);
|
||||||
|
|
||||||
// Resolve the system-global input path.
|
// Resolve the system-global input path.
|
||||||
let input = command.input.canonicalize().map_err(|_| {
|
let input = match &command.input {
|
||||||
eco_format!("input file not found (searched at {})", command.input.display())
|
Input::Stdin => None,
|
||||||
})?;
|
Input::Path(path) => Some(path.canonicalize().map_err(|_| {
|
||||||
|
eco_format!("input file not found (searched at {})", path.display())
|
||||||
|
})?),
|
||||||
|
};
|
||||||
|
|
||||||
// Resolve the system-global root directory.
|
// Resolve the system-global root directory.
|
||||||
let root = {
|
let root = {
|
||||||
let path = command
|
let path = command
|
||||||
.root
|
.root
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.or_else(|| input.parent())
|
.or_else(|| input.as_deref().and_then(|i| i.parent()))
|
||||||
.unwrap_or(Path::new("."));
|
.unwrap_or(Path::new("."));
|
||||||
path.canonicalize().map_err(|_| {
|
path.canonicalize().map_err(|_| {
|
||||||
eco_format!("root directory not found (searched at {})", path.display())
|
eco_format!("root directory not found (searched at {})", path.display())
|
||||||
})?
|
})?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let main = if let Some(path) = &input {
|
||||||
// Resolve the virtual path of the main file within the project root.
|
// Resolve the virtual path of the main file within the project root.
|
||||||
let main_path = VirtualPath::within_root(&input, &root)
|
let main_path = VirtualPath::within_root(path, &root)
|
||||||
.ok_or("source file must be contained in project root")?;
|
.ok_or("source file must be contained in project root")?;
|
||||||
|
FileId::new(None, main_path)
|
||||||
|
} else {
|
||||||
|
// Return the special id of STDIN otherwise
|
||||||
|
*STDIN_ID
|
||||||
|
};
|
||||||
|
|
||||||
let library = {
|
let library = {
|
||||||
// Convert the input pairs to a dictionary.
|
// Convert the input pairs to a dictionary.
|
||||||
@ -87,7 +103,7 @@ impl SystemWorld {
|
|||||||
workdir: std::env::current_dir().ok(),
|
workdir: std::env::current_dir().ok(),
|
||||||
input,
|
input,
|
||||||
root,
|
root,
|
||||||
main: FileId::new(None, main_path),
|
main,
|
||||||
library: Prehashed::new(library),
|
library: Prehashed::new(library),
|
||||||
book: Prehashed::new(searcher.book),
|
book: Prehashed::new(searcher.book),
|
||||||
fonts: searcher.fonts,
|
fonts: searcher.fonts,
|
||||||
@ -130,8 +146,8 @@ impl SystemWorld {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return the canonical path to the input file.
|
/// Return the canonical path to the input file.
|
||||||
pub fn input(&self) -> &PathBuf {
|
pub fn input(&self) -> Option<&PathBuf> {
|
||||||
&self.input
|
self.input.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lookup a source file by id.
|
/// Lookup a source file by id.
|
||||||
@ -231,7 +247,7 @@ impl FileSlot {
|
|||||||
/// Retrieve the source for this file.
|
/// Retrieve the source for this file.
|
||||||
fn source(&mut self, project_root: &Path) -> FileResult<Source> {
|
fn source(&mut self, project_root: &Path) -> FileResult<Source> {
|
||||||
self.source.get_or_init(
|
self.source.get_or_init(
|
||||||
|| system_path(project_root, self.id),
|
|| read(self.id, project_root),
|
||||||
|data, prev| {
|
|data, prev| {
|
||||||
let name = if prev.is_some() { "reparsing file" } else { "parsing file" };
|
let name = if prev.is_some() { "reparsing file" } else { "parsing file" };
|
||||||
let _scope = TimingScope::new(name, None);
|
let _scope = TimingScope::new(name, None);
|
||||||
@ -249,7 +265,7 @@ impl FileSlot {
|
|||||||
/// Retrieve the file's bytes.
|
/// Retrieve the file's bytes.
|
||||||
fn file(&mut self, project_root: &Path) -> FileResult<Bytes> {
|
fn file(&mut self, project_root: &Path) -> FileResult<Bytes> {
|
||||||
self.file
|
self.file
|
||||||
.get_or_init(|| system_path(project_root, self.id), |data, _| Ok(data.into()))
|
.get_or_init(|| read(self.id, project_root), |data, _| Ok(data.into()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,7 +299,7 @@ impl<T: Clone> SlotCell<T> {
|
|||||||
/// Gets the contents of the cell or initialize them.
|
/// Gets the contents of the cell or initialize them.
|
||||||
fn get_or_init(
|
fn get_or_init(
|
||||||
&mut self,
|
&mut self,
|
||||||
path: impl FnOnce() -> FileResult<PathBuf>,
|
load: impl FnOnce() -> FileResult<Vec<u8>>,
|
||||||
f: impl FnOnce(Vec<u8>, Option<T>) -> FileResult<T>,
|
f: impl FnOnce(Vec<u8>, Option<T>) -> FileResult<T>,
|
||||||
) -> FileResult<T> {
|
) -> FileResult<T> {
|
||||||
// If we accessed the file already in this compilation, retrieve it.
|
// If we accessed the file already in this compilation, retrieve it.
|
||||||
@ -294,7 +310,7 @@ impl<T: Clone> SlotCell<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read and hash the file.
|
// Read and hash the file.
|
||||||
let result = timed!("loading file", path().and_then(|p| read(&p)));
|
let result = timed!("loading file", load());
|
||||||
let fingerprint = timed!("hashing file", typst::util::hash128(&result));
|
let fingerprint = timed!("hashing file", typst::util::hash128(&result));
|
||||||
|
|
||||||
// If the file contents didn't change, yield the old processed data.
|
// If the file contents didn't change, yield the old processed data.
|
||||||
@ -329,8 +345,20 @@ fn system_path(project_root: &Path, id: FileId) -> FileResult<PathBuf> {
|
|||||||
id.vpath().resolve(root).ok_or(FileError::AccessDenied)
|
id.vpath().resolve(root).ok_or(FileError::AccessDenied)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read a file.
|
/// Reads a file from a `FileId`.
|
||||||
fn read(path: &Path) -> FileResult<Vec<u8>> {
|
///
|
||||||
|
/// If the ID represents stdin it will read from standard input,
|
||||||
|
/// otherwise it gets the file path of the ID and reads the file from disk.
|
||||||
|
fn read(id: FileId, project_root: &Path) -> FileResult<Vec<u8>> {
|
||||||
|
if id == *STDIN_ID {
|
||||||
|
read_from_stdin()
|
||||||
|
} else {
|
||||||
|
read_from_disk(&system_path(project_root, id)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a file from disk.
|
||||||
|
fn read_from_disk(path: &Path) -> FileResult<Vec<u8>> {
|
||||||
let f = |e| FileError::from_io(e, path);
|
let f = |e| FileError::from_io(e, path);
|
||||||
if fs::metadata(path).map_err(f)?.is_dir() {
|
if fs::metadata(path).map_err(f)?.is_dir() {
|
||||||
Err(FileError::IsDirectory)
|
Err(FileError::IsDirectory)
|
||||||
@ -339,6 +367,18 @@ fn read(path: &Path) -> FileResult<Vec<u8>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read from stdin.
|
||||||
|
fn read_from_stdin() -> FileResult<Vec<u8>> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let result = io::stdin().read_to_end(&mut buf);
|
||||||
|
match result {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => (),
|
||||||
|
Err(err) => return Err(FileError::from_io(err, Path::new("<stdin>"))),
|
||||||
|
}
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
/// Decode UTF-8 with an optional BOM.
|
/// Decode UTF-8 with an optional BOM.
|
||||||
fn decode_utf8(buf: &[u8]) -> FileResult<&str> {
|
fn decode_utf8(buf: &[u8]) -> FileResult<&str> {
|
||||||
// Remove UTF-8 BOM.
|
// Remove UTF-8 BOM.
|
||||||
|
@ -57,6 +57,24 @@ impl FileId {
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new unique ("fake") file specification, which is not
|
||||||
|
/// accessible by path.
|
||||||
|
///
|
||||||
|
/// Caution: the ID returned by this method is the *only* identifier of the
|
||||||
|
/// file, constructing a file ID with a path will *not* reuse the ID even
|
||||||
|
/// if the path is the same. This method should only be used for generating
|
||||||
|
/// "virtual" file ids such as content read from stdin.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn new_fake(path: VirtualPath) -> Self {
|
||||||
|
let mut interner = INTERNER.write().unwrap();
|
||||||
|
let num = interner.from_id.len().try_into().expect("out of file ids");
|
||||||
|
|
||||||
|
let id = FileId(num);
|
||||||
|
let leaked = Box::leak(Box::new((None, path)));
|
||||||
|
interner.from_id.push(leaked);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
/// The package the file resides in, if any.
|
/// The package the file resides in, if any.
|
||||||
pub fn package(&self) -> Option<&'static PackageSpec> {
|
pub fn package(&self) -> Option<&'static PackageSpec> {
|
||||||
self.pair().0.as_ref()
|
self.pair().0.as_ref()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user