mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-03 17:58:17 +00:00
feat: support vscode tasks for exporting query and pdfpc (#490)
* feat: support vscode tasks for exporting query and pdfpc * test: update snapshot
This commit is contained in:
parent
56e20b2590
commit
85c459d4c8
14 changed files with 354 additions and 10 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3768,6 +3768,7 @@ dependencies = [
|
|||
"reflexo",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"sync-lsp",
|
||||
"tinymist-assets",
|
||||
"tinymist-query",
|
||||
|
|
|
@ -153,6 +153,15 @@ mod polymorphic {
|
|||
Html {},
|
||||
Markdown {},
|
||||
Text {},
|
||||
Query {
|
||||
format: String,
|
||||
output_extension: Option<String>,
|
||||
strict: bool,
|
||||
selector: String,
|
||||
field: Option<String>,
|
||||
one: bool,
|
||||
pretty: bool,
|
||||
},
|
||||
Svg {
|
||||
page: PageSelection,
|
||||
},
|
||||
|
@ -180,6 +189,11 @@ mod polymorphic {
|
|||
Self::Text { .. } => "txt",
|
||||
Self::Svg { .. } => "svg",
|
||||
Self::Png { .. } => "png",
|
||||
Self::Query {
|
||||
format,
|
||||
output_extension,
|
||||
..
|
||||
} => output_extension.as_deref().unwrap_or(format),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ env_logger.workspace = true
|
|||
log.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paste.workspace = true
|
||||
|
||||
|
|
|
@ -26,6 +26,18 @@ struct ExportOpts {
|
|||
page: PageSelection,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct QueryOpts {
|
||||
format: String,
|
||||
output_extension: Option<String>,
|
||||
strict: Option<bool>,
|
||||
pretty: Option<bool>,
|
||||
selector: String,
|
||||
field: Option<String>,
|
||||
one: Option<bool>,
|
||||
}
|
||||
|
||||
/// Here are implemented the handlers for each command.
|
||||
impl LanguageState {
|
||||
/// Export the current document as PDF file(s).
|
||||
|
@ -59,6 +71,24 @@ impl LanguageState {
|
|||
self.export(req_id, ExportKind::Text {}, args)
|
||||
}
|
||||
|
||||
/// Query the current document and export the result as JSON file(s).
|
||||
pub fn export_query(&mut self, req_id: RequestId, mut args: Vec<JsonValue>) -> ScheduledResult {
|
||||
let opts = get_arg_or_default!(args[1] as QueryOpts);
|
||||
self.export(
|
||||
req_id,
|
||||
ExportKind::Query {
|
||||
format: opts.format,
|
||||
output_extension: opts.output_extension,
|
||||
strict: opts.strict.unwrap_or(true),
|
||||
selector: opts.selector,
|
||||
field: opts.field,
|
||||
pretty: opts.pretty.unwrap_or(true),
|
||||
one: opts.one.unwrap_or(false),
|
||||
},
|
||||
args,
|
||||
)
|
||||
}
|
||||
|
||||
/// Export the current document as Svg file(s).
|
||||
pub fn export_svg(&mut self, req_id: RequestId, mut args: Vec<JsonValue>) -> ScheduledResult {
|
||||
let opts = get_arg_or_default!(args[1] as ExportOpts);
|
||||
|
|
|
@ -257,6 +257,7 @@ impl LanguageState {
|
|||
.with_command_("tinymist.exportText", State::export_text)
|
||||
.with_command_("tinymist.exportHtml", State::export_html)
|
||||
.with_command_("tinymist.exportMarkdown", State::export_markdown)
|
||||
.with_command_("tinymist.exportQuery", State::export_query)
|
||||
.with_command("tinymist.doClearCache", State::clear_cache)
|
||||
.with_command("tinymist.pinMain", State::pin_document)
|
||||
.with_command("tinymist.focusMain", State::focus_document)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! The actor that handles various document export, like PDF and SVG export.
|
||||
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
|
@ -8,6 +9,7 @@ use once_cell::sync::Lazy;
|
|||
use tinymist_query::{ExportKind, PageSelection};
|
||||
use tokio::sync::mpsc;
|
||||
use typlite::Typlite;
|
||||
use typst::foundations::IntoValue;
|
||||
use typst::{
|
||||
foundations::Smart,
|
||||
layout::{Abs, Frame},
|
||||
|
@ -213,6 +215,39 @@ impl ExportConfig {
|
|||
// todo: timestamp world.now()
|
||||
typst_pdf::pdf(doc, Smart::Auto, timestamp)
|
||||
}
|
||||
Query {
|
||||
format,
|
||||
output_extension: _,
|
||||
strict,
|
||||
selector,
|
||||
field,
|
||||
one,
|
||||
pretty,
|
||||
} => {
|
||||
let elements =
|
||||
typst_ts_compiler::query::retrieve(artifact.world.deref(), &selector, doc)
|
||||
.map_err(|e| anyhow::anyhow!("failed to retrieve: {e}"))?;
|
||||
if one && elements.len() != 1 {
|
||||
bail!("expected exactly one element, found {}", elements.len());
|
||||
}
|
||||
|
||||
let mapped: Vec<_> = elements
|
||||
.into_iter()
|
||||
.filter_map(|c| match &field {
|
||||
Some(field) => c.get_by_name(field),
|
||||
_ => Some(c.into_value()),
|
||||
})
|
||||
.collect();
|
||||
|
||||
if one {
|
||||
let Some(value) = mapped.first() else {
|
||||
bail!("no such field found for element");
|
||||
};
|
||||
serialize(value, &format, strict, pretty).map(String::into_bytes)?
|
||||
} else {
|
||||
serialize(&mapped, &format, strict, pretty).map(String::into_bytes)?
|
||||
}
|
||||
}
|
||||
Html {} => typst_ts_svg_exporter::render_svg_html(doc).into_bytes(),
|
||||
Text {} => format!("{}", FullTextDigest(doc.clone())).into_bytes(),
|
||||
Markdown {} => {
|
||||
|
@ -355,6 +390,39 @@ fn convert_datetime(date_time: chrono::DateTime<chrono::Utc>) -> Option<TypstDat
|
|||
)
|
||||
}
|
||||
|
||||
/// Serialize data to the output format.
|
||||
fn serialize(
|
||||
data: &impl serde::Serialize,
|
||||
format: &str,
|
||||
strict: bool,
|
||||
pretty: bool,
|
||||
) -> anyhow::Result<String> {
|
||||
Ok(match format {
|
||||
"json" if pretty => serde_json::to_string_pretty(data)?,
|
||||
"json" => serde_json::to_string(data)?,
|
||||
"yaml" => serde_yaml::to_string(&data)?,
|
||||
format if format == "txt" || !strict => {
|
||||
use serde_json::Value::*;
|
||||
let value = serde_json::to_value(data)?;
|
||||
match value {
|
||||
String(s) => s,
|
||||
_ => {
|
||||
let kind = match value {
|
||||
Null => "null",
|
||||
Bool(_) => "boolean",
|
||||
Number(_) => "number",
|
||||
String(_) => "string",
|
||||
Array(_) => "array",
|
||||
Object(_) => "object",
|
||||
};
|
||||
bail!("expected a string value for format: {format}, got {kind}")
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => bail!("unsupported format for query: {format}"),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -4,6 +4,84 @@
|
|||
|
||||
You can export your documents to various formats using the `export` feature.
|
||||
|
||||
== Export from Query Result
|
||||
|
||||
=== Hello World Example (VSCode Tasks)
|
||||
|
||||
You can export the result of a query as text using the `export` command.
|
||||
|
||||
Given a code:
|
||||
|
||||
```typ
|
||||
#println("Hello World!")
|
||||
#println("Hello World! Again...")
|
||||
```
|
||||
|
||||
LSP should export the result of the query as text with the following content:
|
||||
|
||||
```txt
|
||||
Hello World!
|
||||
Hello World! Again...
|
||||
```
|
||||
|
||||
This requires the following configuration in your `tasks.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Query as Text",
|
||||
"type": "typst",
|
||||
"command": "export",
|
||||
"export": {
|
||||
"format": "query",
|
||||
"query.format": "txt",
|
||||
"query.outputExtension": "out",
|
||||
"query.field": "value",
|
||||
"query.selector": "<print-effect>",
|
||||
"query.one": true
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
See the #link("https://github.com/Myriad-Dreamin/tinymist/tree/main/editors/vscode/e2e-workspaces/print-state")[Sample Workspace: print-state] for more details.
|
||||
|
||||
=== Pdfpc Example (VSCode Tasks)
|
||||
|
||||
A more practical example is exporting the result of a query as a pdfpc file. You can use the following configuration in your `tasks.json` file to export the result of a query as a pdfpc file, which is adapted by #link("https://touying-typ.github.io/touying/")[Touying Slides].
|
||||
|
||||
```json
|
||||
{
|
||||
"label": "Query as Pdfpc",
|
||||
"type": "typst",
|
||||
"command": "export",
|
||||
"export": {
|
||||
"format": "query",
|
||||
"query.format": "json",
|
||||
"query.outputExtension": "pdfpc",
|
||||
"query.selector": "<pdfpc-file>",
|
||||
"query.field": "value",
|
||||
"query.one": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To simplify configuration,
|
||||
|
||||
```json
|
||||
{
|
||||
"label": "Query as Pdfpc",
|
||||
"type": "typst",
|
||||
"command": "export",
|
||||
"export": {
|
||||
"format": "pdfpc"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
== VSCode: Task Configuration
|
||||
|
||||
You can configure tasks in your `tasks.json` file to "persist" the arguments for exporting documents.
|
||||
|
@ -58,6 +136,14 @@ Example:
|
|||
"merged": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Query as Pdfpc",
|
||||
"type": "typst",
|
||||
"command": "export",
|
||||
"export": {
|
||||
"format": "pdfpc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Export as PNG and SVG",
|
||||
"type": "typst",
|
||||
|
|
29
editors/vscode/e2e-workspaces/print-state/.vscode/tasks.json
vendored
Normal file
29
editors/vscode/e2e-workspaces/print-state/.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Query as Text",
|
||||
"type": "typst",
|
||||
"command": "export",
|
||||
"export": {
|
||||
"format": "query",
|
||||
"query.format": "txt",
|
||||
"query.outputExtension": "out",
|
||||
"query.field": "value",
|
||||
"query.selector": "<print-effect>",
|
||||
"query.one": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Export PNG and SVG....",
|
||||
"type": "typst",
|
||||
"command": "export",
|
||||
"export": {
|
||||
"format": ["png", "svg"],
|
||||
"png.ppi": 288,
|
||||
"merged": true,
|
||||
"merged.gap": "100pt"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
12
editors/vscode/e2e-workspaces/print-state/effect.typ
Normal file
12
editors/vscode/e2e-workspaces/print-state/effect.typ
Normal file
|
@ -0,0 +1,12 @@
|
|||
|
||||
#let print-state = state("print-effect", ())
|
||||
#let print(k, end: none) = print-state.update(it => it + (k, end))
|
||||
#let println = print.with(end: "\n")
|
||||
|
||||
#let main = content => {
|
||||
context [
|
||||
#let prints = print-state.final()
|
||||
#metadata(prints.join()) <print-effect>
|
||||
]
|
||||
content
|
||||
}
|
7
editors/vscode/e2e-workspaces/print-state/main.typ
Normal file
7
editors/vscode/e2e-workspaces/print-state/main.typ
Normal file
|
@ -0,0 +1,7 @@
|
|||
|
||||
#import "effect.typ": *
|
||||
|
||||
#show: main
|
||||
|
||||
#println("Hello World!")
|
||||
#println("Hello World! Again...")
|
|
@ -90,12 +90,22 @@
|
|||
"enum": [
|
||||
"pdf",
|
||||
"png",
|
||||
"svg"
|
||||
"svg",
|
||||
"html",
|
||||
"markdown",
|
||||
"text",
|
||||
"query",
|
||||
"pdfpc"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"PDF",
|
||||
"PNG",
|
||||
"SVG"
|
||||
"SVG",
|
||||
"HTML",
|
||||
"Markdown",
|
||||
"Plain Text",
|
||||
"Query Result",
|
||||
"Pdfpc (From Query)"
|
||||
],
|
||||
"default": "pdf"
|
||||
},
|
||||
|
@ -111,7 +121,9 @@
|
|||
"svg",
|
||||
"html",
|
||||
"markdown",
|
||||
"text"
|
||||
"text",
|
||||
"query",
|
||||
"pdfpc"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"PDF",
|
||||
|
@ -119,7 +131,9 @@
|
|||
"SVG",
|
||||
"HTML",
|
||||
"Markdown",
|
||||
"Plain Text"
|
||||
"Plain Text",
|
||||
"Query Result",
|
||||
"Pdfpc (From Query)"
|
||||
],
|
||||
"default": "pdf"
|
||||
}
|
||||
|
@ -192,6 +206,45 @@
|
|||
"type": "string",
|
||||
"description": "The gap between the pages when merging **with absolute typst unit**. Affected formats: `png`",
|
||||
"default": "0pt"
|
||||
},
|
||||
"query.format": {
|
||||
"type": "string",
|
||||
"description": "The format of the query output. Defaults to `json`.",
|
||||
"default": "json",
|
||||
"enum": [
|
||||
"json",
|
||||
"yaml",
|
||||
"txt"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"JSON",
|
||||
"YAML",
|
||||
"Plain Text if the result is a string, otherwise raises an error. You may specific the field to use for the query with `query.field` and assert that there is only one result with `query.one`."
|
||||
]
|
||||
},
|
||||
"query.outputExtension": {
|
||||
"type": "string",
|
||||
"description": "The extension of the query output. Inferring from `query.format` if not specified."
|
||||
},
|
||||
"query.strict": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to strictly check the query format. Defaults to `true`."
|
||||
},
|
||||
"query.pretty": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to pretty print the query output. Defaults to `true`."
|
||||
},
|
||||
"query.selector": {
|
||||
"type": "string",
|
||||
"description": "The selector to use for the query. Must specified if `format`."
|
||||
},
|
||||
"query.field": {
|
||||
"type": "string",
|
||||
"description": "The field to use for the query."
|
||||
},
|
||||
"query.one": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to only return one result. Defaults to `false`."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,9 @@ export const tinymist = {
|
|||
exportText(uri: string, extraOpts?: any) {
|
||||
return doExport("tinymist.exportText", uri, extraOpts);
|
||||
},
|
||||
exportQuery(uri: string, extraOpts?: any) {
|
||||
return doExport("tinymist.exportQuery", uri, extraOpts);
|
||||
},
|
||||
};
|
||||
|
||||
function doExport(command: string, uri: string, extraOpts?: any): Promise<string> {
|
||||
|
|
|
@ -13,7 +13,7 @@ export function activateTaskProvider(context: vscode.ExtensionContext): vscode.D
|
|||
return vscode.tasks.registerTaskProvider(TYPST_TASK_TYPE, provider);
|
||||
}
|
||||
|
||||
export type ExportFormat = "pdf" | "png" | "svg";
|
||||
export type ExportFormat = "pdf" | "png" | "svg" | "html" | "markdown" | "text" | "query" | "pdfpc";
|
||||
|
||||
export type TaskDefinition = vscode.TaskDefinition & {
|
||||
readonly type: typeof TYPST_TASK_TYPE;
|
||||
|
@ -32,6 +32,13 @@ export type TaskDefinition = vscode.TaskDefinition & {
|
|||
"merged.gap"?: string;
|
||||
"png.merged.gap"?: string;
|
||||
"svg.merged.gap"?: string;
|
||||
"query.format"?: string;
|
||||
"query.outputExtension"?: string;
|
||||
"query.strict"?: boolean;
|
||||
"query.pretty"?: boolean;
|
||||
"query.selector"?: string;
|
||||
"query.field"?: string;
|
||||
"query.one"?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -121,6 +128,20 @@ export async function callTypstExportCommand(): Promise<vscode.CustomExecution>
|
|||
},
|
||||
export: tinymist.exportText,
|
||||
},
|
||||
query: {
|
||||
opts() {
|
||||
return {
|
||||
format: exportArgs["query.format"],
|
||||
outputExtension: exportArgs["query.outputExtension"],
|
||||
strict: exportArgs["query.strict"],
|
||||
pretty: exportArgs["query.pretty"],
|
||||
selector: exportArgs["query.selector"],
|
||||
field: exportArgs["query.field"],
|
||||
one: exportArgs["query.one"],
|
||||
};
|
||||
},
|
||||
export: tinymist.exportQuery,
|
||||
},
|
||||
};
|
||||
|
||||
return Promise.resolve({
|
||||
|
@ -131,8 +152,17 @@ export async function callTypstExportCommand(): Promise<vscode.CustomExecution>
|
|||
|
||||
try {
|
||||
await run();
|
||||
} catch (e) {
|
||||
writeEmitter.fire("Typst export task failed: " + obj(e) + "\r\n");
|
||||
} catch (e: any) {
|
||||
writeEmitter.fire(
|
||||
"Typst export task failed: " +
|
||||
obj({
|
||||
code: e.code,
|
||||
message: e.message,
|
||||
stack: e.stack,
|
||||
error: e,
|
||||
}) +
|
||||
"\r\n",
|
||||
);
|
||||
} finally {
|
||||
closeEmitter.fire(0);
|
||||
}
|
||||
|
@ -152,7 +182,16 @@ export async function callTypstExportCommand(): Promise<vscode.CustomExecution>
|
|||
return;
|
||||
}
|
||||
|
||||
for (const format of formats) {
|
||||
for (let format of formats) {
|
||||
if (format === "pdfpc") {
|
||||
format = "query";
|
||||
exportArgs["query.format"] = "json";
|
||||
exportArgs["query.outputExtension"] = "pdfpc";
|
||||
exportArgs["query.selector"] = "<pdfpc-file>";
|
||||
exportArgs["query.field"] = "value";
|
||||
exportArgs["query.one"] = true;
|
||||
}
|
||||
|
||||
const provider = formatProvider[format];
|
||||
if (!provider) {
|
||||
writeEmitter.fire("Unsupported export format: " + format + "\r\n");
|
||||
|
|
|
@ -374,7 +374,7 @@ fn e2e() {
|
|||
});
|
||||
|
||||
let hash = replay_log(&tinymist_binary, &root.join("neovim"));
|
||||
insta::assert_snapshot!(hash, @"siphash128_13:a54cfbafc7174bc8465fc0adb99aa28d");
|
||||
insta::assert_snapshot!(hash, @"siphash128_13:466ece1c48d23e59d3ff339a4c8ff461");
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -385,7 +385,7 @@ fn e2e() {
|
|||
});
|
||||
|
||||
let hash = replay_log(&tinymist_binary, &root.join("vscode"));
|
||||
insta::assert_snapshot!(hash, @"siphash128_13:5c9f14019d55dc472177fff5f215fa40");
|
||||
insta::assert_snapshot!(hash, @"siphash128_13:5d7fb66abe6c73f204e5deb44ae6a343");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue