mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-04 02:08:17 +00:00
feat: model and document project tasks (#1202)
* feat: model and document project tasks * fix: compile error
This commit is contained in:
parent
6d1e40d3a9
commit
04f688e122
10 changed files with 467 additions and 369 deletions
|
@ -4,6 +4,20 @@ use proc_macro::TokenStream;
|
|||
use quote::quote;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn toml_model(
|
||||
_metadata: proc_macro::TokenStream,
|
||||
input: proc_macro::TokenStream,
|
||||
) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
let output = quote! {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#input
|
||||
};
|
||||
output.into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(BindTyCtx, attributes(bind))]
|
||||
pub fn bind_ty_ctx(input: TokenStream) -> TokenStream {
|
||||
// Parse the input tokens into a syntax tree
|
||||
|
|
|
@ -30,12 +30,15 @@ serde_json.workspace = true
|
|||
tinymist-world = { workspace = true, features = ["system"] }
|
||||
tinymist-fs.workspace = true
|
||||
tinymist-std.workspace = true
|
||||
tinymist-derive.workspace = true
|
||||
toml.workspace = true
|
||||
typst.workspace = true
|
||||
typst-assets.workspace = true
|
||||
typst-preview.workspace = true
|
||||
notify.workspace = true
|
||||
|
||||
[features]
|
||||
|
||||
fonts = ["typst-assets/fonts"]
|
||||
# "reflexo-typst/no-content-hint"
|
||||
no-content-hint = []
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
use crate::DocIdArgs;
|
||||
use core::fmt;
|
||||
use std::{num::NonZeroUsize, ops::RangeInclusive, str::FromStr};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{num::NonZeroUsize, ops::RangeInclusive, path::Path, str::FromStr, sync::OnceLock};
|
||||
use tinymist_std::{bail, error::prelude::ZResult};
|
||||
pub use tinymist_world::args::{CompileFontArgs, CompilePackageArgs};
|
||||
pub use typst_preview::{PreviewArgs, PreviewMode};
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::ValueEnum;
|
||||
use clap::{ValueEnum, ValueHint};
|
||||
|
||||
use crate::model::*;
|
||||
use crate::PROJECT_ROUTE_USER_ACTION_PRIORITY;
|
||||
|
||||
macro_rules! display_possible_values {
|
||||
($ty:ty) => {
|
||||
|
@ -19,17 +27,7 @@ macro_rules! display_possible_values {
|
|||
|
||||
/// When to export an output file.
|
||||
#[derive(
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Hash,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
serde::Serialize,
|
||||
serde::Deserialize,
|
||||
ValueEnum,
|
||||
Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, ValueEnum, Serialize, Deserialize,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[clap(rename_all = "camelCase")]
|
||||
|
@ -72,9 +70,7 @@ pub enum OutputFormat {
|
|||
display_possible_values!(OutputFormat);
|
||||
|
||||
/// A PDF standard that Typst can enforce conformance with.
|
||||
#[derive(
|
||||
Debug, Copy, Clone, Eq, PartialEq, Hash, ValueEnum, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, ValueEnum, Serialize, Deserialize)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum PdfStandard {
|
||||
/// PDF 1.7.
|
||||
|
@ -170,3 +166,192 @@ fn parse_page_number(value: &str) -> Result<NonZeroUsize, &'static str> {
|
|||
NonZeroUsize::from_str(value).map_err(|_| "not a valid page number")
|
||||
}
|
||||
}
|
||||
|
||||
/// Project document commands.
|
||||
#[derive(Debug, Clone, clap::Subcommand)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
pub enum DocCommands {
|
||||
/// Declare a document (project input).
|
||||
New(DocNewArgs),
|
||||
/// Configure document priority in workspace.
|
||||
Configure(DocConfigureArgs),
|
||||
}
|
||||
|
||||
/// Project task commands.
|
||||
#[derive(Debug, Clone, clap::Subcommand)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
// clippy bug: TaskCompileArgs is evaluated as 0 bytes
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum TaskCommands {
|
||||
/// Declare a compile task (output).
|
||||
Compile(TaskCompileArgs),
|
||||
/// Declare a preview task.
|
||||
Preview(TaskPreviewArgs),
|
||||
}
|
||||
|
||||
/// Declare a document (project's input).
|
||||
#[derive(Debug, Clone, clap::Parser)]
|
||||
pub struct DocNewArgs {
|
||||
/// Argument to identify a project.
|
||||
#[clap(flatten)]
|
||||
pub id: DocIdArgs,
|
||||
/// Configures the project root (for absolute paths).
|
||||
#[clap(long = "root", env = "TYPST_ROOT", value_name = "DIR")]
|
||||
pub root: Option<String>,
|
||||
/// Common font arguments.
|
||||
#[clap(flatten)]
|
||||
pub font: CompileFontArgs,
|
||||
/// Common package arguments.
|
||||
#[clap(flatten)]
|
||||
pub package: CompilePackageArgs,
|
||||
}
|
||||
|
||||
/// Configure project's priorities.
|
||||
#[derive(Debug, Clone, clap::Parser)]
|
||||
pub struct DocConfigureArgs {
|
||||
/// Argument to identify a project.
|
||||
#[clap(flatten)]
|
||||
pub id: DocIdArgs,
|
||||
/// Set the unsigned priority of these task (lower numbers are higher
|
||||
/// priority).
|
||||
#[clap(long = "priority", default_value_t = PROJECT_ROUTE_USER_ACTION_PRIORITY)]
|
||||
pub priority: u32,
|
||||
}
|
||||
|
||||
/// Declare an compile task.
|
||||
#[derive(Debug, Clone, clap::Parser)]
|
||||
pub struct TaskCompileArgs {
|
||||
/// Argument to identify a project.
|
||||
#[clap(flatten)]
|
||||
pub declare: DocNewArgs,
|
||||
|
||||
/// Name a task.
|
||||
#[clap(long = "task")]
|
||||
pub task_name: Option<String>,
|
||||
|
||||
/// When to run the task
|
||||
#[arg(long = "when")]
|
||||
pub when: Option<TaskWhen>,
|
||||
|
||||
/// Path to output file (PDF, PNG, SVG, or HTML). Use `-` to write output to
|
||||
/// stdout.
|
||||
///
|
||||
/// For output formats emitting one file per page (PNG & SVG), a page number
|
||||
/// template must be present if the source document renders to multiple
|
||||
/// pages. Use `{p}` for page numbers, `{0p}` for zero padded page numbers
|
||||
/// and `{t}` for page count. For example, `page-{0p}-of-{t}.png` creates
|
||||
/// `page-01-of-10.png`, `page-02-of-10.png`, and so on.
|
||||
#[clap(value_hint = ValueHint::FilePath)]
|
||||
pub output: Option<String>,
|
||||
|
||||
/// The format of the output file, inferred from the extension by default.
|
||||
#[arg(long = "format", short = 'f')]
|
||||
pub format: Option<OutputFormat>,
|
||||
|
||||
/// Which pages to export. When unspecified, all pages are exported.
|
||||
///
|
||||
/// Pages to export are separated by commas, and can be either simple page
|
||||
/// numbers (e.g. '2,5' to export only pages 2 and 5) or page ranges (e.g.
|
||||
/// '2,3-6,8-' to export page 2, pages 3 to 6 (inclusive), page 8 and any
|
||||
/// pages after it).
|
||||
///
|
||||
/// Page numbers are one-indexed and correspond to physical page numbers in
|
||||
/// the document (therefore not being affected by the document's page
|
||||
/// counter).
|
||||
#[arg(long = "pages", value_delimiter = ',')]
|
||||
pub pages: Option<Vec<Pages>>,
|
||||
|
||||
/// One (or multiple comma-separated) PDF standards that Typst will enforce
|
||||
/// conformance with.
|
||||
#[arg(long = "pdf-standard", value_delimiter = ',')]
|
||||
pub pdf_standard: Vec<PdfStandard>,
|
||||
|
||||
/// The PPI (pixels per inch) to use for PNG export.
|
||||
#[arg(long = "ppi", default_value_t = 144.0)]
|
||||
pub ppi: f32,
|
||||
|
||||
#[clap(skip)]
|
||||
pub output_format: OnceLock<ZResult<OutputFormat>>,
|
||||
}
|
||||
|
||||
impl TaskCompileArgs {
|
||||
pub fn to_task(self, doc_id: Id) -> ZResult<ProjectTask> {
|
||||
let new_task_id = self.task_name.map(Id::new);
|
||||
let task_id = new_task_id.unwrap_or(doc_id.clone());
|
||||
|
||||
let output_format = if let Some(specified) = self.format {
|
||||
specified
|
||||
} else if let Some(output) = &self.output {
|
||||
let output = Path::new(output);
|
||||
|
||||
match output.extension() {
|
||||
Some(ext) if ext.eq_ignore_ascii_case("pdf") => OutputFormat::Pdf,
|
||||
Some(ext) if ext.eq_ignore_ascii_case("png") => OutputFormat::Png,
|
||||
Some(ext) if ext.eq_ignore_ascii_case("svg") => OutputFormat::Svg,
|
||||
Some(ext) if ext.eq_ignore_ascii_case("html") => OutputFormat::Html,
|
||||
_ => bail!(
|
||||
"could not infer output format for path {output:?}.\n\
|
||||
consider providing the format manually with `--format/-f`",
|
||||
),
|
||||
}
|
||||
} else {
|
||||
OutputFormat::Pdf
|
||||
};
|
||||
|
||||
let when = self.when.unwrap_or(TaskWhen::Never);
|
||||
|
||||
let mut transforms = vec![];
|
||||
|
||||
if let Some(pages) = &self.pages {
|
||||
transforms.push(ExportTransform::Pages {
|
||||
ranges: pages.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let export = ExportTask {
|
||||
document: doc_id,
|
||||
id: task_id.clone(),
|
||||
when,
|
||||
transform: transforms,
|
||||
};
|
||||
|
||||
Ok(match output_format {
|
||||
OutputFormat::Pdf => ProjectTask::ExportPdf(ExportPdfTask {
|
||||
export,
|
||||
pdf_standards: self.pdf_standard.clone(),
|
||||
creation_timestamp: None,
|
||||
}),
|
||||
OutputFormat::Png => ProjectTask::ExportPng(ExportPngTask {
|
||||
export,
|
||||
ppi: self.ppi.try_into().unwrap(),
|
||||
fill: None,
|
||||
}),
|
||||
OutputFormat::Svg => ProjectTask::ExportSvg(ExportSvgTask { export }),
|
||||
OutputFormat::Html => ProjectTask::ExportSvg(ExportSvgTask { export }),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Declare an lsp task.
|
||||
#[derive(Debug, Clone, clap::Parser)]
|
||||
pub struct TaskPreviewArgs {
|
||||
/// Argument to identify a project.
|
||||
#[clap(flatten)]
|
||||
pub declare: DocNewArgs,
|
||||
|
||||
/// Name a task.
|
||||
#[clap(long = "task")]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// When to run the task
|
||||
#[arg(long = "when")]
|
||||
pub when: Option<TaskWhen>,
|
||||
|
||||
/// Preview arguments
|
||||
#[clap(flatten)]
|
||||
pub preview: PreviewArgs,
|
||||
|
||||
/// Preview mode
|
||||
#[clap(long = "preview-mode", default_value = "document", value_name = "MODE")]
|
||||
pub preview_mode: PreviewMode,
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use std::{cmp::Ordering, path::Path, str::FromStr};
|
|||
use anyhow::{bail, Context};
|
||||
use clap::ValueHint;
|
||||
use ecow::{eco_vec, EcoVec};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tinymist_std::path::unix_slash;
|
||||
use tinymist_std::ImmutPath;
|
||||
use tinymist_world::EntryReader;
|
||||
|
@ -15,6 +16,9 @@ use typst::syntax::FileId;
|
|||
|
||||
pub use anyhow::Result;
|
||||
|
||||
pub mod task;
|
||||
pub use task::*;
|
||||
|
||||
use crate::LspWorld;
|
||||
|
||||
use super::{Pages, PdfStandard, TaskWhen};
|
||||
|
@ -307,9 +311,7 @@ impl Ord for Scalar {
|
|||
}
|
||||
|
||||
/// A project ID.
|
||||
#[derive(
|
||||
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Id(String);
|
||||
|
||||
|
@ -472,153 +474,6 @@ pub struct ProjectInput {
|
|||
pub package_cache_path: Option<ResourcePath>,
|
||||
}
|
||||
|
||||
/// A project task specifier.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", tag = "type")]
|
||||
pub enum ProjectTask {
|
||||
/// A preview task.
|
||||
Preview(PreviewTask),
|
||||
/// An export PDF task.
|
||||
ExportPdf(ExportPdfTask),
|
||||
/// An export PNG task.
|
||||
ExportPng(ExportPngTask),
|
||||
/// An export SVG task.
|
||||
ExportSvg(ExportSvgTask),
|
||||
/// An export HTML task.
|
||||
ExportHtml(ExportHtmlTask),
|
||||
/// An export Markdown task.
|
||||
ExportMarkdown(ExportMarkdownTask),
|
||||
/// An export Text task.
|
||||
ExportText(ExportTextTask),
|
||||
// todo: compatibility
|
||||
// An export task of another type.
|
||||
// Other(serde_json::Value),
|
||||
}
|
||||
|
||||
impl ProjectTask {
|
||||
/// Returns the task's ID.
|
||||
pub fn doc_id(&self) -> &Id {
|
||||
match self {
|
||||
ProjectTask::Preview(task) => &task.doc_id,
|
||||
ProjectTask::ExportPdf(task) => &task.export.document,
|
||||
ProjectTask::ExportPng(task) => &task.export.document,
|
||||
ProjectTask::ExportSvg(task) => &task.export.document,
|
||||
ProjectTask::ExportHtml(task) => &task.export.document,
|
||||
ProjectTask::ExportMarkdown(task) => &task.export.document,
|
||||
ProjectTask::ExportText(task) => &task.export.document,
|
||||
// ProjectTask::Other(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the task's ID.
|
||||
pub fn id(&self) -> &Id {
|
||||
match self {
|
||||
ProjectTask::Preview(task) => &task.id,
|
||||
ProjectTask::ExportPdf(task) => &task.export.id,
|
||||
ProjectTask::ExportPng(task) => &task.export.id,
|
||||
ProjectTask::ExportSvg(task) => &task.export.id,
|
||||
ProjectTask::ExportHtml(task) => &task.export.id,
|
||||
ProjectTask::ExportMarkdown(task) => &task.export.id,
|
||||
ProjectTask::ExportText(task) => &task.export.id,
|
||||
// ProjectTask::Other(_) => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An lsp task specifier.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct PreviewTask {
|
||||
/// The task's ID.
|
||||
pub id: Id,
|
||||
/// The doc's ID.
|
||||
pub doc_id: Id,
|
||||
/// When to run the task
|
||||
pub when: TaskWhen,
|
||||
}
|
||||
|
||||
/// An export task specifier.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ExportTask {
|
||||
/// The task's ID.
|
||||
pub id: Id,
|
||||
/// The doc's ID.
|
||||
pub document: Id,
|
||||
/// When to run the task
|
||||
pub when: TaskWhen,
|
||||
/// The task's transforms.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
pub transform: Vec<ExportTransform>,
|
||||
}
|
||||
|
||||
/// A project export transform specifier.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ExportTransform {
|
||||
/// Only pick a subset of pages.
|
||||
Pages(Vec<Pages>),
|
||||
}
|
||||
|
||||
/// An export pdf task specifier.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ExportPdfTask {
|
||||
/// The shared export arguments
|
||||
#[serde(flatten)]
|
||||
pub export: ExportTask,
|
||||
/// The pdf standards.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
pub pdf_standards: Vec<PdfStandard>,
|
||||
}
|
||||
|
||||
/// An export png task specifier.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ExportPngTask {
|
||||
/// The shared export arguments
|
||||
#[serde(flatten)]
|
||||
pub export: ExportTask,
|
||||
/// The PPI (pixels per inch) to use for PNG export.
|
||||
pub ppi: Scalar,
|
||||
}
|
||||
|
||||
/// An export svg task specifier.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ExportSvgTask {
|
||||
/// The shared export arguments
|
||||
#[serde(flatten)]
|
||||
pub export: ExportTask,
|
||||
}
|
||||
|
||||
/// An export html task specifier.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ExportHtmlTask {
|
||||
/// The shared export arguments
|
||||
#[serde(flatten)]
|
||||
pub export: ExportTask,
|
||||
}
|
||||
|
||||
/// An export markdown task specifier.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ExportMarkdownTask {
|
||||
/// The shared export arguments
|
||||
#[serde(flatten)]
|
||||
pub export: ExportTask,
|
||||
}
|
||||
|
||||
/// An export text task specifier.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ExportTextTask {
|
||||
/// The shared export arguments
|
||||
#[serde(flatten)]
|
||||
pub export: ExportTask,
|
||||
}
|
||||
|
||||
/// A project route specifier.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
|
|
196
crates/tinymist-project/src/model/task.rs
Normal file
196
crates/tinymist-project/src/model/task.rs
Normal file
|
@ -0,0 +1,196 @@
|
|||
use std::hash::Hash;
|
||||
|
||||
pub use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tinymist_derive::toml_model;
|
||||
|
||||
use super::{Id, Pages, PdfStandard, Scalar, TaskWhen};
|
||||
|
||||
/// A project task specifier.
|
||||
#[toml_model]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ProjectTask {
|
||||
/// A preview task.
|
||||
Preview(PreviewTask),
|
||||
/// An export PDF task.
|
||||
ExportPdf(ExportPdfTask),
|
||||
/// An export PNG task.
|
||||
ExportPng(ExportPngTask),
|
||||
/// An export SVG task.
|
||||
ExportSvg(ExportSvgTask),
|
||||
/// An export HTML task.
|
||||
ExportHtml(ExportHtmlTask),
|
||||
/// An export Markdown task.
|
||||
ExportMarkdown(ExportMarkdownTask),
|
||||
/// An export Text task.
|
||||
ExportText(ExportTextTask),
|
||||
/// An query task.
|
||||
Query(QueryTask),
|
||||
// todo: compatibility
|
||||
// An export task of another type.
|
||||
// Other(serde_json::Value),
|
||||
}
|
||||
|
||||
impl ProjectTask {
|
||||
/// Returns the task's ID.
|
||||
pub fn doc_id(&self) -> &Id {
|
||||
match self {
|
||||
ProjectTask::Preview(task) => &task.document,
|
||||
ProjectTask::ExportPdf(task) => &task.export.document,
|
||||
ProjectTask::ExportPng(task) => &task.export.document,
|
||||
ProjectTask::ExportSvg(task) => &task.export.document,
|
||||
ProjectTask::ExportHtml(task) => &task.export.document,
|
||||
ProjectTask::ExportMarkdown(task) => &task.export.document,
|
||||
ProjectTask::ExportText(task) => &task.export.document,
|
||||
ProjectTask::Query(task) => &task.export.document,
|
||||
// ProjectTask::Other(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the task's ID.
|
||||
pub fn id(&self) -> &Id {
|
||||
match self {
|
||||
ProjectTask::Preview(task) => &task.id,
|
||||
ProjectTask::ExportPdf(task) => &task.export.id,
|
||||
ProjectTask::ExportPng(task) => &task.export.id,
|
||||
ProjectTask::ExportSvg(task) => &task.export.id,
|
||||
ProjectTask::ExportHtml(task) => &task.export.id,
|
||||
ProjectTask::ExportMarkdown(task) => &task.export.id,
|
||||
ProjectTask::ExportText(task) => &task.export.id,
|
||||
ProjectTask::Query(task) => &task.export.id,
|
||||
// ProjectTask::Other(_) => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An lsp task specifier.
|
||||
#[toml_model]
|
||||
pub struct PreviewTask {
|
||||
/// The task's ID.
|
||||
pub id: Id,
|
||||
/// The doc's ID.
|
||||
pub document: Id,
|
||||
/// When to run the task
|
||||
pub when: TaskWhen,
|
||||
}
|
||||
|
||||
/// An export task specifier.
|
||||
#[toml_model]
|
||||
pub struct ExportTask {
|
||||
/// The task's ID.
|
||||
pub id: Id,
|
||||
/// The doc's ID.
|
||||
pub document: Id,
|
||||
/// When to run the task
|
||||
pub when: TaskWhen,
|
||||
/// The task's transforms.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
pub transform: Vec<ExportTransform>,
|
||||
}
|
||||
|
||||
/// A project export transform specifier.
|
||||
#[toml_model]
|
||||
pub enum ExportTransform {
|
||||
/// Only pick a subset of pages.
|
||||
Pages {
|
||||
/// The page ranges to export.
|
||||
ranges: Vec<Pages>,
|
||||
},
|
||||
/// Merge pages into a single page.
|
||||
Merge {
|
||||
/// The gap between pages (in pt).
|
||||
gap: Scalar,
|
||||
},
|
||||
/// Uses a pretty printer to format the output.
|
||||
Pretty {
|
||||
/// The pretty printer id provided by editor.
|
||||
/// If not provided, the default pretty printer will be used.
|
||||
/// Note: the builtin one may be only effective for json outputs.
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
id: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// An export pdf task specifier.
|
||||
#[toml_model]
|
||||
pub struct ExportPdfTask {
|
||||
/// The shared export arguments
|
||||
#[serde(flatten)]
|
||||
pub export: ExportTask,
|
||||
/// The pdf standards.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
pub pdf_standards: Vec<PdfStandard>,
|
||||
/// The document's creation date formatted as a UNIX timestamp (in second
|
||||
/// unit).
|
||||
///
|
||||
/// For more information, see <https://reproducible-builds.org/specs/source-date-epoch/>.
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub creation_timestamp: Option<i64>,
|
||||
}
|
||||
|
||||
/// An export png task specifier.
|
||||
#[toml_model]
|
||||
pub struct ExportPngTask {
|
||||
/// The shared export arguments
|
||||
#[serde(flatten)]
|
||||
pub export: ExportTask,
|
||||
/// The PPI (pixels per inch) to use for PNG export.
|
||||
pub ppi: Scalar,
|
||||
/// The background fill color (in typst script).
|
||||
/// e.g. `#ffffff`, `#000000`, `rgba(255, 255, 255, 0.5)`.
|
||||
///
|
||||
/// If not provided, the default background color specified in the document
|
||||
/// will be used.
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub fill: Option<String>,
|
||||
}
|
||||
|
||||
/// An export svg task specifier.
|
||||
#[toml_model]
|
||||
pub struct ExportSvgTask {
|
||||
/// The shared export arguments
|
||||
#[serde(flatten)]
|
||||
pub export: ExportTask,
|
||||
}
|
||||
|
||||
/// An export html task specifier.
|
||||
#[toml_model]
|
||||
pub struct ExportHtmlTask {
|
||||
/// The shared export arguments
|
||||
#[serde(flatten)]
|
||||
pub export: ExportTask,
|
||||
}
|
||||
|
||||
/// An export markdown task specifier.
|
||||
#[toml_model]
|
||||
pub struct ExportMarkdownTask {
|
||||
/// The shared export arguments
|
||||
#[serde(flatten)]
|
||||
pub export: ExportTask,
|
||||
}
|
||||
|
||||
/// An export text task specifier.
|
||||
#[toml_model]
|
||||
pub struct ExportTextTask {
|
||||
/// The shared export arguments
|
||||
#[serde(flatten)]
|
||||
pub export: ExportTask,
|
||||
}
|
||||
|
||||
/// An export query task specifier.
|
||||
#[toml_model]
|
||||
pub struct QueryTask {
|
||||
/// The shared export arguments
|
||||
#[serde(flatten)]
|
||||
pub export: ExportTask,
|
||||
/// The format to serialize in. Can be `json`, `yaml`, or `txt`,
|
||||
pub format: String,
|
||||
/// Specify a different output extension than the format.
|
||||
pub output_extension: String,
|
||||
/// Defines which elements to retrieve.
|
||||
pub selector: String,
|
||||
/// Extracts just one field from all retrieved elements.
|
||||
pub field: Option<String>,
|
||||
/// Expects and retrieves exactly one element.
|
||||
pub one: bool,
|
||||
}
|
|
@ -44,8 +44,8 @@ impl DiagMessage {}
|
|||
#[non_exhaustive]
|
||||
pub enum ErrKind {
|
||||
None,
|
||||
Msg(String),
|
||||
Diag(DiagMessage),
|
||||
Msg(EcoString),
|
||||
Diag(Box<DiagMessage>),
|
||||
Inner(Error),
|
||||
}
|
||||
|
||||
|
@ -61,43 +61,43 @@ impl ErrKindExt for ErrKind {
|
|||
|
||||
impl ErrKindExt for std::io::Error {
|
||||
fn to_error_kind(self) -> ErrKind {
|
||||
ErrKind::Msg(self.to_string())
|
||||
ErrKind::Msg(self.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrKindExt for String {
|
||||
fn to_error_kind(self) -> ErrKind {
|
||||
ErrKind::Msg(self)
|
||||
ErrKind::Msg(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrKindExt for &str {
|
||||
fn to_error_kind(self) -> ErrKind {
|
||||
ErrKind::Msg(self.to_string())
|
||||
ErrKind::Msg(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrKindExt for &String {
|
||||
fn to_error_kind(self) -> ErrKind {
|
||||
ErrKind::Msg(self.to_string())
|
||||
ErrKind::Msg(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrKindExt for EcoString {
|
||||
fn to_error_kind(self) -> ErrKind {
|
||||
ErrKind::Msg(self.to_string())
|
||||
ErrKind::Msg(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrKindExt for &dyn std::fmt::Display {
|
||||
fn to_error_kind(self) -> ErrKind {
|
||||
ErrKind::Msg(self.to_string())
|
||||
ErrKind::Msg(self.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrKindExt for serde_json::Error {
|
||||
fn to_error_kind(self) -> ErrKind {
|
||||
ErrKind::Msg(self.to_string())
|
||||
ErrKind::Msg(self.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,7 +160,7 @@ impl std::error::Error for Error {}
|
|||
#[cfg(feature = "web")]
|
||||
impl ErrKindExt for wasm_bindgen::JsValue {
|
||||
fn to_error_kind(self) -> ErrKind {
|
||||
ErrKind::Msg(format!("{self:?}"))
|
||||
ErrKind::Msg(ecow::eco_format!("{self:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,7 +179,6 @@ impl From<&Error> for wasm_bindgen::JsValue {
|
|||
}
|
||||
|
||||
pub mod prelude {
|
||||
|
||||
use super::ErrKindExt;
|
||||
use crate::Error;
|
||||
|
||||
|
@ -285,6 +284,20 @@ pub mod prelude {
|
|||
Error::new(loc, crate::ErrKind::None, args)
|
||||
}
|
||||
|
||||
pub fn _msg(loc: &'static str, msg: EcoString) -> Error {
|
||||
Error::new(loc, crate::ErrKind::Msg(msg), Box::new([]))
|
||||
}
|
||||
|
||||
pub use ecow::eco_format as _eco_format;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! bail {
|
||||
($($arg:tt)+) => {{
|
||||
let args = $crate::error::prelude::_eco_format!($($arg)+);
|
||||
return Err($crate::error::prelude::_msg(file!(), args))
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! error_once {
|
||||
($loc:expr, $($arg_key:ident: $arg:expr),+ $(,)?) => {
|
||||
|
@ -315,6 +328,7 @@ pub mod prelude {
|
|||
};
|
||||
}
|
||||
|
||||
use ecow::EcoString;
|
||||
pub use error_once;
|
||||
pub use error_once_map;
|
||||
pub use error_once_map_string;
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::path::Path;
|
|||
use sync_lsp::transport::MirrorArgs;
|
||||
|
||||
use tinymist::{
|
||||
tool::project::{DocCommands, TaskCommands},
|
||||
project::{DocCommands, TaskCommands},
|
||||
CompileFontArgs, CompileOnceArgs,
|
||||
};
|
||||
use tinymist_core::LONG_VERSION;
|
||||
|
|
|
@ -231,6 +231,7 @@ impl ExportConfig {
|
|||
ProjectTask::ExportPdf(ExportPdfTask {
|
||||
export,
|
||||
pdf_standards: Default::default(),
|
||||
creation_timestamp: None,
|
||||
})
|
||||
}
|
||||
Html {} => ProjectTask::ExportHtml(ExportHtmlTask { export }),
|
||||
|
@ -253,7 +254,11 @@ impl ExportConfig {
|
|||
|
||||
let ppi = ppi.unwrap_or(144.) as f32;
|
||||
let ppi = ppi.try_into().unwrap();
|
||||
ProjectTask::ExportPng(ExportPngTask { export, ppi })
|
||||
ProjectTask::ExportPng(ExportPngTask {
|
||||
export,
|
||||
ppi,
|
||||
fill: None,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -3,16 +3,13 @@
|
|||
use std::path::Path;
|
||||
|
||||
use crate::project::*;
|
||||
use anyhow::bail;
|
||||
use clap::ValueHint;
|
||||
use typst_preview::{PreviewArgs, PreviewMode};
|
||||
|
||||
use crate::{CompileFontArgs, CompilePackageArgs};
|
||||
use prelude::ZResult;
|
||||
|
||||
trait LockFileExt {
|
||||
fn preview(&mut self, doc_id: Id, args: &TaskPreviewArgs) -> anyhow::Result<Id>;
|
||||
fn declare(&mut self, args: &DocNewArgs) -> Id;
|
||||
fn export(&mut self, doc_id: Id, args: &TaskCompileArgs) -> anyhow::Result<Id>;
|
||||
fn preview(&mut self, doc_id: Id, args: &TaskPreviewArgs) -> ZResult<Id>;
|
||||
fn compile(&mut self, args: TaskCompileArgs) -> ZResult<Id>;
|
||||
fn export(&mut self, doc_id: Id, args: TaskCompileArgs) -> ZResult<Id>;
|
||||
}
|
||||
|
||||
impl LockFileExt for LockFile {
|
||||
|
@ -59,67 +56,21 @@ impl LockFileExt for LockFile {
|
|||
id
|
||||
}
|
||||
|
||||
fn export(&mut self, doc_id: Id, args: &TaskCompileArgs) -> anyhow::Result<Id> {
|
||||
let task_id = args
|
||||
.task_name
|
||||
.as_ref()
|
||||
.map(|t| Id::new(t.clone()))
|
||||
.unwrap_or(doc_id.clone());
|
||||
fn compile(&mut self, args: TaskCompileArgs) -> ZResult<Id> {
|
||||
let id = self.declare(&args.declare);
|
||||
self.export(id, args)
|
||||
}
|
||||
|
||||
let output_format = if let Some(specified) = args.format {
|
||||
specified
|
||||
} else if let Some(output) = &args.output {
|
||||
let output = Path::new(output);
|
||||
|
||||
match output.extension() {
|
||||
Some(ext) if ext.eq_ignore_ascii_case("pdf") => OutputFormat::Pdf,
|
||||
Some(ext) if ext.eq_ignore_ascii_case("png") => OutputFormat::Png,
|
||||
Some(ext) if ext.eq_ignore_ascii_case("svg") => OutputFormat::Svg,
|
||||
Some(ext) if ext.eq_ignore_ascii_case("html") => OutputFormat::Html,
|
||||
_ => bail!(
|
||||
"could not infer output format for path {}.\n\
|
||||
consider providing the format manually with `--format/-f`",
|
||||
output.display()
|
||||
),
|
||||
}
|
||||
} else {
|
||||
OutputFormat::Pdf
|
||||
};
|
||||
|
||||
let when = args.when.unwrap_or(TaskWhen::Never);
|
||||
|
||||
let mut transforms = vec![];
|
||||
|
||||
if let Some(pages) = &args.pages {
|
||||
transforms.push(ExportTransform::Pages(pages.clone()));
|
||||
}
|
||||
|
||||
let export = ExportTask {
|
||||
document: doc_id,
|
||||
id: task_id.clone(),
|
||||
when,
|
||||
transform: transforms,
|
||||
};
|
||||
|
||||
let task = match output_format {
|
||||
OutputFormat::Pdf => ProjectTask::ExportPdf(ExportPdfTask {
|
||||
export,
|
||||
pdf_standards: args.pdf_standard.clone(),
|
||||
}),
|
||||
OutputFormat::Png => ProjectTask::ExportPng(ExportPngTask {
|
||||
export,
|
||||
ppi: args.ppi.try_into().unwrap(),
|
||||
}),
|
||||
OutputFormat::Svg => ProjectTask::ExportSvg(ExportSvgTask { export }),
|
||||
OutputFormat::Html => ProjectTask::ExportSvg(ExportSvgTask { export }),
|
||||
};
|
||||
fn export(&mut self, doc_id: Id, args: TaskCompileArgs) -> ZResult<Id> {
|
||||
let task = args.to_task(doc_id)?;
|
||||
let task_id = task.id().clone();
|
||||
|
||||
self.replace_task(task);
|
||||
|
||||
Ok(task_id)
|
||||
}
|
||||
|
||||
fn preview(&mut self, doc_id: Id, args: &TaskPreviewArgs) -> anyhow::Result<Id> {
|
||||
fn preview(&mut self, doc_id: Id, args: &TaskPreviewArgs) -> ZResult<Id> {
|
||||
let task_id = args
|
||||
.name
|
||||
.as_ref()
|
||||
|
@ -129,7 +80,7 @@ impl LockFileExt for LockFile {
|
|||
let when = args.when.unwrap_or(TaskWhen::OnType);
|
||||
let task = ProjectTask::Preview(PreviewTask {
|
||||
id: task_id.clone(),
|
||||
doc_id,
|
||||
document: doc_id,
|
||||
when,
|
||||
});
|
||||
|
||||
|
@ -139,132 +90,6 @@ impl LockFileExt for LockFile {
|
|||
}
|
||||
}
|
||||
|
||||
/// Project document commands.
|
||||
#[derive(Debug, Clone, clap::Subcommand)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
pub enum DocCommands {
|
||||
/// Declare a document (project input).
|
||||
New(DocNewArgs),
|
||||
/// Configure document priority in workspace.
|
||||
Configure(DocConfigureArgs),
|
||||
}
|
||||
|
||||
/// Project task commands.
|
||||
#[derive(Debug, Clone, clap::Subcommand)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
pub enum TaskCommands {
|
||||
/// Declare a compile task (output).
|
||||
Compile(TaskCompileArgs),
|
||||
/// Declare a preview task.
|
||||
Preview(TaskPreviewArgs),
|
||||
}
|
||||
|
||||
/// Declare a document (project's input).
|
||||
#[derive(Debug, Clone, clap::Parser)]
|
||||
pub struct DocNewArgs {
|
||||
/// Argument to identify a project.
|
||||
#[clap(flatten)]
|
||||
pub id: DocIdArgs,
|
||||
/// Configures the project root (for absolute paths).
|
||||
#[clap(long = "root", env = "TYPST_ROOT", value_name = "DIR")]
|
||||
pub root: Option<String>,
|
||||
/// Common font arguments.
|
||||
#[clap(flatten)]
|
||||
pub font: CompileFontArgs,
|
||||
/// Common package arguments.
|
||||
#[clap(flatten)]
|
||||
pub package: CompilePackageArgs,
|
||||
}
|
||||
|
||||
/// Configure project's priorities.
|
||||
#[derive(Debug, Clone, clap::Parser)]
|
||||
pub struct DocConfigureArgs {
|
||||
/// Argument to identify a project.
|
||||
#[clap(flatten)]
|
||||
pub id: DocIdArgs,
|
||||
/// Set the unsigned priority of these task (lower numbers are higher
|
||||
/// priority).
|
||||
#[clap(long = "priority", default_value_t = PROJECT_ROUTE_USER_ACTION_PRIORITY)]
|
||||
pub priority: u32,
|
||||
}
|
||||
|
||||
/// Declare an compile task.
|
||||
#[derive(Debug, Clone, clap::Parser)]
|
||||
pub struct TaskCompileArgs {
|
||||
/// Argument to identify a project.
|
||||
#[clap(flatten)]
|
||||
pub declare: DocNewArgs,
|
||||
|
||||
/// Name a task.
|
||||
#[clap(long = "task")]
|
||||
pub task_name: Option<String>,
|
||||
|
||||
/// When to run the task
|
||||
#[arg(long = "when")]
|
||||
pub when: Option<TaskWhen>,
|
||||
|
||||
/// Path to output file (PDF, PNG, SVG, or HTML). Use `-` to write output to
|
||||
/// stdout.
|
||||
///
|
||||
/// For output formats emitting one file per page (PNG & SVG), a page number
|
||||
/// template must be present if the source document renders to multiple
|
||||
/// pages. Use `{p}` for page numbers, `{0p}` for zero padded page numbers
|
||||
/// and `{t}` for page count. For example, `page-{0p}-of-{t}.png` creates
|
||||
/// `page-01-of-10.png`, `page-02-of-10.png`, and so on.
|
||||
#[clap(value_hint = ValueHint::FilePath)]
|
||||
pub output: Option<String>,
|
||||
|
||||
/// The format of the output file, inferred from the extension by default.
|
||||
#[arg(long = "format", short = 'f')]
|
||||
pub format: Option<OutputFormat>,
|
||||
|
||||
/// Which pages to export. When unspecified, all pages are exported.
|
||||
///
|
||||
/// Pages to export are separated by commas, and can be either simple page
|
||||
/// numbers (e.g. '2,5' to export only pages 2 and 5) or page ranges (e.g.
|
||||
/// '2,3-6,8-' to export page 2, pages 3 to 6 (inclusive), page 8 and any
|
||||
/// pages after it).
|
||||
///
|
||||
/// Page numbers are one-indexed and correspond to physical page numbers in
|
||||
/// the document (therefore not being affected by the document's page
|
||||
/// counter).
|
||||
#[arg(long = "pages", value_delimiter = ',')]
|
||||
pub pages: Option<Vec<Pages>>,
|
||||
|
||||
/// One (or multiple comma-separated) PDF standards that Typst will enforce
|
||||
/// conformance with.
|
||||
#[arg(long = "pdf-standard", value_delimiter = ',')]
|
||||
pub pdf_standard: Vec<PdfStandard>,
|
||||
|
||||
/// The PPI (pixels per inch) to use for PNG export.
|
||||
#[arg(long = "ppi", default_value_t = 144.0)]
|
||||
pub ppi: f32,
|
||||
}
|
||||
|
||||
/// Declare an lsp task.
|
||||
#[derive(Debug, Clone, clap::Parser)]
|
||||
pub struct TaskPreviewArgs {
|
||||
/// Argument to identify a project.
|
||||
#[clap(flatten)]
|
||||
pub declare: DocNewArgs,
|
||||
|
||||
/// Name a task.
|
||||
#[clap(long = "task")]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// When to run the task
|
||||
#[arg(long = "when")]
|
||||
pub when: Option<TaskWhen>,
|
||||
|
||||
/// Preview arguments
|
||||
#[clap(flatten)]
|
||||
pub preview: PreviewArgs,
|
||||
|
||||
/// Preview mode
|
||||
#[clap(long = "preview-mode", default_value = "document", value_name = "MODE")]
|
||||
pub preview_mode: PreviewMode,
|
||||
}
|
||||
|
||||
/// Project document commands' main
|
||||
pub fn project_main(args: DocCommands) -> anyhow::Result<()> {
|
||||
LockFile::update(Path::new("."), |state| {
|
||||
|
@ -291,8 +116,7 @@ pub fn task_main(args: TaskCommands) -> anyhow::Result<()> {
|
|||
LockFile::update(Path::new("."), |state| {
|
||||
match args {
|
||||
TaskCommands::Compile(args) => {
|
||||
let id = state.declare(&args.declare);
|
||||
let _ = state.export(id, &args);
|
||||
let _ = state.compile(args);
|
||||
}
|
||||
TaskCommands::Preview(args) => {
|
||||
let id = state.declare(&args.declare);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue