feat: execute export and query on the task model (#1214)

* feat: extract id and doc id from config

* dev: merge `TaskWhen` and move `PathPattern`

* g

* dev: let it compile

* dev: rename a bit

* dev: finish cmd conversions

* dev: configure server

* dev: run export

* dev: clean code

* dev: parse gap on export

* fix: when test
This commit is contained in:
Myriad-Dreamin 2025-01-27 13:14:17 +08:00 committed by GitHub
parent 86d3b912d4
commit 1b80d8c31d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 608 additions and 592 deletions

View file

@ -148,7 +148,7 @@ pub struct TaskCompileArgs {
impl TaskCompileArgs {
/// Convert the arguments to a project task.
pub fn to_task(self, doc_id: Id) -> Result<ProjectTask> {
pub fn to_task(self, doc_id: Id) -> Result<ApplyProjectTask> {
let new_task_id = self.task_name.map(Id::new);
let task_id = new_task_id.unwrap_or(doc_id.clone());
@ -182,13 +182,12 @@ impl TaskCompileArgs {
}
let export = ExportTask {
document: doc_id,
id: task_id.clone(),
when,
output: None,
transform: transforms,
};
Ok(match output_format {
let config = match output_format {
OutputFormat::Pdf => ProjectTask::ExportPdf(ExportPdfTask {
export,
pdf_standards: self.pdf_standard.clone(),
@ -201,6 +200,12 @@ impl TaskCompileArgs {
}),
OutputFormat::Svg => ProjectTask::ExportSvg(ExportSvgTask { export }),
OutputFormat::Html => ProjectTask::ExportSvg(ExportSvgTask { export }),
};
Ok(ApplyProjectTask {
id: task_id.clone(),
document: doc_id,
task: config,
})
}
}

View file

@ -11,7 +11,7 @@ use tinymist_std::{bail, ImmutPath};
use typst::diag::EcoString;
use typst::World;
use crate::model::{Id, ProjectInput, ProjectRoute, ProjectTask, ResourcePath};
use crate::model::{ApplyProjectTask, Id, ProjectInput, ProjectRoute, ResourcePath};
use crate::{LockFile, LockFileCompat, LspWorld, ProjectPathMaterial, LOCK_VERSION};
pub const LOCK_FILENAME: &str = "tinymist.lock";
@ -33,7 +33,7 @@ impl LockFile {
}
}
pub fn replace_task(&mut self, task: ProjectTask) {
pub fn replace_task(&mut self, task: ApplyProjectTask) {
let id = task.id().clone();
let index = self.task.iter().position(|i| *i.id() == id);
if let Some(index) = index {
@ -225,7 +225,7 @@ pub fn update_lock(root: ImmutPath) -> LockFileUpdate {
enum LockUpdate {
Input(ProjectInput),
Task(ProjectTask),
Task(ApplyProjectTask),
Material(ProjectPathMaterial),
Route(ProjectRoute),
}
@ -278,7 +278,7 @@ impl LockFileUpdate {
Some(id)
}
pub fn task(&mut self, task: ProjectTask) {
pub fn task(&mut self, task: ApplyProjectTask) {
self.updates.push(LockUpdate::Task(task));
}

View file

@ -9,9 +9,10 @@ use clap::ValueEnum;
use ecow::EcoVec;
use serde::{Deserialize, Serialize};
use tinymist_std::error::prelude::*;
use tinymist_std::path::unix_slash;
use tinymist_std::path::{unix_slash, PathClean};
use tinymist_std::{bail, ImmutPath};
use tinymist_world::EntryReader;
use tinymist_world::vfs::WorkspaceResolver;
use tinymist_world::{EntryReader, EntryState};
use typst::diag::EcoString;
use typst::syntax::FileId;
@ -24,7 +25,7 @@ use crate::LspWorld;
pub const LOCK_VERSION: &str = "0.1.0-beta0";
/// A scalar that is not NaN.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct Scalar(f32);
impl TryFrom<f32> for Scalar {
@ -39,6 +40,13 @@ impl TryFrom<f32> for Scalar {
}
}
impl Scalar {
/// Converts the scalar to an f32.
pub fn to_f32(self) -> f32 {
self.0
}
}
impl PartialEq for Scalar {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
@ -118,17 +126,16 @@ macro_rules! display_possible_values {
/// alias typst="tinymist compile --when=onSave"
/// typst compile main.typ
/// ```
#[derive(
Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, ValueEnum, Serialize, Deserialize,
)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Hash, ValueEnum, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[clap(rename_all = "camelCase")]
pub enum TaskWhen {
/// Never watch to run task.
#[default]
Never,
/// Run task on save.
/// Run task on saving the document, i.e. on `textDocument/didSave` events.
OnSave,
/// Run task on type.
/// Run task on typing, i.e. on `textDocument/didChange` events.
OnType,
/// *DEPRECATED* Run task when a document has a title and on saved, which is
/// useful to filter out template files.
@ -161,6 +168,71 @@ pub enum OutputFormat {
display_possible_values!(OutputFormat);
/// The path pattern that could be substituted.
///
/// # Examples
/// - `$root` is the root of the project.
/// - `$root/$dir` is the parent directory of the input (main) file.
/// - `$root/main` will help store pdf file to `$root/main.pdf` constantly.
/// - (default) `$root/$dir/$name` will help store pdf file along with the input
/// file.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PathPattern(pub String);
impl PathPattern {
/// Creates a new path pattern.
pub fn new(pattern: &str) -> Self {
Self(pattern.to_owned())
}
/// Substitutes the path pattern with `$root`, and `$dir/$name`.
pub fn substitute(&self, entry: &EntryState) -> Option<ImmutPath> {
self.substitute_impl(entry.root(), entry.main())
}
#[comemo::memoize]
fn substitute_impl(&self, root: Option<ImmutPath>, main: Option<FileId>) -> Option<ImmutPath> {
log::info!("Check path {main:?} and root {root:?} with output directory {self:?}");
let (root, main) = root.zip(main)?;
// Files in packages are not exported
if WorkspaceResolver::is_package_file(main) {
return None;
}
// Files without a path are not exported
let path = main.vpath().resolve(&root)?;
// todo: handle untitled path
if let Ok(path) = path.strip_prefix("/untitled") {
let tmp = std::env::temp_dir();
let path = tmp.join("typst").join(path);
return Some(path.as_path().into());
}
if self.0.is_empty() {
return Some(path.to_path_buf().clean().into());
}
let path = path.strip_prefix(&root).ok()?;
let dir = path.parent();
let file_name = path.file_name().unwrap_or_default();
let w = root.to_string_lossy();
let f = file_name.to_string_lossy();
// replace all $root
let mut path = self.0.replace("$root", &w);
if let Some(dir) = dir {
let d = dir.to_string_lossy();
path = path.replace("$dir", &d);
}
path = path.replace("$name", &f);
Some(PathBuf::from(path).clean().into())
}
}
/// A PDF standard that Typst can enforce conformance with.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, ValueEnum, Serialize, Deserialize)]
#[allow(non_camel_case_types)]
@ -185,6 +257,11 @@ display_possible_values!(PdfStandard);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Pages(pub RangeInclusive<Option<NonZeroUsize>>);
impl Pages {
/// Selects the first page.
pub const FIRST: Pages = Pages(NonZeroUsize::new(1)..=None);
}
impl FromStr for Pages {
type Err = &'static str;
@ -392,7 +469,7 @@ pub struct LockFile {
pub document: Vec<ProjectInput>,
/// The project's task (output).
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub task: Vec<ProjectTask>,
pub task: Vec<ApplyProjectTask>,
/// The project's task route.
#[serde(skip_serializing_if = "EcoVec::is_empty", default)]
pub route: EcoVec<ProjectRoute>,
@ -472,3 +549,33 @@ pub struct ProjectRoute {
/// The priority of the project. (lower numbers are higher priority).
pub priority: u32,
}
#[cfg(test)]
mod tests {
use super::*;
use typst::syntax::VirtualPath;
#[test]
fn test_substitute_path() {
let root = Path::new("/root");
let entry =
EntryState::new_rooted(root.into(), Some(VirtualPath::new("/dir1/dir2/file.txt")));
assert_eq!(
PathPattern::new("/substitute/$dir/$name").substitute(&entry),
Some(PathBuf::from("/substitute/dir1/dir2/file.txt").into())
);
assert_eq!(
PathPattern::new("/substitute/$dir/../$name").substitute(&entry),
Some(PathBuf::from("/substitute/dir1/file.txt").into())
);
assert_eq!(
PathPattern::new("/substitute/$name").substitute(&entry),
Some(PathBuf::from("/substitute/file.txt").into())
);
assert_eq!(
PathPattern::new("/substitute/target/$dir/$name").substitute(&entry),
Some(PathBuf::from("/substitute/target/dir1/dir2/file.txt").into())
);
}
}

View file

@ -4,11 +4,11 @@ use std::hash::Hash;
use serde::{Deserialize, Serialize};
use super::{Id, Pages, PdfStandard, Scalar, TaskWhen};
use super::{Id, Pages, PathPattern, PdfStandard, Scalar, TaskWhen};
/// A project task specifier. This is used for specifying tasks in a project.
/// When the language service notifies an update event of the project, it will
/// check whether any associated tasks need to be run.
/// A project task application specifier. This is used for specifying tasks to
/// run in a project. When the language service notifies an update event of the
/// project, it will check whether any associated tasks need to be run.
///
/// Each task can have different timing and conditions for running. See
/// [`TaskWhen`] for more information.
@ -32,6 +32,31 @@ use super::{Id, Pages, PdfStandard, Scalar, TaskWhen};
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "type")]
pub struct ApplyProjectTask {
/// The task's ID.
pub id: Id,
/// The document's ID.
pub document: Id,
/// The task to run.
#[serde(flatten)]
pub task: ProjectTask,
}
impl ApplyProjectTask {
/// Returns the document's ID.
pub fn doc_id(&self) -> &Id {
&self.document
}
/// Returns the task's ID.
pub fn id(&self) -> &Id {
&self.id
}
}
/// A project task specifier. This structure specifies the arguments for a task.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "type")]
pub enum ProjectTask {
/// A preview task.
Preview(PreviewTask),
@ -55,33 +80,48 @@ pub enum ProjectTask {
}
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 timing of executing the task.
pub fn when(&self) -> Option<TaskWhen> {
Some(match self {
Self::Preview(task) => task.when,
Self::ExportPdf(..)
| Self::ExportPng(..)
| Self::ExportSvg(..)
| Self::ExportHtml(..)
| Self::ExportMarkdown(..)
| Self::ExportText(..)
| Self::Query(..) => self.as_export()?.when,
})
}
/// Returns the document's ID.
pub fn id(&self) -> &Id {
/// Returns the export configuration of a task.
pub fn as_export(&self) -> Option<&ExportTask> {
Some(match self {
Self::Preview(..) => return None,
Self::ExportPdf(task) => &task.export,
Self::ExportPng(task) => &task.export,
Self::ExportSvg(task) => &task.export,
Self::ExportHtml(task) => &task.export,
Self::ExportMarkdown(task) => &task.export,
Self::ExportText(task) => &task.export,
Self::Query(task) => &task.export,
})
}
/// Returns extension of the artifact.
pub fn extension(&self) -> &str {
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,
Self::ExportPdf { .. } => "pdf",
Self::Preview(..) | Self::ExportHtml { .. } => "html",
Self::ExportMarkdown { .. } => "md",
Self::ExportText { .. } => "txt",
Self::ExportSvg { .. } => "svg",
Self::ExportPng { .. } => "png",
Self::Query(QueryTask {
format,
output_extension,
..
}) => output_extension.as_deref().unwrap_or(format),
}
}
}
@ -90,30 +130,55 @@ impl ProjectTask {
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct PreviewTask {
/// The task's ID.
pub id: Id,
/// The document's ID.
pub document: Id,
/// When to run the task. See [`TaskWhen`] for more
/// information.
pub when: TaskWhen,
}
/// An export task specifier.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ExportTask {
/// The task's ID.
pub id: Id,
/// The document's ID.
pub document: Id,
/// When to run the task
pub when: TaskWhen,
/// The output path pattern.
pub output: Option<PathPattern>,
/// The task's transforms.
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub transform: Vec<ExportTransform>,
}
impl ExportTask {
/// Creates a new unmounted export task.
pub fn new(when: TaskWhen) -> Self {
Self {
when,
output: None,
transform: Vec::new(),
}
}
/// Pretty prints the output whenever possible.
pub fn apply_pretty(&mut self) {
self.transform
.push(ExportTransform::Pretty { script: None });
}
}
/// The legacy page selection specifier.
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PageSelection {
/// Selects the first page.
#[default]
First,
/// Merges all pages into a single page.
Merged {
/// The gap between pages (in pt).
gap: Option<String>,
},
}
/// A project export transform specifier.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
@ -125,8 +190,8 @@ pub enum ExportTransform {
},
/// Merge pages into a single page.
Merge {
/// The gap between pages (in pt).
gap: Scalar,
/// The gap between pages (typst code expression, e.g. `1pt`).
gap: Option<String>,
},
/// Execute a transform script.
Script {
@ -228,7 +293,7 @@ pub struct QueryTask {
pub format: String,
/// Uses a different output extension from the one inferring from the
/// [`Self::format`].
pub output_extension: String,
pub output_extension: Option<String>,
/// Defines which elements to retrieve.
pub selector: String,
/// Extracts just one field from all retrieved elements.