feat: model and document project tasks (#1202)

* feat: model and document project tasks

* fix: compile error
This commit is contained in:
Myriad-Dreamin 2025-01-20 20:24:54 +08:00 committed by GitHub
parent 6d1e40d3a9
commit 04f688e122
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 467 additions and 369 deletions

View file

@ -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 = []

View file

@ -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,
}

View file

@ -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")]

View 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,
}