mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 11:17:14 -04:00
fix(cli): accept a UTF-8 BOM in scripts and document the syntax in script --help (fixes #654)
This commit is contained in:
1 parent
64f0ca8fa6
commit
9e15d5a17e
5 files changed
+73
-4
No files matched your search
@@ -106,6 +106,7 @@ cargo run --release -p designcraft -- --sample # open the sample mag
|
||||
cargo run --release -p designcraft -- --sample --control 7979 # + JSON control channel
|
||||
cargo run --release -p designcraft-cli -- run --sample --all-pages out/ # headless: render every page to PNG
|
||||
cargo run --release -p designcraft-cli -- commands # list every command
|
||||
cargo run --release -p designcraft-cli -- script --help # command script syntax (docs/agents.md)
|
||||
cargo xtask ci # fmt, clippy, tests, assets, layering, wasm
|
||||
```
|
||||
|
||||
|
||||
@@ -38,6 +38,37 @@ use serde_json::{Value, json};
|
||||
/// The usage block. Shared by `--help` (stdout, success) and an unknown command (stderr, failure).
|
||||
const USAGE: &str = "usage: designcraft-cli run [--in FILE | --sample] [--cmd ID[=JSON]]... [--page N] [--scale S] [--pdf-options JSON] [--export OUT] [--all-pages DIR]\n (--page, --scale and --pdf-options apply to the exports that follow them)\n designcraft-cli commands [FILTER]\n designcraft-cli describe COMMAND\n designcraft-cli script [FILE|-] [--in FILE | --sample] [--connect PORT] [--save OUT] [--export OUT] [--keep-going]\n designcraft-cli app [--port PORT] COMMAND [JSON] | --method METHOD [JSON]\n designcraft-cli mcp [--connect PORT] [--sample]\n designcraft-cli perf [--pages N] [--runs N] [--strict]\n designcraft-cli bench FILE [--runs N]\n designcraft-cli links\n designcraft-cli --version";
|
||||
|
||||
/// `script --help`: the script syntax, otherwise only in `describe script.run` and docs/agents.md.
|
||||
const SCRIPT_HELP: &str = r#"usage: designcraft-cli script [FILE|-] [--in FILE | --sample] [--connect PORT] [--save OUT] [--export OUT] [--keep-going]
|
||||
|
||||
Runs a command script from FILE (stdin when FILE is `-` or missing), headless or, with
|
||||
--connect PORT, in the running app (designcraft --control PORT).
|
||||
|
||||
Script text, one step per line:
|
||||
command.id {"param": 1} also `command.id={json}`, or `command.id` alone
|
||||
{"command": "command.id", "params": {...}} JSON lines, or a JSON array of them
|
||||
# comment, // comment skipped, like blank lines
|
||||
UTF-8 with or without a byte order mark; LF or CRLF line endings.
|
||||
|
||||
References to earlier results (in string parameters):
|
||||
"$N.path" the value at `path` in the result of step N. N is the 0-based step index:
|
||||
comments and blank lines are not steps. `$last` is the previous step.
|
||||
Paths go through result fields and array indices: "$1.story", "$3.ids.0".
|
||||
"${N.path}" inside a longer string, replaced by the value's text: "Frame ${0.id}".
|
||||
|
||||
Text parameters: "\n" starts a new paragraph, "\u2028" is a forced line break, "\t" a tab.
|
||||
Most commands ignore parameters they do not know instead of rejecting them: check names
|
||||
with `designcraft-cli describe COMMAND`.
|
||||
|
||||
Options:
|
||||
--in FILE / --sample open a document (.designcraft or .idml) or the sample first
|
||||
--save OUT save as .designcraft after the script succeeds
|
||||
--export OUT export after the script succeeds (.pdf, .idml, .epub, .png, .jpg); repeatable
|
||||
--keep-going run every step, recording failures as {"error": ...} results
|
||||
|
||||
Prints {"completed": n, "results": [...]}, plus failedIndex / failedCommand / error and a
|
||||
non-zero exit when a step fails."#;
|
||||
|
||||
/// The links line under the usage block.
|
||||
fn usage_footer() -> String {
|
||||
format!(
|
||||
@@ -335,6 +366,10 @@ fn script(args: &[String]) -> Result<(), String> {
|
||||
"--save" => save = Some(val()?),
|
||||
"--export" => export.push(val()?),
|
||||
"--keep-going" => keep_going = true,
|
||||
"--help" | "-h" => {
|
||||
outln!("{SCRIPT_HELP}");
|
||||
return Ok(());
|
||||
}
|
||||
f if !f.starts_with("--") && file.is_none() => file = Some(f.to_string()),
|
||||
other => return Err(format!("unknown script option `{other}`")),
|
||||
}
|
||||
|
||||
@@ -87,6 +87,21 @@ fn help_goes_to_stdout_and_unknown_args_to_stderr() {
|
||||
assert!(String::from_utf8_lossy(&out.stderr).starts_with("usage: designcraft-cli"));
|
||||
}
|
||||
|
||||
/// `script --help` was "unknown script option `--help`"; the line syntax was only in
|
||||
/// `describe script.run` (#654).
|
||||
#[test]
|
||||
fn script_help_documents_the_syntax() {
|
||||
for flag in ["--help", "-h"] {
|
||||
let out = cli().args(["script", flag]).output().unwrap();
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(out.status.success(), "{flag}: {:?} {}", out.status, String::from_utf8_lossy(&out.stderr));
|
||||
assert!(stdout.starts_with("usage: designcraft-cli script"), "{flag}: {stdout:?}");
|
||||
for needle in ["0-based", "$1.story", "$3.ids.0", "\\u2028", "ignore"] {
|
||||
assert!(stdout.contains(needle), "{flag}: no {needle:?} in {stdout:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `designcraft-cli commands | head -1` panicked with "failed printing to
|
||||
/// stdout: Broken pipe (os error 32)" and exit status 101.
|
||||
#[test]
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
//!
|
||||
//! **Script text** ([`parse`]): a JSON array of steps, JSON lines (`{"command": …, "params": …}`),
|
||||
//! or compact lines `command.id {json}` / `command.id={json}` / `command.id`; blank lines and
|
||||
//! lines starting with `#` or `//` are ignored.
|
||||
//! lines starting with `#` or `//` are ignored. A leading byte order mark and CRLF line endings are
|
||||
//! accepted.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -22,7 +23,9 @@ pub struct Step {
|
||||
|
||||
/// Parse script text (see the module docs).
|
||||
pub fn parse(text: &str) -> Result<Vec<Step>, String> {
|
||||
let t = text.trim();
|
||||
// Windows tools save "UTF-8 with BOM"; `trim` keeps U+FEFF, which then became part of the
|
||||
// first command id.
|
||||
let t = text.trim_start_matches('\u{feff}').trim();
|
||||
if t.starts_with('[') {
|
||||
let v: Value = serde_json::from_str(t).map_err(|e| format!("script: {e}"))?;
|
||||
return v
|
||||
@@ -174,6 +177,17 @@ mod tests {
|
||||
assert!(parse("frame.create {bad").unwrap_err().contains("line 1"));
|
||||
}
|
||||
|
||||
/// Windows editors and PowerShell 5.1 save "UTF-8 with BOM" with CRLF line endings; the BOM
|
||||
/// became part of the first command id (#654).
|
||||
#[test]
|
||||
fn a_leading_bom_and_crlf_parse_like_plain_text() {
|
||||
let plain = "file.new {\"width\": 100}\nframe.create {\"rect\": [0, 0, 10, 10]}\n";
|
||||
let windows = format!("\u{feff}{}", plain.replace('\n', "\r\n"));
|
||||
assert_eq!(parse(&windows).unwrap(), parse(plain).unwrap());
|
||||
let array = r#"[{"command": "file.new"}]"#;
|
||||
assert_eq!(parse(&format!("\u{feff}{array}")).unwrap(), parse(array).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_references() {
|
||||
let results = vec![json!({"id": 8, "story": 9, "ids": [4, 5]}), json!({"name": "A"})];
|
||||
|
||||
+6
-2
@@ -21,8 +21,12 @@ Parameters are documented on each command (`params`), e.g. `{anchor? | paragraph
|
||||
## Scripts: several commands, later ones using earlier results
|
||||
|
||||
A script is one command per line (`command.id {json params}`; `#` comments), JSON lines, or a JSON array of
|
||||
`{command, params}`. A parameter string `"$N.path"` becomes that value of step N's result (0-based, `$last` for the
|
||||
previous step); `"${N.path}"` interpolates inside longer strings.
|
||||
`{command, params}`, in UTF-8 with or without a byte order mark. A parameter string `"$N.path"` becomes that value
|
||||
of step N's result: N is the 0-based step index (comments and blank lines are not steps, `$last` is the previous
|
||||
step) and the path goes through result fields and array indices (`"$1.story"`, `"$3.ids.0"`); `"${N.path}"`
|
||||
interpolates inside longer strings. In text parameters `\n` starts a new paragraph and `\u2028` is a forced line
|
||||
break. Most commands ignore parameters they don't know instead of rejecting them, so check names with `describe`.
|
||||
`designcraft-cli script --help` prints this reference.
|
||||
|
||||
```sh
|
||||
cat > page.dcs <<'DCS'
|
||||
|
||||
Reference in new issue
Block a user