mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-04 02:08:17 +00:00
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:
parent
86d3b912d4
commit
1b80d8c31d
14 changed files with 608 additions and 592 deletions
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue