mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-07-07 13:05:02 +00:00
feat: CLI compile documents with lock updates (#1218)
This commit is contained in:
parent
ebd811db13
commit
b541daf50e
10 changed files with 238 additions and 144 deletions
|
@ -22,11 +22,7 @@ pub enum DocCommands {
|
|||
/// 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),
|
||||
}
|
||||
|
@ -48,6 +44,50 @@ pub struct DocNewArgs {
|
|||
pub package: CompilePackageArgs,
|
||||
}
|
||||
|
||||
impl DocNewArgs {
|
||||
/// Converts to project input.
|
||||
pub fn to_input(&self) -> ProjectInput {
|
||||
let id: Id = (&self.id).into();
|
||||
|
||||
let root = self
|
||||
.root
|
||||
.as_ref()
|
||||
.map(|root| ResourcePath::from_user_sys(Path::new(root)));
|
||||
let main = ResourcePath::from_user_sys(Path::new(&self.id.input));
|
||||
|
||||
let font_paths = self
|
||||
.font
|
||||
.font_paths
|
||||
.iter()
|
||||
.map(|p| ResourcePath::from_user_sys(p))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let package_path = self
|
||||
.package
|
||||
.package_path
|
||||
.as_ref()
|
||||
.map(|p| ResourcePath::from_user_sys(p));
|
||||
|
||||
let package_cache_path = self
|
||||
.package
|
||||
.package_cache_path
|
||||
.as_ref()
|
||||
.map(|p| ResourcePath::from_user_sys(p));
|
||||
|
||||
ProjectInput {
|
||||
id: id.clone(),
|
||||
root,
|
||||
main,
|
||||
// todo: inputs
|
||||
inputs: vec![],
|
||||
font_paths,
|
||||
system_fonts: !self.font.ignore_system_fonts,
|
||||
package_path,
|
||||
package_cache_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The id of a document.
|
||||
///
|
||||
/// If an identifier is not provided, the document's path is used as the id.
|
||||
|
|
|
@ -37,7 +37,7 @@ pub struct ProjectInsId(EcoString);
|
|||
///
|
||||
/// Whether to export depends on the current state of the document and the user
|
||||
/// settings.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ExportSignal {
|
||||
/// Whether the revision is annotated by memory events.
|
||||
pub by_mem_events: bool,
|
||||
|
@ -62,6 +62,17 @@ pub struct CompileSnapshot<F: CompilerFeat> {
|
|||
}
|
||||
|
||||
impl<F: CompilerFeat + 'static> CompileSnapshot<F> {
|
||||
/// Creates a snapshot from the world.
|
||||
pub fn from_world(world: CompilerWorld<F>) -> Self {
|
||||
Self {
|
||||
id: ProjectInsId("primary".into()),
|
||||
signal: ExportSignal::default(),
|
||||
env: CompileEnv::default(),
|
||||
world,
|
||||
success_doc: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Forks a new snapshot that compiles a different document.
|
||||
///
|
||||
/// Note: the resulting document should not be shared in system, because we
|
||||
|
|
|
@ -23,6 +23,10 @@ impl LockFile {
|
|||
self.document.iter().find(|i| &i.id == id)
|
||||
}
|
||||
|
||||
pub fn get_task(&self, id: &Id) -> Option<&ApplyProjectTask> {
|
||||
self.task.iter().find(|i| &i.id == id)
|
||||
}
|
||||
|
||||
pub fn replace_document(&mut self, input: ProjectInput) {
|
||||
let id = input.id.clone();
|
||||
let index = self.document.iter().position(|i| i.id == id);
|
||||
|
@ -263,10 +267,13 @@ impl LockFileUpdate {
|
|||
let _ = package_cache_path;
|
||||
let _ = package_path;
|
||||
|
||||
// todo: freeze the sys.inputs
|
||||
|
||||
let input = ProjectInput {
|
||||
id: id.clone(),
|
||||
root: Some(root),
|
||||
main: Some(main),
|
||||
main,
|
||||
inputs: vec![],
|
||||
font_paths,
|
||||
system_fonts: true, // !args.font.ignore_system_fonts,
|
||||
package_path: None,
|
||||
|
|
|
@ -407,13 +407,13 @@ impl ResourcePath {
|
|||
}
|
||||
}
|
||||
/// Converts the resource path to an absolute file system path.
|
||||
pub fn to_abs_path(&self, rel: &Path) -> Option<PathBuf> {
|
||||
pub fn to_abs_path(&self, base: &Path) -> Option<PathBuf> {
|
||||
if self.0 == "file" {
|
||||
let path = Path::new(&self.1);
|
||||
if path.is_absolute() {
|
||||
Some(path.to_owned())
|
||||
} else {
|
||||
Some(rel.join(path))
|
||||
Some(base.join(path))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
|
@ -481,12 +481,13 @@ pub struct LockFile {
|
|||
pub struct ProjectInput {
|
||||
/// The project's ID.
|
||||
pub id: Id,
|
||||
/// The project's root directory.
|
||||
/// The path to the root directory of the project.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub root: Option<ResourcePath>,
|
||||
/// The project's main file.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub main: Option<ResourcePath>,
|
||||
/// The path to the main file of the project.
|
||||
pub main: ResourcePath,
|
||||
/// The key-value pairs visible through `sys.inputs`
|
||||
pub inputs: Vec<(String, String)>,
|
||||
/// The project's font paths.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub font_paths: Vec<ResourcePath>,
|
||||
|
|
|
@ -13,7 +13,7 @@ use std::path::Path;
|
|||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use tinymist_std::error::prelude::*;
|
||||
use tinymist_std::ImmutPath;
|
||||
use tinymist_std::{bail, ImmutPath};
|
||||
use tinymist_world::font::system::SystemFontSearcher;
|
||||
use tinymist_world::package::{http::HttpRegistry, RegistryPathMapper};
|
||||
use tinymist_world::vfs::{system::SystemAccessModel, Vfs};
|
||||
|
@ -22,6 +22,7 @@ use typst::foundations::{Dict, Str, Value};
|
|||
use typst::utils::LazyHash;
|
||||
|
||||
use crate::font::TinymistFontResolver;
|
||||
use crate::ProjectInput;
|
||||
|
||||
/// Compiler feature for LSP universe and worlds without typst.ts to implement
|
||||
/// more for tinymist. type trait of [`CompilerUniverse`].
|
||||
|
@ -114,6 +115,80 @@ impl WorldProvider for CompileOnceArgs {
|
|||
}
|
||||
}
|
||||
|
||||
// todo: merge me with the above impl
|
||||
impl WorldProvider for (ProjectInput, ImmutPath) {
|
||||
fn resolve(&self) -> Result<LspUniverse> {
|
||||
let (proj, lock_dir) = self;
|
||||
let entry = self.entry()?.try_into()?;
|
||||
let inputs = proj
|
||||
.inputs
|
||||
.iter()
|
||||
.map(|(k, v)| (Str::from(k.as_str()), Value::Str(Str::from(v.as_str()))))
|
||||
.collect();
|
||||
let fonts = LspUniverseBuilder::resolve_fonts(CompileFontArgs {
|
||||
font_paths: {
|
||||
proj.font_paths
|
||||
.iter()
|
||||
.flat_map(|p| p.to_abs_path(lock_dir))
|
||||
.collect::<Vec<_>>()
|
||||
},
|
||||
ignore_system_fonts: !proj.system_fonts,
|
||||
})?;
|
||||
let package = LspUniverseBuilder::resolve_package(
|
||||
// todo: recover certificate path
|
||||
None,
|
||||
Some(&CompilePackageArgs {
|
||||
package_path: proj
|
||||
.package_path
|
||||
.as_ref()
|
||||
.and_then(|p| p.to_abs_path(lock_dir)),
|
||||
package_cache_path: proj
|
||||
.package_cache_path
|
||||
.as_ref()
|
||||
.and_then(|p| p.to_abs_path(lock_dir)),
|
||||
}),
|
||||
);
|
||||
|
||||
LspUniverseBuilder::build(
|
||||
entry,
|
||||
Arc::new(LazyHash::new(inputs)),
|
||||
Arc::new(fonts),
|
||||
package,
|
||||
)
|
||||
.context("failed to create universe")
|
||||
}
|
||||
|
||||
fn entry(&self) -> Result<EntryOpts> {
|
||||
let (proj, lock_dir) = self;
|
||||
|
||||
let entry = proj
|
||||
.main
|
||||
.to_abs_path(lock_dir)
|
||||
.context("failed to resolve entry file")?;
|
||||
|
||||
let root = if let Some(root) = &proj.root {
|
||||
root.to_abs_path(lock_dir)
|
||||
.context("failed to resolve root")?
|
||||
} else {
|
||||
lock_dir.as_ref().to_owned()
|
||||
};
|
||||
|
||||
if !entry.starts_with(&root) {
|
||||
bail!("entry file must be in the root directory, {entry:?}, {root:?}");
|
||||
}
|
||||
|
||||
let relative_entry = match entry.strip_prefix(&root) {
|
||||
Ok(relative_entry) => relative_entry,
|
||||
Err(_) => bail!("entry path must be inside the root: {}", entry.display()),
|
||||
};
|
||||
|
||||
Ok(EntryOpts::new_rooted(
|
||||
root.clone(),
|
||||
Some(relative_entry.to_owned()),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for LSP universe.
|
||||
pub struct LspUniverseBuilder;
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ use sync_lsp::transport::MirrorArgs;
|
|||
|
||||
use tinymist::{
|
||||
project::{DocCommands, TaskCommands},
|
||||
tool::project::CompileArgs,
|
||||
CompileFontArgs, CompileOnceArgs,
|
||||
};
|
||||
use tinymist_core::LONG_VERSION;
|
||||
|
@ -32,8 +33,7 @@ pub enum Commands {
|
|||
#[cfg(feature = "preview")]
|
||||
Preview(tinymist::tool::preview::PreviewCliArgs),
|
||||
|
||||
/// Runs compile commands
|
||||
#[clap(hide(true))] // still in development
|
||||
/// Runs compile command like `typst-cli compile`
|
||||
Compile(CompileArgs),
|
||||
/// Runs language query
|
||||
#[clap(hide(true))] // still in development
|
||||
|
@ -146,17 +146,6 @@ pub struct TraceLspArgs {
|
|||
pub compile: CompileOnceArgs,
|
||||
}
|
||||
|
||||
/// Common arguments of compile, watch, and query.
|
||||
#[derive(Debug, Clone, Default, clap::Parser)]
|
||||
pub struct CompileArgs {
|
||||
#[clap(flatten)]
|
||||
pub compile: CompileOnceArgs,
|
||||
|
||||
/// Path to output file
|
||||
#[clap(value_name = "OUTPUT")]
|
||||
pub output: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, clap::Parser)]
|
||||
pub struct LspArgs {
|
||||
#[clap(flatten)]
|
||||
|
|
|
@ -16,18 +16,18 @@ use futures::future::MaybeDone;
|
|||
use lsp_server::RequestId;
|
||||
use once_cell::sync::Lazy;
|
||||
use reflexo::ImmutPath;
|
||||
use reflexo_typst::{package::PackageSpec, Compiler, TypstDict};
|
||||
use reflexo_typst::{package::PackageSpec, TypstDict};
|
||||
use serde_json::Value as JsonValue;
|
||||
use sync_lsp::{
|
||||
internal_error,
|
||||
transport::{with_stdio_transport, MirrorArgs},
|
||||
LspBuilder, LspClientRoot, LspResult,
|
||||
};
|
||||
use tinymist::world::TaskInputs;
|
||||
use tinymist::{
|
||||
tool::project::{project_main, task_main},
|
||||
tool::project::{compile_main, project_main, task_main},
|
||||
CompileConfig, Config, RegularInit, ServerState, SuperInit, UserActionTask,
|
||||
};
|
||||
use tinymist::{world::TaskInputs, world::WorldProvider};
|
||||
use tinymist_core::LONG_VERSION;
|
||||
use tinymist_project::EntryResolver;
|
||||
use tinymist_query::package::PackageInfo;
|
||||
|
@ -65,27 +65,29 @@ fn main() -> Result<()> {
|
|||
#[cfg(feature = "dhat-heap")]
|
||||
let _profiler = dhat::Profiler::new_heap();
|
||||
|
||||
// Parse command line arguments
|
||||
let args = CliArguments::parse();
|
||||
|
||||
let is_transient_cmd = matches!(args.command, Some(Commands::Compile(..)));
|
||||
|
||||
// Start logging
|
||||
let _ = {
|
||||
use log::LevelFilter::*;
|
||||
let base_level = if is_transient_cmd { Warn } else { Info };
|
||||
|
||||
env_logger::builder()
|
||||
.filter_module("tinymist", Info)
|
||||
.filter_module("tinymist", base_level)
|
||||
.filter_module("typst_preview", Debug)
|
||||
.filter_module("typlite", Info)
|
||||
.filter_module("reflexo", Info)
|
||||
.filter_module("sync_lsp", Info)
|
||||
.filter_module("reflexo_typst::service::compile", Info)
|
||||
.filter_module("reflexo_typst::service::watch", Info)
|
||||
.filter_module("typlite", base_level)
|
||||
.filter_module("reflexo", base_level)
|
||||
.filter_module("sync_lsp", base_level)
|
||||
.filter_module("reflexo_typst::diag::console", Info)
|
||||
.try_init()
|
||||
};
|
||||
|
||||
// Parse command line arguments
|
||||
let args = CliArguments::parse();
|
||||
|
||||
match args.command.unwrap_or_default() {
|
||||
Commands::Completion(args) => completion(args),
|
||||
Commands::Compile(args) => compile(args),
|
||||
Commands::Compile(args) => RUNTIMES.tokio_runtime.block_on(compile_main(args)),
|
||||
Commands::Query(query_cmds) => query_main(query_cmds),
|
||||
Commands::Lsp(args) => lsp_main(args),
|
||||
Commands::TraceLsp(args) => trace_lsp_main(args),
|
||||
|
@ -114,41 +116,6 @@ pub fn completion(args: ShellCompletionArgs) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs compilation
|
||||
pub fn compile(args: CompileArgs) -> Result<()> {
|
||||
use std::io::Write;
|
||||
|
||||
let input = args
|
||||
.compile
|
||||
.input
|
||||
.as_ref()
|
||||
.context("Missing required argument: INPUT")?;
|
||||
let output = match args.output {
|
||||
Some(stdout_path) if stdout_path == "-" => None,
|
||||
Some(output_path) => Some(PathBuf::from(output_path)),
|
||||
None => Some(Path::new(input).with_extension("pdf")),
|
||||
};
|
||||
|
||||
let universe = args.compile.resolve()?;
|
||||
let world = universe.snapshot();
|
||||
|
||||
let converter = std::marker::PhantomData.compile(&world, &mut Default::default());
|
||||
let pdf = typst_pdf::pdf(&converter.unwrap().output, &Default::default());
|
||||
|
||||
match (pdf, output) {
|
||||
(Ok(pdf), None) => {
|
||||
std::io::stdout().write_all(&pdf).unwrap();
|
||||
}
|
||||
(Ok(pdf), Some(output)) => std::fs::write(output, pdf).unwrap(),
|
||||
(Err(err), ..) => {
|
||||
eprintln!("{err:?}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The main entry point for the language server.
|
||||
pub fn lsp_main(args: LspArgs) -> Result<()> {
|
||||
let pairs = LONG_VERSION.trim().split('\n');
|
||||
|
|
|
@ -263,8 +263,8 @@ impl ServerState {
|
|||
.unwrap_or_else(|| lock_dir.clone());
|
||||
let main = input
|
||||
.main
|
||||
.as_ref()
|
||||
.and_then(|main| Some(main.to_abs_path(lock_dir)?.as_path().into()))
|
||||
.to_abs_path(lock_dir)
|
||||
.map(|path| path.as_path().into())
|
||||
.unwrap_or_else(|| path.clone());
|
||||
let entry = self
|
||||
.entry_resolver()
|
||||
|
|
|
@ -1,76 +1,34 @@
|
|||
//! Project management tools.
|
||||
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use reflexo::ImmutPath;
|
||||
use tinymist_std::error::prelude::*;
|
||||
|
||||
use crate::project::*;
|
||||
use crate::{project::*, task::ExportTask};
|
||||
|
||||
/// Common arguments of compile, watch, and query.
|
||||
#[derive(Debug, Clone, clap::Parser)]
|
||||
pub struct CompileArgs {
|
||||
/// Inherits the compile task arguments.
|
||||
#[clap(flatten)]
|
||||
pub compile: TaskCompileArgs,
|
||||
|
||||
/// Saves the compilation arguments to the lock file.
|
||||
#[clap(long)]
|
||||
pub save_lock: bool,
|
||||
|
||||
/// Specifies the path to the lock file. If the path is
|
||||
/// set, the lock file will be saved.
|
||||
#[clap(long)]
|
||||
pub lockfile: Option<PathBuf>,
|
||||
}
|
||||
|
||||
trait LockFileExt {
|
||||
fn declare(&mut self, args: &DocNewArgs) -> Id;
|
||||
fn preview(&mut self, doc_id: Id, args: &TaskPreviewArgs) -> Result<Id>;
|
||||
fn compile(&mut self, args: TaskCompileArgs) -> Result<Id>;
|
||||
fn export(&mut self, doc_id: Id, args: TaskCompileArgs) -> Result<Id>;
|
||||
}
|
||||
|
||||
impl LockFileExt for LockFile {
|
||||
fn declare(&mut self, args: &DocNewArgs) -> Id {
|
||||
let id: Id = (&args.id).into();
|
||||
|
||||
let root = args
|
||||
.root
|
||||
.as_ref()
|
||||
.map(|root| ResourcePath::from_user_sys(Path::new(root)));
|
||||
let main = ResourcePath::from_user_sys(Path::new(&args.id.input));
|
||||
|
||||
let font_paths = args
|
||||
.font
|
||||
.font_paths
|
||||
.iter()
|
||||
.map(|p| ResourcePath::from_user_sys(p))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let package_path = args
|
||||
.package
|
||||
.package_path
|
||||
.as_ref()
|
||||
.map(|p| ResourcePath::from_user_sys(p));
|
||||
|
||||
let package_cache_path = args
|
||||
.package
|
||||
.package_cache_path
|
||||
.as_ref()
|
||||
.map(|p| ResourcePath::from_user_sys(p));
|
||||
|
||||
let input = ProjectInput {
|
||||
id: id.clone(),
|
||||
root,
|
||||
main: Some(main),
|
||||
font_paths,
|
||||
system_fonts: !args.font.ignore_system_fonts,
|
||||
package_path,
|
||||
package_cache_path,
|
||||
};
|
||||
|
||||
self.replace_document(input);
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
fn compile(&mut self, args: TaskCompileArgs) -> Result<Id> {
|
||||
let id = self.declare(&args.declare);
|
||||
self.export(id, args)
|
||||
}
|
||||
|
||||
fn export(&mut self, doc_id: Id, args: TaskCompileArgs) -> Result<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) -> Result<Id> {
|
||||
let task_id = args
|
||||
.name
|
||||
|
@ -92,12 +50,51 @@ impl LockFileExt for LockFile {
|
|||
}
|
||||
}
|
||||
|
||||
/// Runs project compilation(s)
|
||||
pub async fn compile_main(args: CompileArgs) -> Result<()> {
|
||||
// Identifies the input and output
|
||||
let input = args.compile.declare.to_input();
|
||||
let output = args.compile.to_task(input.id.clone())?;
|
||||
|
||||
// Saves the lock file if the flags are set
|
||||
let save_lock = args.save_lock || args.lockfile.is_some();
|
||||
// todo: respect the name of the lock file
|
||||
let lock_dir: ImmutPath = if let Some(lockfile) = args.lockfile {
|
||||
lockfile.parent().context("no parent")?.into()
|
||||
} else {
|
||||
std::env::current_dir().context("lock directory")?.into()
|
||||
};
|
||||
|
||||
if save_lock {
|
||||
LockFile::update(&lock_dir, |state| {
|
||||
state.replace_document(input.clone());
|
||||
state.replace_task(output.clone());
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
|
||||
// Prepares for the compilation
|
||||
let universe = (input, lock_dir.clone()).resolve()?;
|
||||
let world = universe.snapshot();
|
||||
let snap = CompileSnapshot::from_world(world);
|
||||
|
||||
// Compiles the project
|
||||
let compiled = snap.compile();
|
||||
|
||||
// Exports the compiled project
|
||||
let lock_dir = save_lock.then_some(lock_dir);
|
||||
ExportTask::do_export(output.task, compiled, lock_dir).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Project document commands' main
|
||||
pub fn project_main(args: DocCommands) -> Result<()> {
|
||||
LockFile::update(Path::new("."), |state| {
|
||||
match args {
|
||||
DocCommands::New(args) => {
|
||||
state.declare(&args);
|
||||
state.replace_document(args.to_input());
|
||||
}
|
||||
DocCommands::Configure(args) => {
|
||||
let id: Id = (&args.id).into();
|
||||
|
@ -117,11 +114,10 @@ pub fn project_main(args: DocCommands) -> Result<()> {
|
|||
pub fn task_main(args: TaskCommands) -> Result<()> {
|
||||
LockFile::update(Path::new("."), |state| {
|
||||
match args {
|
||||
TaskCommands::Compile(args) => {
|
||||
let _ = state.compile(args);
|
||||
}
|
||||
TaskCommands::Preview(args) => {
|
||||
let id = state.declare(&args.declare);
|
||||
let input = args.declare.to_input();
|
||||
let id = input.id.clone();
|
||||
state.replace_document(input);
|
||||
let _ = state.preview(id, &args);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,16 @@ version = "0.1.0-beta0"
|
|||
|
||||
[[document]]
|
||||
id = "file:docs/tinymist/book.typ"
|
||||
inputs = []
|
||||
main = "file:docs/tinymist/book.typ"
|
||||
root = "file:."
|
||||
system-fonts = true
|
||||
|
||||
[[document]]
|
||||
id = "file:docs/tinymist/ebook.typ"
|
||||
inputs = []
|
||||
main = "file:docs/tinymist/ebook.typ"
|
||||
root = "file:."
|
||||
system-fonts = true
|
||||
|
||||
[[task]]
|
||||
|
@ -23,7 +29,9 @@ type = "export-svg"
|
|||
when = "never"
|
||||
|
||||
[[task.transform]]
|
||||
pages = ["1-1"]
|
||||
|
||||
[task.transform.pages]
|
||||
ranges = ["1-1"]
|
||||
|
||||
[[task]]
|
||||
document = "file:docs/tinymist/ebook.typ"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue