Merge pull request #1608 from dotCooCoo/feat/script-checker

Scripts: Check Script… reports what a .jsx script needs and does, without running it
This commit is contained in:
BFlat authored and GitHub committed 2026-10-13 14:03:59 +07:00
commit 24afe0f517
46 files changed
+7845 -19

No files matched your search

Generated
+8
View File
@@ -4654,6 +4654,7 @@ dependencies = [
"vectorcraft-pdf",
"vectorcraft-plugins",
"vectorcraft-render",
"vectorcraft-scriptcheck",
"vectorcraft-svg",
"vectorcraft-testkit",
"vectorcraft-text",
@@ -4823,6 +4824,13 @@ dependencies = [
"weezl",
]
[[package]]
name = "vectorcraft-scriptcheck"
version = "0.10.0"
dependencies = [
"proptest",
]
[[package]]
name = "vectorcraft-svg"
version = "0.10.0"
+1
View File
@@ -37,6 +37,7 @@ vectorcraft-eps = { path = "crates/eps" }
vectorcraft-metafile = { path = "crates/metafile" }
vectorcraft-plugins = { path = "crates/plugins" }
vectorcraft-affinity = { path = "crates/affinity" }
vectorcraft-scriptcheck = { path = "crates/scriptcheck" }
# foundation
kurbo = { version = "0.13", features = ["serde"] }
+1
View File
@@ -26,6 +26,7 @@ vectorcraft-eps = { workspace = true }
vectorcraft-metafile = { workspace = true }
vectorcraft-plugins = { workspace = true }
vectorcraft-affinity = { workspace = true }
vectorcraft-scriptcheck = { workspace = true }
image = { workspace = true }
flate2 = { workspace = true }
kurbo = { workspace = true }
+21 -1
View File
@@ -674,7 +674,7 @@ fn format_filters() -> impl Iterator<Item = (&'static str, &'static [&'static st
/// Open-dialog filters: "All readable files" first, then one per readable format, then swatch
/// libraries (which open in the library panel), Libraries panel libraries, flattener, PDF and print
/// presets (imported).
/// presets (imported), scripts (checked without running them) and plug-ins (installed).
pub fn open_filters() -> impl Iterator<Item = (&'static str, &'static [&'static str])> {
std::iter::once(("All readable files", OPEN_EXTS))
.chain(format_filters())
@@ -683,6 +683,7 @@ pub fn open_filters() -> impl Iterator<Item = (&'static str, &'static [&'static
.chain(std::iter::once(("Flattener presets", super::flatten::PRESET_EXTS)))
.chain(std::iter::once(("PDF presets", super::pdfcmds::PRESET_EXTS)))
.chain(std::iter::once(("Print presets", super::printpresets::PRESET_EXTS)))
.chain(std::iter::once(("Scripts", super::scripts::EXTS)))
.chain(std::iter::once(("Plug-ins", super::plugin::EXTS)))
}
@@ -766,6 +767,20 @@ pub(crate) fn read_file(path: &str) -> Result<Vec<u8>> {
std::fs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))
}
/// The first `max` bytes of the file at `path` (all of it when it is shorter). Anything but a
/// regular file (a folder, a pipe, a device) is refused, also through a link.
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn read_file_head(path: &str, max: usize) -> Result<Vec<u8>> {
use std::io::Read;
may_read(path)?;
if !std::fs::metadata(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?.is_file() {
return Err(EngineError::Other(format!("{path}: not a file")));
}
let mut out = vec![];
std::fs::File::open(path).and_then(|f| f.take(max as u64).read_to_end(&mut out)).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
Ok(out)
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn write_file(path: &str, bytes: &[u8]) -> Result<()> {
may_write(path)?;
@@ -944,6 +959,11 @@ pub(crate) fn read_file(path: &str) -> Result<Vec<u8>> {
Err(no_fs(path))
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn read_file_head(path: &str, _: usize) -> Result<Vec<u8>> {
Err(no_fs(path))
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn write_file(path: &str, _: &[u8]) -> Result<()> {
Err(no_fs(path))
+3 -2
View File
@@ -159,9 +159,10 @@ fn open_exts_cover_every_readable_format() {
assert_eq!(filters[0], ("All readable files", OPEN_EXTS));
assert_eq!(
filters.len(),
7 + FORMATS.iter().filter(|f| f.read).count(),
"and swatch libraries, libraries, flattener, PDF and print presets, plug-ins"
8 + FORMATS.iter().filter(|f| f.read).count(),
"and swatch libraries, libraries, flattener, PDF and print presets, scripts, plug-ins"
);
assert!(filters.contains(&("Scripts", crate::cmd::scripts::EXTS)), "File › Open checks scripts");
assert_eq!(filters.last(), Some(&("Plug-ins", crate::cmd::plugin::EXTS)), "File › Open installs plug-ins");
}
+2
View File
@@ -64,6 +64,7 @@ pub mod printtiling;
pub mod rasterfx;
mod recolor;
pub mod recovery;
pub mod scripts;
mod select;
pub(crate) mod shaper;
pub(crate) mod slices;
@@ -278,6 +279,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
v.extend(printpresets::specs());
v.extend(printtiling::specs());
v.extend(plugin::specs());
v.extend(scripts::specs());
v.extend(cut::specs());
v.extend(halftone::specs());
v.extend(perspgrid::specs());
+159
View File
@@ -0,0 +1,159 @@
//! `script.check`: a script written for the reference app (`.jsx`, `.jsxinc`, `.js`, or a compiled
//! `.jsxbin`) checked without running any of it, by `vectorcraft-scriptcheck`: what kind of file it
//! is, what it needs, its directives and includes, the platform features and document-model member
//! chains it uses, the menu commands it runs, what it does outside the document, and its problems.
//! With a path and `includes`, the files its `#include` lines name are read from inside its folder,
//! each through the same checks as every other file a command reads.
use serde_json::{Value, json};
use vectorcraft_scriptcheck::{Limits, Loaded, Loader, Report};
/// The ids a reply can hold, for the windows that show it.
pub use vectorcraft_scriptcheck::{IncludeStatus, Need, OUTSIDE, PROBLEMS};
use super::*;
/// The extensions of the scripts File › Open shows a check for.
pub const EXTS: &[&str] = &["jsx", "jsxinc", "jsxbin"];
pub fn specs() -> Vec<CommandSpec> {
vec![cmd!(
query "script.check",
"Check Script",
[],
None,
"{path? | data?: the script's text | dataBase64?, name?: the file's name (default: the path's; with a path, only the reply's name), includes?: false (with a path: also check the files its #include lines name, inside the script's folder; not on the web)} check a script written for the reference app (.jsx, .jsxinc, .js, or a compiled .jsxbin) without running any of it → {name, kind: script|compiled|module|notText|tooLarge, needs: [language|dom|files|menuCommands|dialogs|hostOnly], lines, targets: [{value, app, instance?, version?, locale?, line, file?}], directives: [{name, value, line, form: \"#\"|\"//@\", file?}], includes: [{value, line, status: checked|missing|notFollowed|cycle|notRead|limit|unreadable, path?, file?}], features: [{id, count, lines}], dom: {members: [{path, count}], types: [{name, count}]}, menuCommands: {strings: [{value, count, lines}], dynamic}, outside: [{id: filesWritten|filesOpened|network|nativeCode|otherApps|environment|unseenCode|preferences, count, lines, modes?}], problems: [{code, line?, file?, detail?}], truncated} (file: the included file it is in; lines: at most 20 of the script's own, while counts include the included files; at most 8 MiB a file and 32 MiB in all, 64 included files 16 deep, 1,024 paths looked up for them, 1,000 entries a list, text echoed cut to 120 characters)",
always,
check
)]
}
/// The folder a script is in, which its included files are read from.
struct Folder {
dir: std::path::PathBuf,
/// The folder with its links followed (none where the file system can't tell).
real: Option<std::path::PathBuf>,
}
impl Folder {
/// The folder of the script at `path`.
fn of(path: &str) -> Self {
let dir = std::path::Path::new(path).parent().filter(|d| !d.as_os_str().is_empty()).unwrap_or(std::path::Path::new("."));
Folder { real: std::fs::canonicalize(dir).ok(), dir: dir.to_path_buf() }
}
}
impl Loader for Folder {
fn load(&mut self, path: &str, max: usize) -> Loaded {
let file = self.dir.join(path);
// A link that leads out of the script's folder isn't followed.
if let (Some(real), Ok(target)) = (&self.real, std::fs::canonicalize(&file))
&& !target.starts_with(real)
{
return Loaded::Refused;
}
let file = file.to_string_lossy();
if crate::file_access::check_read(&file).is_err() {
return Loaded::Refused;
}
super::fileio::read_file_head(&file, max).map_or(Loaded::Missing, Loaded::Bytes)
}
}
/// The bytes the base64 text `b64` holds, up to `max` and a few more: only the text they take is
/// decoded.
fn decode_head(b64: &str, max: usize) -> Option<Vec<u8>> {
let chars = max.div_ceil(3).saturating_mul(4);
let mut kept = 0usize;
let end = b64
.bytes()
.position(|b| {
kept += usize::from(!b.is_ascii_whitespace());
kept > chars
})
.unwrap_or(b64.len());
vectorcraft_format::base64_decode(b64.get(..end)?)
}
fn check(_: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "script.check";
let limits = Limits::default();
// One byte more than a file may have shows that the file is too large.
let max = limits.file_bytes.saturating_add(1);
let path = str_param(p, "path");
let bytes: std::borrow::Cow<[u8]> = match (path, str_param(p, "data"), str_param(p, "dataBase64")) {
(Some(path), ..) => super::fileio::read_file_head(path, max)?.into(),
(None, Some(text), _) => text.as_bytes().get(..max).unwrap_or(text.as_bytes()).into(),
(None, None, Some(b64)) => decode_head(b64, max).ok_or_else(|| bad(C, "bad dataBase64"))?.into(),
_ => return Err(bad(C, "give `path`, `data` or `dataBase64`")),
};
let name = super::fileio::file_name(str_param(p, "name").or(path).unwrap_or_default());
// With a path, the file is checked under its own name, which its includes are followed from;
// `name` names the reply.
let file = path.map(super::fileio::file_name).unwrap_or_else(|| name.clone());
let mut folder = path.filter(|_| bool_or(p, "includes", false)).map(Folder::of);
let report = vectorcraft_scriptcheck::check(&file, &bytes, folder.as_mut().map(|f| f as &mut dyn Loader), &limits);
Ok(report_json(&name, &report))
}
/// Object `v` with `key: value` added when there is a value.
fn with(mut v: Value, key: &str, value: Option<impl serde::Serialize>) -> Value {
if let (Some(o), Some(x)) = (v.as_object_mut(), value) {
o.insert(key.into(), json!(x));
}
v
}
/// The reply for report `r` on the script `name`.
fn report_json(name: &str, r: &Report) -> Value {
let targets: Vec<Value> = r
.targets
.iter()
.map(|t| {
let v = json!({"value": t.value, "app": t.app, "line": t.line});
let v = with(v, "instance", t.instance.as_deref());
let v = with(v, "version", t.version.as_deref());
let v = with(v, "locale", t.locale.as_deref());
with(v, "file", t.file.as_deref())
})
.collect();
let directives: Vec<Value> = r
.directives
.iter()
.map(|d| with(json!({"name": d.name, "value": d.value, "line": d.line, "form": d.form}), "file", d.file.as_deref()))
.collect();
let includes: Vec<Value> = r
.includes
.iter()
.map(|i| with(with(json!({"value": i.value, "line": i.line, "status": i.status.id()}), "path", i.path.as_deref()), "file", i.file.as_deref()))
.collect();
let features: Vec<Value> = r.features.iter().map(|f| json!({"id": f.id, "count": f.count, "lines": f.lines})).collect();
let members: Vec<Value> = r.members.iter().map(|m| json!({"path": m.name, "count": m.count})).collect();
let types: Vec<Value> = r.types.iter().map(|t| json!({"name": t.name, "count": t.count})).collect();
let strings: Vec<Value> = r.menu_strings.iter().map(|s| json!({"value": s.value, "count": s.count, "lines": s.lines})).collect();
let outside: Vec<Value> = r
.outside
.iter()
.map(|o| with(json!({"id": o.id, "count": o.count, "lines": o.lines}), "modes", Some(&o.modes).filter(|m| !m.is_empty())))
.collect();
let problems: Vec<Value> = r
.problems
.iter()
.map(|p| with(with(with(json!({"code": p.code}), "line", p.line), "file", p.file.as_deref()), "detail", p.detail.as_deref()))
.collect();
json!({
"name": name,
"kind": r.kind.id(),
"needs": r.needs.iter().map(|n| n.id()).collect::<Vec<_>>(),
"lines": r.lines,
"targets": targets,
"directives": directives,
"includes": includes,
"features": features,
"dom": {"members": members, "types": types},
"menuCommands": {"strings": strings, "dynamic": r.menu_dynamic},
"outside": outside,
"problems": problems,
"truncated": r.truncated,
})
}
+2
View File
@@ -1917,6 +1917,8 @@ mod tests_saveoptions;
#[cfg(test)]
mod tests_scalestrokes;
#[cfg(test)]
mod tests_scripts;
#[cfg(test)]
mod tests_shaper;
#[cfg(test)]
mod tests_slices;
+171
View File
@@ -0,0 +1,171 @@
//! `script.check`: scripts written for the reference app checked without running them, from a path,
//! text or base64; the files their `#include` lines name read from the script's folder when asked,
//! through the automation read root; and the limits.
use serde_json::{Value, json};
use super::*;
fn check(p: Value) -> Value {
Session::new().execute("script.check", &p).unwrap_or_else(|e| panic!("script.check {p}: {e}"))
}
/// A fresh folder for the files of test `name`.
fn folder(name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("vc-script-check-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("scripts/lib")).unwrap();
dir
}
fn statuses(r: &Value) -> Vec<&str> {
r["includes"].as_array().unwrap().iter().map(|i| i["status"].as_str().unwrap()).collect()
}
#[test]
fn a_script_is_reported_without_running_it() {
let src = concat!(
"#target acme-12\n",
"var doc = app.activeDocument;\n",
"app.executeMenuCommand('acmecopyall');\n",
"var f = new File(p); f.open('w');\n",
"alert('done');\n",
);
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
let undo = s.doc().unwrap().history.undo.len();
let r = s.execute("script.check", &json!({"data": src, "name": "/Users/me/Tidy Layers.jsx"})).unwrap();
assert_eq!((r["name"].as_str(), r["kind"].as_str(), r["lines"].as_u64()), (Some("Tidy Layers.jsx"), Some("script"), Some(5)));
assert_eq!(r["needs"], json!(["language", "dom", "files", "menuCommands"]));
assert_eq!(r["targets"], json!([{"value": "acme-12", "app": "acme", "version": "12", "line": 1}]));
assert_eq!(r["directives"], json!([{"name": "target", "value": "acme-12", "line": 1, "form": "#"}]));
assert_eq!(r["menuCommands"], json!({"strings": [{"value": "acmecopyall", "count": 1, "lines": [3]}], "dynamic": 0}));
assert_eq!(r["outside"], json!([{"id": "filesWritten", "count": 1, "lines": [4], "modes": ["w"]}]));
assert_eq!(r["dom"], json!({"members": [{"path": "app.activeDocument", "count": 1}], "types": []}));
let notify = r["features"].as_array().unwrap().iter().find(|f| f["id"] == "notify").unwrap();
assert_eq!(notify, &json!({"id": "notify", "count": 1, "lines": [5]}));
assert_eq!((r["problems"].clone(), r["includes"].clone(), r["truncated"].clone()), (json!([]), json!([]), json!(false)));
// Nothing ran: the document has no new undo step, and the check isn't recorded.
assert_eq!(s.doc().unwrap().history.undo.len(), undo);
assert!(!find_command("script.check").unwrap().journal);
}
#[test]
fn a_path_text_and_base64_give_one_report() {
let dir = folder("forms");
let src = "#include 'lib/x.jsxinc'\nvar s = 'open;\n";
let path = dir.join("scripts/Tidy.jsx");
std::fs::write(&path, src).unwrap();
let by_path = check(json!({"path": path.to_string_lossy()}));
assert_eq!(by_path["name"], "Tidy.jsx");
assert_eq!(by_path["problems"], json!([{"code": "unterminatedString", "line": 2}]));
assert_eq!(check(json!({"data": src, "name": "Tidy.jsx"})), by_path);
assert_eq!(check(json!({"dataBase64": vectorcraft_format::base64_encode(src.as_bytes()), "name": "Tidy.jsx"})), by_path);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn includes_are_read_from_the_scripts_folder_only_when_asked() {
let dir = folder("includes");
std::fs::write(dir.join("scripts/main.jsx"), "#include 'lib/net.jsxinc'\n#include '../outside.jsxinc'\n#include 'lib/none.jsxinc'\n").unwrap();
std::fs::write(dir.join("scripts/lib/net.jsxinc"), "var s = new Socket();\n").unwrap();
std::fs::write(dir.join("outside.jsxinc"), "alert('outside');\n").unwrap();
let path = dir.join("scripts/main.jsx").to_string_lossy().into_owned();
let r = check(json!({"path": path}));
assert_eq!(statuses(&r), ["notRead", "notRead", "notRead"]);
assert_eq!(r["needs"], json!(["language"]));
let r = check(json!({"path": path, "includes": true}));
assert_eq!(statuses(&r), ["checked", "notFollowed", "missing"]);
assert_eq!(r["includes"][0]["path"], "lib/net.jsxinc");
assert_eq!(r["needs"], json!(["language", "hostOnly"]));
assert_eq!(r["outside"], json!([{"id": "network", "count": 1, "lines": []}]), "an included file's lines aren't the script's");
// Text has no folder to read from.
let r = check(json!({"data": "#include 'lib/net.jsxinc'\n", "includes": true}));
assert_eq!(statuses(&r), ["notRead"]);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn a_name_given_with_a_path_names_the_reply_and_the_includes_follow_the_path() {
let dir = folder("named");
std::fs::write(dir.join("scripts/main.jsx"), "#include 'helper.jsx'\n").unwrap();
std::fs::write(dir.join("scripts/helper.jsx"), "var s = new Socket();\n").unwrap();
let path = dir.join("scripts/main.jsx").to_string_lossy().into_owned();
let r = check(json!({"path": path, "name": "helper.jsx", "includes": true}));
assert_eq!(r["name"], "helper.jsx");
assert_eq!(statuses(&r), ["checked"]);
assert_eq!(r["needs"], json!(["language", "hostOnly"]));
let _ = std::fs::remove_dir_all(dir);
}
#[cfg(unix)]
#[test]
fn links_out_of_the_folder_and_the_automation_read_root_keep_reads_inside() {
let dir = folder("root");
std::fs::write(dir.join("scripts/main.jsx"), "#include 'lib/link.jsxinc'\n#include 'lib/ok.jsxinc'\n").unwrap();
std::fs::write(dir.join("scripts/lib/ok.jsxinc"), "alert(1);\n").unwrap();
std::fs::write(dir.join("secret.jsxinc"), "var s = new Socket();\n").unwrap();
std::os::unix::fs::symlink(dir.join("secret.jsxinc"), dir.join("scripts/lib/link.jsxinc")).unwrap();
std::fs::write(dir.join("other.jsx"), "alert(2);\n").unwrap();
let main = dir.join("scripts/main.jsx").to_string_lossy().into_owned();
// A link that leads out of the script's folder isn't followed.
let r = check(json!({"path": main, "includes": true}));
assert_eq!(statuses(&r), ["notFollowed", "checked"]);
assert_eq!(r["outside"], json!([]));
// Under a read root, the script and its includes are read inside it, and a script outside isn't.
let roots = crate::file_access::AutomationRoots::new(Some(&dir.join("scripts")), None).unwrap().unwrap();
let mut s = Session::new();
let r = crate::file_access::confine(Some(&roots), || s.execute("script.check", &json!({"path": main, "includes": true}))).unwrap();
assert_eq!(statuses(&r), ["notFollowed", "checked"]);
let other = dir.join("other.jsx").to_string_lossy().into_owned();
let e = crate::file_access::confine(Some(&roots), || s.execute("script.check", &json!({"path": other}))).unwrap_err();
assert!(e.to_string().contains("outside the read root"), "{e}");
let _ = std::fs::remove_dir_all(dir);
}
#[cfg(unix)]
#[test]
fn pipes_and_folders_are_not_read() {
let dir = folder("pipes");
let pipe = dir.join("scripts/lib/pipe.jsxinc");
assert!(std::process::Command::new("mkfifo").arg(&pipe).status().unwrap().success());
std::fs::write(dir.join("scripts/main.jsx"), "#include 'lib/pipe.jsxinc'\n#include 'lib'\n").unwrap();
let main = dir.join("scripts/main.jsx").to_string_lossy().into_owned();
let pipe = pipe.to_string_lossy().into_owned();
// Opening a pipe that has no writer waits for one: the checks run on a thread with a time limit.
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let mut s = Session::new();
let included = s.execute("script.check", &json!({"path": main, "includes": true})).map(|r| r["includes"].clone());
let direct = s.execute("script.check", &json!({ "path": pipe })).map_err(|e| e.to_string());
let _ = tx.send((included.map_err(|e| e.to_string()), direct));
});
let (included, direct) = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("the checks return");
let statuses: Vec<String> = included.unwrap().as_array().unwrap().iter().map(|i| i["status"].as_str().unwrap().to_string()).collect();
assert_eq!(statuses, ["missing", "missing"]);
assert!(direct.is_err_and(|e| e.contains("not a file")));
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn kinds_limits_and_bad_params() {
let kind = |p: Value| check(p)["kind"].as_str().unwrap().to_string();
assert_eq!(kind(json!({"data": "@JSXBIN@ES@2.0@AcmeBody", "name": "a.jsx"})), "compiled");
assert_eq!(kind(json!({"data": "alert(1);", "name": "a.jsxbin"})), "compiled");
assert_eq!(kind(json!({"data": "import a from 'a';\nexport default a;\n"})), "module");
assert_eq!(kind(json!({"dataBase64": vectorcraft_format::base64_encode(b"var a;\0")})), "notText");
// Past 8 MiB as text and as base64: only the head is decoded.
let big = "a".repeat((8 << 20) + 1);
assert_eq!(kind(json!({"data": big})), "tooLarge");
assert_eq!(kind(json!({"dataBase64": vectorcraft_format::base64_encode(big.as_bytes())})), "tooLarge");
assert_eq!(kind(json!({"data": &big[1..]})), "script");
let mut s = Session::new();
for (p, want) in [
(json!({}), "give `path`, `data` or `dataBase64`"),
(json!({"dataBase64": "@@@"}), "bad dataBase64"),
(json!({"path": std::env::temp_dir().join("vc-no-such-script.jsx").to_string_lossy()}), "vc-no-such-script.jsx"),
] {
let e = s.execute("script.check", &p).unwrap_err().to_string();
assert!(e.contains(want), "{p}: {e}");
}
}
+114 -1
View File
@@ -8,7 +8,8 @@
//! Illustrator EPS and `.ai` files (the layers they carry, damaged or hostile), nor Affinity documents
//! (mutated object streams, archives and indexed PNG previews, hostile image dimensions), nor the
//! font files a folder search reads (damaged fonts, collections whose headers give any count of
//! faces).
//! faces), nor scripts written for the reference app checked without running them (garbage,
//! generated script text and mutated scripts).
//!
//! `PROPTEST_CASES=20000 cargo test -p vectorcraft-engine --test import_fuzz` runs a deeper search.
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness).
@@ -2327,3 +2328,115 @@ proptest! {
search_reads("mutated font file", &bytes)?;
}
}
// ---------- scripts checked without running them ----------
/// A script written for these tests, with invented names: directives in both forms, member chains,
/// files, a dialog, menu commands, E4X, an operator and a message to another app.
const SCRIPT_SAMPLE: &str = concat!(
"#target acme_2-12-en_US\n",
"//@include \"lib/util.jsxinc\"\n",
"#includepath \"lib;shared\"\n",
"#targetengine session\n",
"var doc = app.activeDocument;\n",
"var w = new Window(\"dialog { text: 'Tidy' }\");\n",
"var f = File.saveDialog('Save'); f.open('w'); f.write(doc.acmeName); f.close();\n",
"app.executeMenuCommand('acmecopyall');\n",
"var x = <root><item id=\"1\">{doc.acmeName}</item></root>;\n",
"for each (var i in x..item) { $.writeln(i.@id); }\n",
"Vec.prototype['+'] = function (o) { return o; };\n",
"var bt = new BridgeTalk(); bt.target = 'acme'; bt.send();\n",
"if (item.typename == 'AcmeShape') { alert(/a[/]b/g.test(s) ? 'yes' : \"no\"); }\n",
);
/// The pieces generated scripts are made of.
const SCRIPT_PIECES: &[&str] = &[
"#target acme",
"//@include 'a.jsxinc'",
"#includepath 'x;..'",
"#pragma",
"app",
"activeDocument",
"selection",
"var doc = ",
".",
".acmeItems",
"(",
")",
"[",
"]",
"{",
"}",
";",
"=",
"'str'",
"\"open",
"/re[/]x/g",
"/",
"<a b=\"{v}\">",
"</a>",
"<!--",
".@",
"..",
"`t",
"=>",
"\n",
"\r",
"\u{2028}",
"@JSXBIN@",
"executeMenuCommand",
"new File(p)",
"Window(",
"import",
"/*",
"\\",
"0x1F",
" ",
];
/// `script.check` with `p`, which mustn't panic, and whose lists keep to their limit.
fn checked(what: &str, p: Value) -> Result<(), TestCaseError> {
let r = catch_quiet(|| no_panic(vectorcraft_engine::Session::new().execute("script.check", &p)))
.map_err(|e| TestCaseError::fail(format!("{what}: script.check panicked: {e}")))?;
if let Ok(r) = r {
for list in [&r["targets"], &r["directives"], &r["includes"], &r["problems"], &r["dom"]["members"], &r["menuCommands"]["strings"]] {
prop_assert!(list.as_array().is_some_and(|l| l.len() <= 1000), "{}: {}", what, list);
}
}
Ok(())
}
/// The sample the properties mutate reaches every part of the check.
#[test]
fn the_script_sample_is_checked_whole() {
let r = vectorcraft_engine::Session::new().execute("script.check", &json!({"data": SCRIPT_SAMPLE})).unwrap();
assert_eq!(r["kind"], "script");
assert_eq!(r["needs"], json!(["language", "dom", "files", "menuCommands", "dialogs", "hostOnly"]));
assert_eq!(r["problems"], json!([]));
}
proptest! {
#![proptest_config(config())]
#[test]
fn script_garbage_never_panics(bytes in prop::collection::vec(any::<u8>(), 0..2_000), compiled in any::<bool>()) {
let name = if compiled { "garbage.jsxbin" } else { "garbage.jsx" };
checked("script garbage", json!({"dataBase64": vectorcraft_format::base64_encode(&bytes), "name": name}))?;
}
#[test]
fn generated_scripts_never_panic(pieces in prop::collection::vec(prop::sample::select(SCRIPT_PIECES.to_vec()), 0..120)) {
checked("generated script", json!({"data": pieces.concat()}))?;
}
#[test]
fn mutated_scripts_never_panic(cut in 0usize..1_200, edits in prop::collection::vec((0usize..1_200, any::<u8>()), 0..12)) {
let mut bytes = SCRIPT_SAMPLE.as_bytes().to_vec();
for (at, b) in edits {
let n = bytes.len();
bytes[at % n] = b;
}
bytes.truncate(cut);
checked("mutated script", json!({"dataBase64": vectorcraft_format::base64_encode(&bytes), "name": "sample.jsx"}))?;
}
}
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "vectorcraft-scriptcheck"
description = "Static checker for scripts written for the reference app: what a script uses, without running it"
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[lints]
workspace = true
[dev-dependencies]
proptest = { workspace = true }
File diff suppressed because it is too large. Load diff
+179
View File
@@ -0,0 +1,179 @@
//! The names the checker looks for, each list with the public page it comes from, and the need each
//! feature sets. Only names are taken from these pages; no description or example is copied.
use crate::Need;
// The preprocessor directives, written `#name` or `//@name`:
// https://extendscript.docsforadobe.dev/extendscript-tools-features/preprocessor-directives/
pub(crate) const DIRECTIVES: &[&str] = &["include", "includepath", "script", "strict", "target", "targetengine"];
// The `$` object's environment and file functions:
// https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/
pub(crate) const DOLLAR: &str = "$";
pub(crate) const DOLLAR_ENV: &[&str] = &["getenv", "setenv"];
pub(crate) const DOLLAR_EVAL_FILE: &str = "evalFile";
// https://extendscript.docsforadobe.dev/extendscript-tools-features/user-notification-dialogs/
pub(crate) const NOTIFY: &[&str] = &["alert", "confirm", "prompt"];
// https://extendscript.docsforadobe.dev/extendscript-tools-features/localizing-extendscript-strings/
pub(crate) const LOCALIZE: &str = "localize";
// https://extendscript.docsforadobe.dev/extendscript-tools-features/specifying-measurement-values/
pub(crate) const UNIT_VALUE: &str = "UnitValue";
// File and Folder objects, their functions and their open modes:
// https://extendscript.docsforadobe.dev/file-system-access/file-object/
// https://extendscript.docsforadobe.dev/file-system-access/folder-object/
pub(crate) const FILE: &str = "File";
pub(crate) const FOLDER: &str = "Folder";
pub(crate) const FILE_READS: &[&str] = &["read", "readch", "readln"];
pub(crate) const FOLDER_READS: &[&str] = &["getFiles"];
pub(crate) const FILE_WRITES: &[&str] = &["write", "writeln", "remove", "rename", "copy", "createAlias", "create"];
pub(crate) const FILE_DIALOGS: &[&str] = &["openDialog", "saveDialog"];
pub(crate) const FOLDER_DIALOGS: &[&str] = &["selectDialog"];
pub(crate) const INSTANCE_DIALOGS: &[&str] = &["openDlg", "saveDlg", "selectDlg"];
pub(crate) const FOLDER_PLACES: &[&str] =
&["appData", "appPackage", "commonFiles", "current", "desktop", "myDocuments", "startup", "system", "temp", "trash", "userData"];
pub(crate) const EXECUTE: &str = "execute";
pub(crate) const OPEN: &str = "open";
/// The open modes that write: write, append and edit.
pub(crate) const WRITE_MODES: &[&str] = &["w", "a", "e"];
// Windows and their types, and drawing:
// https://extendscript.docsforadobe.dev/user-interface-tools/window-object/
// https://extendscript.docsforadobe.dev/user-interface-tools/resource-specifications/
// https://extendscript.docsforadobe.dev/user-interface-tools/graphic-customization-objects/
pub(crate) const WINDOW: &str = "Window";
pub(crate) const ON_DRAW: &str = "onDraw";
pub(crate) const DRAWING: &[&str] = &["newPen", "newBrush"];
// https://extendscript.docsforadobe.dev/interapplication-communication/bridgetalk-message-object/
pub(crate) const BRIDGE_TALK: &str = "BridgeTalk";
// https://extendscript.docsforadobe.dev/external-communication/socket-object/
pub(crate) const SOCKET: &str = "Socket";
// https://extendscript.docsforadobe.dev/integrating-external-libraries/externalobject-object/
pub(crate) const EXTERNAL_OBJECT: &str = "ExternalObject";
// https://extendscript.docsforadobe.dev/scripting-xmp/xmpscript-object-reference/
pub(crate) const XMP: &[&str] = &["XMPMeta", "XMPFile", "XMPUtils", "XMPConst", "XMPDateTime"];
// https://extendscript.docsforadobe.dev/integrating-xml/the-xml-object/
pub(crate) const XML: &[&str] = &["XML", "XMLList"];
// The operators a script can define for its own classes:
// https://extendscript.docsforadobe.dev/extendscript-tools-features/operator-overloading/
pub(crate) const OPERATORS: &[&str] = &["+", "-", "*", "/", "%", "^", "<", "<=", "==", "<<", ">>", ">>>", "&", "|", "===", "~"];
// https://extendscript.docsforadobe.dev/extendscript-tools-features/extendscript-reflection-interface/
pub(crate) const REFLECT: &str = "reflect";
// The roots of the document model: https://ai-scripting.docsforadobe.dev/jsobjref/Application/
pub(crate) const DOM_ROOTS: [&str; 4] = ["app", "activeDocument", "documents", "selection"];
// Application methods that page doesn't list, named in public forum posts:
// https://community.adobe.com/t5/illustrator-discussions/changes-in-cs6/td-p/4173362 (executeMenuCommand,
// doScript, loadAction, unloadAction) and
// https://community.adobe.com/t5/illustrator-discussions/what-s-new-in-illustrator-scripting-cc2020/m-p/10773439
// (selectTool).
pub(crate) const MENU_COMMAND: &str = "executeMenuCommand";
pub(crate) const ACTIONS: &[&str] = &["doScript", "loadAction", "unloadAction"];
pub(crate) const SELECT_TOOL: &str = "selectTool";
/// The application methods above, which the report lists as features, not as member chains.
pub(crate) const APP_METHODS: &[&str] = &["executeMenuCommand", "doScript", "loadAction", "unloadAction", "selectTool"];
/// Text an action set written out at run time holds.
pub(crate) const ACTION_TEXT: &str = "/actionCount";
/// Constructors of the language (ECMA-262 3rd edition, section 15) and of E4X (ECMA-357 2nd edition,
/// section 13), and the platform's own: `new` of these is no document-model type.
pub(crate) const CONSTRUCTORS: &[&str] = &[
"Object",
"Function",
"Array",
"String",
"Boolean",
"Number",
"Date",
"RegExp",
"Error",
"EvalError",
"RangeError",
"ReferenceError",
"SyntaxError",
"TypeError",
"URIError",
"Namespace",
"QName",
"XML",
"XMLList",
"File",
"Folder",
"Window",
"Socket",
"BridgeTalk",
"ExternalObject",
"UnitValue",
"XMPMeta",
"XMPFile",
"XMPDateTime",
];
/// Every feature id a report can list, in the order the report lists them.
pub const FEATURES: &[&str] = &[
"actions",
"bridgeTalk",
"directive.include",
"directive.other",
"directive.target",
"directive.targetengine",
"dollar",
"dollar.env",
"dollar.evalFile",
"dynamicCode",
"e4x",
"externalObject",
"file.dialog",
"file.execute",
"file.objects",
"file.places",
"file.read",
"file.write",
"hostCheck",
"localize",
"menuCommand",
"notify",
"operatorOverloading",
"preferences",
"reflection",
"scriptui.dialog",
"scriptui.drawing",
"scriptui.palette",
"scriptui.window",
"selectTool",
"socket",
"unitValue",
"xml",
"xmp",
];
/// The need feature `id` sets.
pub(crate) fn need(id: &str) -> Option<Need> {
Some(match id {
"directive.target" | "directive.include" | "directive.other" | "dollar" | "dynamicCode" | "notify" | "localize" | "unitValue"
| "preferences" | "hostCheck" => Need::Language,
"dollar.env" | "dollar.evalFile" | "file.objects" | "file.read" | "file.write" | "file.dialog" | "file.places" | "file.execute" => {
Need::Files
}
"menuCommand" => Need::MenuCommands,
"scriptui.dialog" | "scriptui.window" | "scriptui.drawing" => Need::Dialogs,
"directive.targetengine"
| "scriptui.palette"
| "bridgeTalk"
| "socket"
| "externalObject"
| "xmp"
| "xml"
| "e4x"
| "operatorOverloading"
| "reflection"
| "actions"
| "selectTool" => Need::HostOnly,
_ => return None,
})
}
+87
View File
@@ -0,0 +1,87 @@
//! Preprocessor lines (`#target acme-12`, `//@include "lib.jsxinc"`): their names and values, the
//! parts of a target's application specifier, and the folders an `#includepath` line names.
use crate::catalog;
/// A preprocessor line.
pub(crate) struct Line<'s> {
/// `#` or `//@`.
pub form: &'static str,
/// The name as written.
pub name: &'s str,
/// The documented directive the name is (compared without case), if it is one.
pub known: Option<&'static str>,
/// Its value: the text after the name, without quotes around it, or else without a trailing
/// `//` comment and `;`.
pub value: &'s str,
}
/// The line `text`, which starts with `#` or `//@`.
pub(crate) fn parse(text: &str) -> Line<'_> {
let (form, rest) = match text.strip_prefix("//@") {
Some(rest) => ("//@", rest),
None => ("#", text.strip_prefix('#').unwrap_or(text)),
};
let end = rest.find(|c: char| !c.is_ascii_alphanumeric() && c != '_').unwrap_or(rest.len());
let (name, rest) = (rest.get(..end).unwrap_or(rest), rest.get(end..).unwrap_or_default());
let known = catalog::DIRECTIVES.iter().copied().find(|d| d.eq_ignore_ascii_case(name));
Line { form, name, known, value: value(rest) }
}
fn value(rest: &str) -> &str {
let v = rest.trim();
if let Some(quote) = v.chars().next().filter(|c| matches!(c, '"' | '\'')) {
let inner = v.get(1..).unwrap_or_default();
return inner.find(quote).and_then(|i| inner.get(..i)).unwrap_or(inner);
}
let v = v.find("//").and_then(|i| v.get(..i)).unwrap_or(v);
v.trim_end().trim_end_matches(';').trim_end()
}
/// The parts of an application specifier, `name_instance-version-locale`, every part after the name
/// optional, as
/// https://extendscript.docsforadobe.dev/interapplication-communication/application-and-namespace-specifiers/
/// lays it out.
pub(crate) struct Specifier<'s> {
pub app: &'s str,
pub instance: Option<&'s str>,
pub version: Option<&'s str>,
pub locale: Option<&'s str>,
}
pub(crate) fn target(value: &str) -> Specifier<'_> {
let (head, tail) = match value.split_once('-') {
Some((head, tail)) => (head, Some(tail)),
None => (value, None),
};
let (app, instance) = match head.split_once('_') {
Some((app, instance)) => (app, Some(instance)),
None => (head, None),
};
let (version, locale) = match tail.map(|t| t.split_once('-').map_or((t, None), |(v, l)| (v, Some(l)))) {
Some((version, locale)) => (Some(version), locale),
None => (None, None),
};
fn part(s: Option<&str>) -> Option<&str> {
s.filter(|s| !s.is_empty())
}
Specifier { app, instance: part(instance), version: part(version), locale: part(locale) }
}
/// The most folders an `#includepath` line names that are kept.
const MAX_PATHS: usize = 64;
/// The longest `#include` name or `#includepath` folder followed, in bytes.
pub(crate) const MAX_PATH: usize = 4096;
/// The folders an `#includepath` value names, separated by `;`: the first [`MAX_PATHS`] of those
/// no longer than [`MAX_PATH`].
pub(crate) fn include_paths(value: &str) -> Vec<String> {
value
.split(';')
.map(|p| p.trim().trim_matches(['"', '\'']))
.filter(|p| !p.is_empty() && p.len() <= MAX_PATH)
.take(MAX_PATHS)
.map(str::to_string)
.collect()
}
+296
View File
@@ -0,0 +1,296 @@
//! Following `#include` lines: the file each one names, found inside the script's folder and read
//! through the caller's [`Loader`].
//!
//! The preprocessor page (https://extendscript.docsforadobe.dev/extendscript-tools-features/preprocessor-directives/)
//! says that a name is looked for as given and then in each `#includepath` folder in turn, that a
//! name starting with `/` is absolute, and that `JSINCLUDE` is a fallback; it doesn't say which
//! folder a relative name starts from. The checker takes a name, and an `#includepath` folder, from
//! the folder of the file the line is in. It never follows an absolute name, a name or folder that
//! leads out of the script's folder (`..`), a name or folder longer than [`MAX_PATH`] bytes, or
//! `JSINCLUDE`. Each file is read once. A queue holds the files still to follow, in the order they
//! were read, and nothing recurses: each file is reached first at its smallest depth. At most
//! [`crate::Limits::lookups`] paths are looked up. Once every file is read, an
//! `#include` line is a cycle when the file it names includes the file the line is in, directly or
//! through other files.
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::rc::Rc;
use crate::analyze::{self, IncludeRef, Known};
use crate::directives::MAX_PATH;
use crate::report::{Acc, IncludeStatus};
/// An included file that was read and is checked: its path inside the script's folder, its text
/// (and whether it held bytes that aren't UTF-8), and the `#includepath` folders in force at its
/// start.
pub(crate) struct ReadFile {
pub path: String,
pub text: String,
pub invalid: bool,
pub paths: Rc<[String]>,
}
/// What a [`Loader`] found at a path.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Loaded {
/// The file's first bytes (at most the `max` asked for).
Bytes(Vec<u8>),
/// No file there, or it can't be read.
Missing,
/// A file the caller may not read (outside the folders automation may read).
Refused,
}
/// Where the files a script includes come from: the folder the script is in.
pub trait Loader {
/// The first `max` bytes of the file at `path`, a path inside the script's folder with `/`
/// between its names (`lib/util.jsxinc`).
fn load(&mut self, path: &str, max: usize) -> Loaded;
}
/// Follow the script's `includes`: `script` is its file name and `bytes` its size. Each file read
/// is scanned here for its own includes and for what its variables hold, with its tallies left
/// out; it starts with what `known` holds, which then gains what the file adds → the files to
/// check, in the order they were read.
pub(crate) fn follow(
acc: &mut Acc,
includes: Vec<IncludeRef>,
loader: &mut dyn Loader,
script: &str,
bytes: usize,
known: &mut Known,
) -> Vec<ReadFile> {
/// The `#include` lines of a file that was read, still to follow.
struct Work {
/// The file the lines are in: its index in `files` (0: the script).
file: usize,
includes: Vec<IncludeRef>,
depth: usize,
}
/// An `#include` line as the report lists it.
struct Row {
value: String,
line: usize,
/// The file the line is in.
from: usize,
status: IncludeStatus,
path: Option<String>,
/// The file it names, when that file was read.
to: Option<usize>,
}
let limits = acc.limits;
// The files read, by their path inside the script's folder; the script first.
let mut files: Vec<String> = vec![script.to_string()];
// What became of each path found, and the file read there: a file is read once.
let mut read: BTreeMap<String, (IncludeStatus, Option<usize>)> = BTreeMap::from([(script.to_string(), (IncludeStatus::Checked, Some(0)))]);
// The files still to follow, in the order they were read: each file is reached first at its
// smallest depth.
let mut work = VecDeque::from([Work { file: 0, includes, depth: 0 }]);
let mut rows: Vec<Row> = vec![];
// Which file includes which.
let mut edges: BTreeSet<(usize, usize)> = BTreeSet::new();
// The bytes read and the included files read, those not checked too, and the paths looked up.
let (mut total, mut loaded, mut lookups) = (bytes, 0usize, 0usize);
// The files to check.
let mut checked: Vec<ReadFile> = vec![];
while let Some(w) = work.pop_front() {
let folder = if w.file == 0 { String::new() } else { files.get(w.file).map_or("", |f| parent(f)).to_string() };
for inc in w.includes {
acc.steps = acc.steps.saturating_add(1);
let mut status = IncludeStatus::NotFollowed;
let (mut found, mut to): (Option<String>, Option<usize>) = (None, None);
let mut refused = false;
for path in candidates(&folder, &inc.value, &inc.paths) {
acc.steps = acc.steps.saturating_add(1);
if let Some(&(before, file)) = read.get(&path) {
(status, found, to) = (before, Some(path), file);
break;
}
if w.depth + 1 > limits.include_depth || loaded >= limits.includes || total >= limits.total_bytes || lookups >= limits.lookups {
(status, found) = (IncludeStatus::Limit, None);
break;
}
lookups += 1;
status = IncludeStatus::Missing;
// One byte more than a file or the rest of the total may take shows that it is too large.
let max = limits.file_bytes.min(limits.total_bytes.saturating_sub(total)).saturating_add(1);
let b = match loader.load(&path, max) {
Loaded::Bytes(b) => b,
Loaded::Missing => continue,
Loaded::Refused => {
refused = true;
continue;
}
};
// Every file read counts, also one that is too large, compiled or not text.
loaded += 1;
total = total.saturating_add(b.len());
acc.steps = acc.steps.saturating_add(b.len());
status = if b.len() > limits.file_bytes || total > limits.total_bytes {
IncludeStatus::Limit
} else if crate::is_compiled(&path, &b) || !crate::is_text(&b) {
IncludeStatus::Unreadable
} else {
IncludeStatus::Checked
};
if status != IncludeStatus::Checked {
read.insert(path.clone(), (status, None));
found = Some(path);
break;
}
files.push(path.clone());
let file = files.len() - 1;
read.insert(path.clone(), (status, Some(file)));
let (text, invalid) = crate::decode(&b);
let mut tallies = Acc::new(limits);
let scan = analyze::scan(&text, Some(&path), Rc::clone(&inc.paths), known, &mut tallies);
acc.steps = acc.steps.saturating_add(tallies.steps);
known.add(scan.known, limits.entries);
checked.push(ReadFile { path: path.clone(), text: text.into_owned(), invalid, paths: Rc::clone(&inc.paths) });
work.push_back(Work { file, includes: scan.includes, depth: w.depth + 1 });
(found, to) = (Some(path), Some(file));
break;
}
if status == IncludeStatus::Missing && refused {
status = IncludeStatus::NotFollowed;
}
if let Some(to) = to {
edges.insert((w.file, to));
}
// One row more than the report keeps marks it truncated.
if rows.len() <= limits.entries {
rows.push(Row { value: acc.echo(&inc.value), line: inc.line, from: w.file, status, path: found, to });
}
}
}
let reach = reachable(files.len(), &edges, acc);
for r in rows {
let cycle = r.to.and_then(|to| reach.get(to)).and_then(|reached| reached.get(r.from)).copied().unwrap_or(false);
let status = if cycle { IncludeStatus::Cycle } else { r.status };
let file = files.get(r.from).filter(|_| r.from > 0).map(String::as_str);
acc.include(&r.value, r.line, file, status, r.path.as_deref());
}
checked
}
/// For each of `n` files, the files it includes directly or through other files (itself too),
/// with `edges` giving the files each one includes.
fn reachable(n: usize, edges: &BTreeSet<(usize, usize)>, acc: &mut Acc) -> Vec<Vec<bool>> {
let mut next: Vec<Vec<usize>> = vec![vec![]; n];
for &(from, to) in edges {
if let Some(v) = next.get_mut(from) {
v.push(to);
}
}
(0..n)
.map(|start| {
let mut seen = vec![false; n];
let mut stack = vec![start];
while let Some(f) = stack.pop() {
acc.steps = acc.steps.saturating_add(1);
match seen.get_mut(f) {
Some(s) if !*s => *s = true,
_ => continue,
}
stack.extend(next.get(f).into_iter().flatten().copied());
}
seen
})
.collect()
}
/// List the script's `includes` as not read.
pub(crate) fn not_read(acc: &mut Acc, includes: Vec<IncludeRef>) {
for inc in includes {
acc.include(&inc.value, inc.line, None, IncludeStatus::NotRead, None);
}
}
/// The folder part of `path` (`lib/a.jsxinc` → `lib`, `a.jsxinc` → ``).
fn parent(path: &str) -> &str {
path.rsplit_once('/').map_or("", |(folder, _)| folder)
}
/// A name the checker doesn't follow: absolute (`/x`, `\x`, `C:x`, `~/x`), naming an environment
/// variable (`%APPDATA%`, `$HOME`), or longer than [`MAX_PATH`].
fn unfollowed(name: &str) -> bool {
name.starts_with(['/', '\\', '~', '$']) || name.contains([':', '%']) || name.len() > MAX_PATH
}
/// `rel` taken from `folder`, both inside the script's folder, as a path from that folder (empty
/// for the folder itself): none when it leads out of it.
fn walk(folder: &str, rel: &str) -> Option<String> {
let mut parts: Vec<&str> = folder.split('/').filter(|s| !s.is_empty()).collect();
for part in rel.split(['/', '\\']) {
match part {
"" | "." => {}
".." => {
parts.pop()?;
}
p => parts.push(p),
}
}
Some(parts.join("/"))
}
/// The file `rel` names from `folder`, both inside the script's folder: none when it leads out of
/// it, or names the folder itself.
fn join(folder: &str, rel: &str) -> Option<String> {
walk(folder, rel).filter(|path| !path.is_empty())
}
/// The paths an `#include` of `name` may mean, from `folder`, in the order they are tried and each
/// once: the name itself, then the name in each `#includepath` folder. Each is worked out when it
/// is tried. Names and folders the checker doesn't follow, and names or folders that lead out of
/// the script's folder, give none.
fn candidates<'a>(folder: &'a str, name: &'a str, paths: &'a [String]) -> impl Iterator<Item = String> + 'a {
let name = Some(name).filter(|n| !unfollowed(n));
let first = name.and_then(|n| join(folder, n));
let inside = name
.into_iter()
.flat_map(move |n| paths.iter().filter(|p| !unfollowed(p)).filter_map(move |p| walk(folder, p)).filter_map(move |p| join(&p, n)));
let mut tried: Vec<String> = vec![];
first.into_iter().chain(inside).filter(move |p| {
let new = !tried.contains(p);
if new {
tried.push(p.clone());
}
new
})
}
#[cfg(test)]
mod tests {
use super::*;
fn tried(folder: &str, name: &str, paths: &[String]) -> Vec<String> {
candidates(folder, name, paths).collect()
}
#[test]
fn names_are_taken_from_the_including_files_folder() {
assert_eq!(tried("", "a.jsxinc", &[]), ["a.jsxinc"]);
assert_eq!(tried("lib", "b.jsxinc", &[]), ["lib/b.jsxinc"]);
assert_eq!(tried("lib/deep", "../c.jsxinc", &[]), ["lib/c.jsxinc"]);
assert_eq!(tried("lib", "./x\\y.jsxinc", &[]), ["lib/x/y.jsxinc"]);
// Then each include path, inside the folder.
let paths = ["shared".to_string(), "/abs".to_string(), "../out".to_string(), "shared".to_string()];
assert_eq!(tried("", "u.jsxinc", &paths), ["u.jsxinc", "shared/u.jsxinc"]);
// An include path may lead back to the script's own folder.
assert_eq!(tried("lib", "shared.jsxinc", &["..".to_string()]), ["lib/shared.jsxinc", "shared.jsxinc"]);
}
#[test]
fn absolute_names_and_names_leading_out_are_not_followed() {
for name in
["/etc/x.jsx", "\\\\server\\x.jsx", "C:/x.jsx", "~/x.jsx", "%APPDATA%/x.jsx", "$HOME/x.jsx", "../x.jsx", "a/../../x.jsx", ".", "a/.."]
{
assert!(tried("", name, &[]).is_empty(), "{name}");
}
assert!(tried("lib", "../../x.jsx", &[]).is_empty());
// Names and folders longer than a path can be.
let long = "d".repeat(MAX_PATH + 1);
assert!(tried("", &long, &[]).is_empty());
assert_eq!(tried("", "a.jsxinc", &[long]), ["a.jsxinc"]);
}
}
+849
View File
@@ -0,0 +1,849 @@
//! The lexer: script text into tokens that cover it exactly once, blanks, line breaks and comments
//! included. It reads ECMAScript 3 with E4X XML literals, the preprocessor lines (`#name …` at the
//! start of a line, and `//@name …` comments there that name a directive), and the later syntax a
//! script may hold (template literals, `=>`, `?.`), which the analysis reports. An XML literal's
//! markup gives tokens of its own, and the code in its braces gives the same tokens as code anywhere
//! else. Each step moves forward through the text, and nothing recurses: an XML literal's elements
//! are counted, and the open parentheses, the open braces, the `?` of conditionals at each bracket
//! level and the levels of XML literals and their code are kept on stacks capped at the nesting
//! limit.
use crate::catalog;
use crate::report::{TOO_DEEP, UNTERMINATED_COMMENT, UNTERMINATED_REGEXP, UNTERMINATED_STRING, UNTERMINATED_TEMPLATE};
/// What a token is.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum T {
/// Blanks: spaces, tabs and other white space.
Space,
/// One line break: LF, CR LF, CR, U+2028 or U+2029.
Newline,
/// A `//` or `/* */` comment.
Comment,
/// A preprocessor line: `#name …` at the start of a line, or a `//@name …` comment there that
/// names a directive.
Directive,
Ident,
Number,
/// A `'…'` or `"…"` string literal.
Str,
/// A `` `…` `` template literal.
Template,
RegExp,
/// The start of an E4X XML literal (`<a b="1">…</a>`, `<>…</>`, `<!-- … -->`): its markup up to
/// its end, or up to and with the `{` of the code in it.
Xml,
/// More of an XML literal's markup: from the `}` that ends the code in it up to the literal's
/// end or the next `{`.
XmlMore,
Punct,
/// A character no token starts with, or `#` and a name away from the start of a line.
Other,
}
/// A token: what it is, its text and the line it starts on (from 1), with the problem it has.
#[derive(Clone, Copy, Debug)]
pub(crate) struct Token<'s> {
pub kind: T,
pub text: &'s str,
pub line: usize,
pub problem: Option<&'static str>,
/// A statement may start at the token (a `function` there is a declaration).
pub stmt: bool,
/// An expression may start at the token (a `/` there starts a regular expression).
pub expr: bool,
/// The byte offset in the text where the token starts.
pub start: usize,
}
/// The kinds of line break the text has used.
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct Breaks {
pub lf: bool,
pub crlf: bool,
pub cr: bool,
}
/// Punctuators, longest first: ECMAScript 3's, E4X's (`..`, `.@`, `::`, `@`) and later ones (`=>`,
/// `...`, `?.`, `??`, `**` and the assignments built on them), which the analysis reports.
const PUNCTS: &[&str] = &[
">>>=", "===", "!==", ">>>", "<<=", ">>=", "**=", "...", "&&=", "||=", "??=", "==", "!=", "<=", ">=", "&&", "||", "++", "--", "+=", "-=", "*=",
"/=", "%=", "&=", "|=", "^=", "<<", ">>", "=>", "**", "?.", "??", "::", "..", ".@", "{", "}", "(", ")", "[", "]", ";", ",", "<", ">", "+", "-",
"*", "/", "%", "&", "|", "^", "!", "~", "?", ":", "=", ".", "@",
];
pub(crate) struct Lexer<'s> {
src: &'s str,
pos: usize,
line: usize,
/// Only blanks since the last line break (or the start): a `#` here starts a preprocessor line.
line_start: bool,
/// An expression may start here: a `/` starts a regular expression and a `<` an XML literal.
expr: bool,
/// A statement may start here: a `{` here opens a block. A `{` where only an expression may
/// start opens an object literal.
stmt: bool,
/// The last significant token was `return`, `throw`, `break` or `continue`: a line break here
/// ends the statement (ECMA-262 3rd edition, section 7.9.1).
restricted: bool,
/// The last significant token was `.`, `..`, `.@` or `::`: a name here is a property's, also when
/// it is a keyword such as `return`.
member: bool,
/// The last significant token was `if`, `while`, `for`, `with` or the `each` of `for each`: a
/// `(` here opens a condition.
cond: bool,
/// For each `(` still open, whether it opened a condition, up to `nesting` of them; `deeper`
/// counts the ones past that.
parens: Vec<bool>,
deeper: usize,
/// For each `{` still open, what it opened, up to `nesting` of them; `deeper_braces` counts the
/// ones past that.
braces: Vec<Brace>,
deeper_braces: usize,
/// `function` was read and its body's `{` is still to come: whether the function is an
/// expression (it starts where no statement may).
function: Option<bool>,
/// For the text outside any bracket and each bracket still open, how many `?` of a conditional
/// (`? :`) wait for their `:` there, up to `nesting` levels; `deeper_questions` counts the
/// levels past that.
questions: Vec<usize>,
deeper_questions: usize,
/// The XML literal being read, its innermost level last: its markup, the code in a `{…}` in
/// it, an XML literal in that code, and so on. Empty outside an XML literal.
xml: Vec<Level>,
/// The line the outermost XML literal being read starts on.
xml_line: usize,
/// The most open parentheses kept, and the most levels in `xml`.
nesting: usize,
/// The line of the XML literal the text ended in, when it ended in one.
pub unclosed_xml: Option<usize>,
pub breaks: Breaks,
/// One per character read (and per token): the work done.
pub steps: usize,
}
fn is_break(c: char) -> bool {
matches!(c, '\n' | '\r' | '\u{2028}' | '\u{2029}')
}
fn is_blank(c: char) -> bool {
!is_break(c) && (c.is_whitespace() || c == '\u{feff}')
}
fn ident_start(c: char) -> bool {
c == '$' || c == '_' || c.is_ascii_alphabetic() || (!c.is_ascii() && c.is_alphabetic())
}
/// A character a name may hold after its first.
pub(crate) fn ident_part(c: char) -> bool {
ident_start(c) || c.is_ascii_digit() || (!c.is_ascii() && c.is_alphanumeric())
}
/// After `<`, the start of an XML literal: a name, `>` (a list), `!` (a comment or CDATA), `?` (a
/// processing instruction) or `{` (a computed name).
fn xml_start(c: char) -> bool {
c.is_ascii_alphabetic() || matches!(c, '_' | '>' | '!' | '?' | '{')
}
/// Whether an expression may start after a significant token. After a `}`, the brace's kind decides
/// it ([`Lexer::brace`]), after an XML literal's markup [`Lexer::markup_token`] sets it, and after a
/// property's name, a keyword included ([`Lexer::member`]), none starts.
pub(crate) fn expr_after(kind: T, text: &str) -> bool {
match kind {
T::Ident => matches!(text, "return" | "typeof" | "instanceof" | "in" | "new" | "delete" | "void" | "throw" | "case" | "do" | "else"),
T::Number | T::Str | T::Template | T::RegExp => false,
T::Punct => !matches!(text, ")" | "]" | "++" | "--"),
_ => true,
}
}
/// `rest`, the text after `//@`, starts with a directive's name.
fn names_directive(rest: &str) -> bool {
let end = rest.find(|c: char| !c.is_ascii_alphabetic()).unwrap_or(rest.len());
let name = rest.get(..end).unwrap_or_default();
catalog::DIRECTIVES.iter().any(|d| d.eq_ignore_ascii_case(name))
}
/// Where in its markup an XML literal is read.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum At {
/// The `<` of a tag, comment, CDATA section or processing instruction.
Markup,
/// A start tag (`end` false) or an end tag, after its `<`.
Tag { end: bool },
/// An element's text.
Text,
}
/// A level of the XML literal being read.
#[derive(Clone, Copy, Debug)]
enum Level {
/// The markup of an XML literal, `open` of its elements open.
Xml { open: usize, at: At },
/// The code in braces, `braces` of them open.
Code { braces: usize },
}
/// What a `{` opened.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Brace {
/// A block, or a function declaration's body: a statement may start after its `}`.
Block,
/// An object literal: a value.
Object,
/// A function expression's body: statements inside, and a value after its `}`.
Function,
}
/// What reading a little more of an XML literal's markup led to.
enum Step {
/// The markup goes on.
On,
/// The literal ends: none of its elements is open.
Up,
/// Code in braces starts at the next character, a `{`.
Code,
/// The text ended first.
End,
}
impl<'s> Lexer<'s> {
/// A lexer of `src` that keeps at most `nesting` open parentheses, open braces, bracket levels
/// of conditionals and levels of an XML literal ([`crate::Limits::nesting`]).
pub fn new(src: &'s str, nesting: usize) -> Self {
Self {
src,
pos: 0,
line: 1,
line_start: true,
expr: true,
stmt: true,
restricted: false,
member: false,
cond: false,
parens: vec![],
deeper: 0,
braces: vec![],
deeper_braces: 0,
function: None,
questions: vec![0],
deeper_questions: 0,
xml: vec![],
xml_line: 0,
nesting,
unclosed_xml: None,
breaks: Breaks::default(),
steps: 0,
}
}
/// The lines read so far: the line breaks, and one more for a last line without one.
pub fn lines(&self) -> usize {
let last = self.src.chars().next_back();
self.line - usize::from(last.is_none_or(is_break))
}
fn rest(&self) -> &'s str {
self.src.get(self.pos..).unwrap_or_default()
}
fn peek(&self) -> Option<char> {
self.rest().chars().next()
}
fn peek_at(&self, n: usize) -> Option<char> {
self.rest().chars().nth(n)
}
fn starts(&self, s: &str) -> bool {
self.rest().starts_with(s)
}
fn bump(&mut self, c: char) {
self.pos += c.len_utf8();
self.steps += 1;
}
/// Read `n` ASCII characters.
fn skip(&mut self, n: usize) {
self.pos += n;
self.steps += 1;
}
/// A line break here, read: false when there is none.
fn line_break(&mut self) -> bool {
match self.peek() {
Some('\n') => {
self.pos += 1;
self.breaks.lf = true;
}
Some('\r') if self.starts("\r\n") => {
self.pos += 2;
self.breaks.crlf = true;
}
Some('\r') => {
self.pos += 1;
self.breaks.cr = true;
}
Some(c @ ('\u{2028}' | '\u{2029}')) => self.pos += c.len_utf8(),
_ => return false,
}
self.line += 1;
self.steps += 1;
true
}
/// Up to the next line break.
fn until_line_break(&mut self) {
while let Some(c) = self.peek().filter(|&c| !is_break(c)) {
self.bump(c);
}
}
/// The rest of a name (its first character included): letters, digits, `$`, `_` and `\u`
/// escapes.
fn name(&mut self) {
loop {
match self.peek() {
Some('\\') if self.peek_at(1) == Some('u') => {
self.skip(2);
for _ in 0..4 {
match self.peek() {
Some(h) if h.is_ascii_hexdigit() => self.bump(h),
_ => break,
}
}
}
Some(c) if ident_part(c) => self.bump(c),
_ => break,
}
}
}
fn digits(&mut self, ok: fn(char) -> bool) {
while let Some(c) = self.peek().filter(|&c| ok(c) || c == '_') {
self.bump(c);
}
}
/// A number: decimal (with a fraction and an exponent), hexadecimal, or the later binary,
/// octal and `n` (big integer) forms.
fn number(&mut self) {
let b = self.rest().as_bytes();
match (b.first(), b.get(1)) {
(Some(b'0'), Some(b'x' | b'X')) => {
self.skip(2);
self.digits(|c| c.is_ascii_hexdigit());
}
(Some(b'0'), Some(b'b' | b'B' | b'o' | b'O')) => {
self.skip(2);
self.digits(|c| c.is_ascii_digit());
}
_ => {
self.digits(|c| c.is_ascii_digit());
if self.peek() == Some('.') {
self.bump('.');
self.digits(|c| c.is_ascii_digit());
}
let signed = matches!(self.peek_at(1), Some('+' | '-')) && self.peek_at(2).is_some_and(|c| c.is_ascii_digit());
if matches!(self.peek(), Some('e' | 'E')) && (self.peek_at(1).is_some_and(|c| c.is_ascii_digit()) || signed) {
self.skip(if signed { 2 } else { 1 });
self.digits(|c| c.is_ascii_digit());
}
}
}
if self.peek() == Some('n') {
self.bump('n');
}
}
/// A `'…'` or `"…"` string, which ends at its line's end when it isn't closed.
fn string(&mut self, quote: char) -> Option<&'static str> {
self.bump(quote);
loop {
match self.peek() {
None => return Some(UNTERMINATED_STRING),
Some(c) if c == quote => {
self.bump(c);
return None;
}
Some('\\') => {
self.bump('\\');
// A backslash before a line break carries the string on to the next line.
if !self.line_break()
&& let Some(c) = self.peek()
{
self.bump(c);
}
}
Some(c) if is_break(c) => return Some(UNTERMINATED_STRING),
Some(c) => self.bump(c),
}
}
}
fn template(&mut self) -> Option<&'static str> {
self.bump('`');
loop {
if self.line_break() {
continue;
}
match self.peek() {
None => return Some(UNTERMINATED_TEMPLATE),
Some('`') => {
self.bump('`');
return None;
}
Some('\\') => {
self.bump('\\');
if !self.line_break()
&& let Some(c) = self.peek()
{
self.bump(c);
}
}
Some(c) => self.bump(c),
}
}
}
/// A regular expression and its flags, which ends at its line's end when it isn't closed.
fn regexp(&mut self) -> Option<&'static str> {
self.bump('/');
let mut class = false;
loop {
match self.peek() {
None => return Some(UNTERMINATED_REGEXP),
Some(c) if is_break(c) => return Some(UNTERMINATED_REGEXP),
Some('\\') => {
self.bump('\\');
if let Some(c) = self.peek().filter(|&c| !is_break(c)) {
self.bump(c);
}
}
Some('/') if !class => {
self.bump('/');
break;
}
Some(c) => {
match c {
'[' => class = true,
']' => class = false,
_ => {}
}
self.bump(c);
}
}
}
while let Some(c) = self.peek().filter(|&c| ident_part(c)) {
self.bump(c);
}
None
}
fn block_comment(&mut self) -> Option<&'static str> {
self.skip(2);
loop {
if self.starts("*/") {
self.skip(2);
return None;
}
if self.line_break() {
continue;
}
match self.peek() {
Some(c) => self.bump(c),
None => return Some(UNTERMINATED_COMMENT),
}
}
}
/// Up to and past `end`: false when the text ends first.
fn past(&mut self, end: &str) -> bool {
loop {
if self.starts(end) {
self.skip(end.len());
return true;
}
if self.line_break() {
continue;
}
match self.peek() {
Some(c) => self.bump(c),
None => return false,
}
}
}
/// A token of `kind` that starts at `start` on `line`: the markup of the XML literal being
/// read, up to the literal's end or up to and with the `{` of the code in it. A `{` whose code
/// would take a level past `nesting` is read as markup, and the token has the problem
/// [`TOO_DEEP`]. When the text ends first, the literal is left unclosed
/// ([`Lexer::unclosed_xml`]). `stmt`: a statement may start at the token; `expr`: an expression
/// may start at it.
fn markup_token(&mut self, start: usize, line: usize, kind: T, stmt: bool, expr: bool) -> Token<'s> {
let mut problem = None;
while let Some(&Level::Xml { mut open, mut at }) = self.xml.last() {
let step = self.markup(&mut open, &mut at);
if let Some(level) = self.xml.last_mut() {
*level = Level::Xml { open, at };
}
match step {
Step::On => {}
Step::Code if self.xml.len() < self.nesting => {
self.bump('{');
self.xml.push(Level::Code { braces: 1 });
self.open_level();
self.expr = true;
break;
}
Step::Code => {
self.bump('{');
problem = Some(TOO_DEEP);
}
Step::Up => {
// An XML literal is a value: no expression starts after it.
self.xml.pop();
self.expr = false;
break;
}
Step::End => {
self.xml.clear();
self.unclosed_xml = Some(self.xml_line);
self.expr = false;
break;
}
}
}
// The code in an XML literal's braces is an expression, and so is the literal: no statement
// starts after either.
self.line_start = false;
self.stmt = false;
self.cond = false;
self.restricted = false;
self.member = false;
Token { kind, text: self.src.get(start..self.pos).unwrap_or_default(), line, problem, stmt, expr, start }
}
/// A little more of an XML literal's markup, `open` of its elements open and `at` where in it:
/// a comment, CDATA section or processing instruction whole, or one character of a tag or of
/// an element's text.
fn markup(&mut self, open: &mut usize, at: &mut At) -> Step {
match *at {
At::Markup => {
let read = if self.starts("<!--") {
self.past("-->")
} else if self.starts("<![CDATA[") {
self.past("]]>")
} else if self.starts("<?") {
self.past("?>")
} else {
let end = self.starts("</");
self.skip(if end { 2 } else { 1 });
*at = At::Tag { end };
return Step::On;
};
*at = At::Text;
match (read, *open) {
(false, _) => Step::End,
(true, 0) => Step::Up,
(true, _) => Step::On,
}
}
At::Tag { end } => {
if self.line_break() {
return Step::On;
}
// `>` ends a tag, and `/>` a tag whose element closes itself.
let closes_itself = match self.peek() {
None => return Step::End,
Some('>') => {
self.bump('>');
false
}
Some('/') if self.peek_at(1) == Some('>') => {
self.skip(2);
true
}
Some(q @ ('"' | '\'')) => {
self.bump(q);
return if self.past(if q == '"' { "\"" } else { "'" }) { Step::On } else { Step::End };
}
Some('{') => return Step::Code,
Some(c) => {
self.bump(c);
return Step::On;
}
};
*open = match (end, closes_itself) {
(true, _) => open.saturating_sub(1),
(false, false) => open.saturating_add(1),
(false, true) => *open,
};
*at = At::Text;
if *open == 0 { Step::Up } else { Step::On }
}
At::Text => {
if self.line_break() {
return Step::On;
}
match self.peek() {
None => Step::End,
Some('<') => {
*at = At::Markup;
Step::On
}
Some('{') => Step::Code,
Some(c) => {
self.bump(c);
Step::On
}
}
}
}
}
fn punct(&self) -> Option<&'static str> {
let rest = self.rest();
// `?.` before a digit is `?` and a number (`a ?.5 : 1`).
PUNCTS.iter().copied().find(|p| rest.starts_with(p) && !(*p == "?." && rest.as_bytes().get(2).is_some_and(u8::is_ascii_digit)))
}
/// The parentheses after the punctuator `punct`: `(` opens one, a condition when `cond` is set,
/// and `)` closes the last one open. True when `punct` closes a condition.
fn paren(&mut self, punct: &str) -> bool {
match punct {
"(" if self.parens.len() < self.nesting => self.parens.push(self.cond),
"(" => self.deeper = self.deeper.saturating_add(1),
")" if self.deeper > 0 => self.deeper -= 1,
")" => return self.parens.pop().unwrap_or(false),
_ => {}
}
false
}
/// The braces after the punctuator `punct`: `{` opens one and `}` closes the last one open. For
/// `{` and `}`, what the brace opened: the body of the function just read, a block where a
/// statement may start or no expression may, else an object literal. A brace past `nesting` is
/// taken as a block's when it closes.
fn brace(&mut self, punct: &str) -> Option<Brace> {
match punct {
"{" => {
let brace = match self.function.take() {
Some(true) => Brace::Function,
Some(false) => Brace::Block,
None if self.stmt || !self.expr => Brace::Block,
None => Brace::Object,
};
if self.braces.len() < self.nesting {
self.braces.push(brace);
} else {
self.deeper_braces = self.deeper_braces.saturating_add(1);
}
Some(brace)
}
"}" if self.deeper_braces > 0 => {
self.deeper_braces -= 1;
Some(Brace::Block)
}
"}" => Some(self.braces.pop().unwrap_or(Brace::Block)),
_ => None,
}
}
/// A bracket opens: the `?` inside it are counted apart from the ones outside.
fn open_level(&mut self) {
if self.questions.len() <= self.nesting {
self.questions.push(0);
} else {
self.deeper_questions = self.deeper_questions.saturating_add(1);
}
}
/// The innermost bracket closes.
fn close_level(&mut self) {
if self.deeper_questions > 0 {
self.deeper_questions -= 1;
} else if self.questions.len() > 1 {
self.questions.pop();
}
}
/// The punctuator `punct` at the innermost level: `?` opens a conditional there, and a `:`
/// there while one is open is that conditional's. True for that `:`.
fn conditional(&mut self, punct: &str) -> bool {
if self.deeper_questions > 0 {
return false;
}
let Some(open) = self.questions.last_mut() else { return false };
match punct {
"?" => *open = open.saturating_add(1),
":" if *open > 0 => {
*open -= 1;
return true;
}
_ => {}
}
false
}
/// A line break after the last significant token. After the end of an expression a statement
/// may start (JavaScript ends the statement at the line break when the next token can't go on
/// with it), and after `return`, `throw`, `break` and `continue` a new statement starts.
fn line_ended(&mut self) {
if std::mem::take(&mut self.restricted) {
self.expr = true;
self.stmt = true;
}
self.stmt |= !self.expr;
}
}
impl<'s> Iterator for Lexer<'s> {
type Item = Token<'s>;
fn next(&mut self) -> Option<Token<'s>> {
let (start, line, stmt, expr) = (self.pos, self.line, self.stmt, self.expr);
let Some(c) = self.peek() else {
// The text ends in the code of an XML literal.
if !self.xml.is_empty() {
self.xml.clear();
self.unclosed_xml = Some(self.xml_line);
}
return None;
};
let mut problem = None;
let kind = if self.line_break() {
T::Newline
} else if is_blank(c) {
while let Some(c) = self.peek().filter(|&c| is_blank(c)) {
self.bump(c);
}
T::Space
} else if self.starts("//") {
let directive = self.line_start && self.rest().strip_prefix("//@").is_some_and(names_directive);
self.until_line_break();
if directive { T::Directive } else { T::Comment }
} else if self.starts("/*") {
problem = self.block_comment();
T::Comment
} else if c == '#' && self.line_start {
self.until_line_break();
T::Directive
} else if c == '#' && self.peek_at(1).is_some_and(ident_start) {
self.bump('#');
self.name();
T::Other
} else if c == '\'' || c == '"' {
problem = self.string(c);
T::Str
} else if c == '`' {
problem = self.template();
T::Template
} else if c == '/' && self.expr {
problem = self.regexp();
T::RegExp
} else if c == '<' && self.expr && self.peek_at(1).is_some_and(xml_start) {
if self.xml.len() < self.nesting {
// An XML literal starts, in the script or in the code of another one.
if self.xml.is_empty() {
self.xml_line = line;
}
self.xml.push(Level::Xml { open: 0, at: At::Markup });
return Some(self.markup_token(start, line, T::Xml, stmt, expr));
}
// An XML literal more levels deep than are kept: its `<` is read as an operator.
self.skip(1);
problem = Some(TOO_DEEP);
T::Punct
} else if c == '}' && matches!(self.xml.last(), Some(Level::Code { braces: 1 })) {
// The `}` that ends the code in an XML literal's braces: the literal's markup goes on.
self.xml.pop();
self.close_level();
self.bump('}');
return Some(self.markup_token(start, line, T::XmlMore, stmt, expr));
} else if ident_start(c) || (c == '\\' && self.peek_at(1) == Some('u')) {
self.name();
T::Ident
} else if c.is_ascii_digit() || (c == '.' && self.peek_at(1).is_some_and(|d| d.is_ascii_digit())) {
self.number();
T::Number
} else if let Some(p) = self.punct() {
self.skip(p.len());
T::Punct
} else {
self.bump(c);
T::Other
};
let text = self.src.get(start..self.pos).unwrap_or_default();
match kind {
T::Space => {}
T::Newline => {
self.line_start = true;
self.line_ended();
}
// A comment that holds a line break counts as one.
T::Comment => {
self.line_start = false;
if text.contains(is_break) {
self.line_ended();
}
}
_ => {
self.line_start = false;
// A name after `.`, `..`, `.@` or `::` is a property's, also when it is a keyword: no
// expression or statement starts after it.
let property = kind == T::Ident && self.member;
// After the `)` of a condition comes a statement, which may start with a regular
// expression or an XML literal.
let condition = kind == T::Punct && self.paren(text);
// A statement may also start after the `}` of a block. After the `}` of an object
// literal or of a function expression's body, `/` divides.
let brace = if kind == T::Punct { self.brace(text) } else { None };
self.expr = condition
|| match (text, brace) {
("}", Some(brace)) => brace == Brace::Block,
_ => !property && expr_after(kind, text),
};
if kind == T::Ident && !property && text == "function" {
self.function = Some(!self.stmt);
}
if kind == T::Punct {
match text {
"(" | "[" | "{" => self.open_level(),
")" | "]" | "}" => self.close_level(),
_ => {}
}
}
let ternary = kind == T::Punct && self.conditional(text);
// A statement may start inside a block or a function's body, after `;`, `=>`, a
// preprocessor line, `else`, `do`, `try` and `finally`, and after the `:` of a label or
// a `case` (not a conditional's, and outside an object literal).
self.stmt = condition
|| match (text, brace) {
("{", Some(brace)) => brace != Brace::Object,
("}", Some(brace)) => brace == Brace::Block,
_ => false,
}
|| kind == T::Directive
|| (kind == T::Punct && matches!(text, ";" | "=>"))
|| (kind == T::Punct && text == ":" && !ternary && self.braces.last() != Some(&Brace::Object))
|| (kind == T::Ident && !property && matches!(text, "else" | "do" | "try" | "finally"));
self.cond = kind == T::Ident && !property && (matches!(text, "if" | "while" | "for" | "with") || (text == "each" && self.cond));
self.restricted = kind == T::Ident && !property && matches!(text, "return" | "throw" | "break" | "continue");
self.member = kind == T::Punct && matches!(text, "." | ".." | ".@" | "::");
// The braces inside the code of an XML literal.
if kind == T::Punct
&& let Some(Level::Code { braces }) = self.xml.last_mut()
{
match text {
"{" => *braces = braces.saturating_add(1),
"}" => *braces = braces.saturating_sub(1),
_ => {}
}
}
}
}
Some(Token { kind, text, line, problem, stmt, expr, start })
}
}
+251
View File
@@ -0,0 +1,251 @@
//! Static checker for scripts written for the reference app's scripting: `.jsx`, `.jsxinc` and
//! `.js` files, and compiled `.jsxbin` files. [`check`] reads a script's text without running any
//! of it and reports what kind of file it is ([`Kind`]), its preprocessor directives, the platform
//! features and document-model member chains it uses, what it would do outside the open document,
//! the menu-command strings it passes, the files it includes and the problems that would stop it
//! loading.
//!
//! The lexer reads ECMAScript 3 (ECMA-262 3rd edition,
//! <https://ecma-international.org/wp-content/uploads/ECMA-262_3rd_edition_december_1999.pdf>) with
//! the XML literals and operators of E4X (ECMA-357 2nd edition,
//! <https://ecma-international.org/wp-content/uploads/ECMA-357_2nd_edition_december_2005.pdf>), and
//! it reads later syntax (template literals, `=>`) only to report it. The directive and platform
//! names come from the public pages `catalog.rs` cites by URL; only the names are used. Nothing
//! maps the reference app's names to VectorCraft commands.
//!
//! The checker never recurses: it reads each file once, and each scan of a file goes through its
//! tokens front to back, first for the names its functions and its global code declare and then
//! for the analysis. When it follows includes, it scans each file for the includes and what the
//! variables hold, again while that grows (at most three more times), and then for the report. It
//! keeps open brackets and the levels of an XML literal on capped stacks. Its work is linear in the
//! input's size, and no input can overflow the stack.
#![forbid(unsafe_code)]
mod analyze;
mod catalog;
mod directives;
mod includes;
mod lex;
mod report;
pub use includes::{Loaded, Loader};
pub use report::{Directive, Feature, Include, IncludeStatus, MenuString, Named, Outside, Problem, Report, Target};
/// The limits that keep a check small and quick; [`Limits::default`] holds the ones VectorCraft
/// uses.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Limits {
/// The most bytes of one file read: a larger script is [`Kind::TooLarge`], and a larger
/// included file isn't read.
pub file_bytes: usize,
/// The most bytes read from the script and its included files together.
pub total_bytes: usize,
/// The most included files read.
pub includes: usize,
/// How deep includes are followed: a file the script includes is at depth 1.
pub include_depth: usize,
/// The most paths looked up for included files, whether a file is found there or not.
pub lookups: usize,
/// The most open brackets tracked, and the most levels an XML literal is read to (its markup,
/// the code in a `{…}` inside it, an XML literal in that code). Deeper nesting is a problem, and
/// the check goes on.
pub nesting: usize,
/// The most entries in each list of the report, and in each table of names the check keeps (the
/// variables holding a member chain, the variables holding a File or Folder object, the names
/// one function declares, and the functions the global code declares).
pub entries: usize,
/// The most line numbers listed for one entry.
pub lines: usize,
/// The most characters of text echoed from a script (names, values, member chains).
pub echo: usize,
}
impl Default for Limits {
fn default() -> Self {
Self {
file_bytes: 8 << 20,
total_bytes: 32 << 20,
includes: 64,
include_depth: 16,
lookups: 1024,
nesting: 4096,
entries: 1000,
lines: 20,
echo: 120,
}
}
}
/// What kind of file a script is.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
/// Text that reads as a script.
Script,
/// A compiled script (`.jsxbin`, or text that starts with `@JSXBIN@`). It isn't read.
Compiled,
/// A JavaScript module (top-level `import` or `export`), such as a React `.jsx` file: not a
/// script for the reference app.
Module,
/// Not text: a NUL byte, or more than one byte in ten a control character, in the first 8 KiB.
NotText,
/// Larger than [`Limits::file_bytes`].
TooLarge,
}
impl Kind {
pub fn id(self) -> &'static str {
match self {
Kind::Script => "script",
Kind::Compiled => "compiled",
Kind::Module => "module",
Kind::NotText => "notText",
Kind::TooLarge => "tooLarge",
}
}
}
/// A kind of help a script needs from the app that runs it.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Need {
/// The script language's own objects and directives (`$`, `alert`, `UnitValue`, `#target`).
Language,
/// The reference app's document model (`app.activeDocument…`).
Dom,
/// Files and folders.
Files,
/// Menu commands run by name.
MenuCommands,
/// Dialogs and windows of its own.
Dialogs,
/// Features only the reference app has (palettes, messages to other apps, network
/// connections, native code, E4X, actions).
HostOnly,
}
impl Need {
pub const ALL: [Need; 6] = [Need::Language, Need::Dom, Need::Files, Need::MenuCommands, Need::Dialogs, Need::HostOnly];
pub fn id(self) -> &'static str {
match self {
Need::Language => "language",
Need::Dom => "dom",
Need::Files => "files",
Need::MenuCommands => "menuCommands",
Need::Dialogs => "dialogs",
Need::HostOnly => "hostOnly",
}
}
}
/// Every feature id a report can list, in the order the report lists them.
pub use catalog::FEATURES;
/// Every id in [`Report::outside`], in the order the report lists them.
pub use report::OUTSIDE;
/// Every problem code a report can list.
pub use report::PROBLEMS;
/// Check the script `bytes`, named `name` (a file name; only its last part is used). With a
/// `loader`, the files its `#include` lines name are read through it and checked too; without one
/// they are listed as [`IncludeStatus::NotRead`]. When the script or one of those files assigns a
/// member chain or a File object to a variable, the check treats the variable as holding it in
/// every file. Nothing in the script runs.
pub fn check(name: &str, bytes: &[u8], loader: Option<&mut dyn Loader>, limits: &Limits) -> Report {
let mut acc = report::Acc::new(limits);
if bytes.len() > limits.file_bytes {
return acc.finish(Kind::TooLarge, 0);
}
let name = base_name(name);
if is_compiled(name, bytes) {
return acc.finish(Kind::Compiled, 0);
}
if !is_text(bytes) {
return acc.finish(Kind::NotText, 0);
}
let (text, invalid) = decode(bytes);
if invalid {
acc.problem(report::NOT_UTF8, None, None, None);
}
let none = analyze::Known::default();
// With a loader, this first scan finds the includes and what the script's variables hold; its
// tallies are left out.
let mut tallies = report::Acc::new(limits);
let scan = analyze::scan(&text, None, std::rc::Rc::from([]), &none, if loader.is_some() { &mut tallies } else { &mut acc });
acc.steps = acc.steps.saturating_add(tallies.steps);
if scan.module {
// A module's report holds its kind and its lines, and none of what the scan found.
let steps = acc.steps;
let mut acc = report::Acc::new(limits);
acc.steps = steps;
return acc.finish(Kind::Module, scan.lines);
}
let Some(loader) = loader else {
includes::not_read(&mut acc, scan.includes);
return acc.finish(Kind::Script, scan.lines);
};
let mut known = scan.known;
let read = includes::follow(&mut acc, scan.includes, loader, name, bytes.len(), &mut known);
// A variable may get what it holds from another file's variable: the files are scanned again,
// each with what is known so far, while that grows, at most three more times.
for _ in 0..3 {
let before = known.len();
let mut tallies = report::Acc::new(limits);
let more = analyze::scan(&text, None, std::rc::Rc::from([]), &known, &mut tallies).known;
known.add(more, limits.entries);
for file in &read {
let more = analyze::scan(&file.text, Some(&file.path), std::rc::Rc::clone(&file.paths), &known, &mut tallies).known;
known.add(more, limits.entries);
}
acc.steps = acc.steps.saturating_add(tallies.steps);
if known.len() == before {
break;
}
}
// The scans for the report: every file starts with what any file's variables hold.
let lines = analyze::scan(&text, None, std::rc::Rc::from([]), &known, &mut acc).lines;
for file in read {
if file.invalid {
acc.problem(report::NOT_UTF8, None, Some(&file.path), None);
}
analyze::scan(&file.text, Some(&file.path), file.paths, &known, &mut acc);
}
acc.finish(Kind::Script, lines)
}
/// The last part of a path (`lib/Tidy.jsx` → `Tidy.jsx`), whichever separator it uses.
pub(crate) fn base_name(name: &str) -> &str {
name.rsplit(['/', '\\']).next().unwrap_or(name)
}
/// A compiled script: named `.jsxbin`, or text that starts with `@JSXBIN@` (after a byte order mark
/// and blanks).
pub(crate) fn is_compiled(name: &str, bytes: &[u8]) -> bool {
if base_name(name).rsplit_once('.').is_some_and(|(_, ext)| ext.eq_ignore_ascii_case("jsxbin")) {
return true;
}
let b = bytes.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(bytes);
let start = b.iter().position(|c| !c.is_ascii_whitespace()).unwrap_or(b.len());
b.get(start..).is_some_and(|b| b.starts_with(b"@JSXBIN@"))
}
/// Text, judged by its first 8 KiB: no NUL byte, and at most one byte in ten a control character
/// other than tab, line feed, vertical tab, form feed, carriage return and escape.
pub(crate) fn is_text(bytes: &[u8]) -> bool {
let head = bytes.get(..bytes.len().min(8 << 10)).unwrap_or(bytes);
if head.contains(&0) {
return false;
}
let controls = head.iter().filter(|&&b| b < 0x20 && !matches!(b, b'\t' | b'\n' | b'\r' | 0x0b | 0x0c | 0x1b)).count();
controls * 10 <= head.len()
}
/// The text of `bytes`: UTF-8 after an optional byte order mark, with invalid bytes replaced by
/// U+FFFD (`true` when there were any).
pub(crate) fn decode(bytes: &[u8]) -> (std::borrow::Cow<'_, str>, bool) {
let b = bytes.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(bytes);
let text = String::from_utf8_lossy(b);
let invalid = matches!(text, std::borrow::Cow::Owned(_));
(text, invalid)
}
#[cfg(test)]
mod tests;
+417
View File
@@ -0,0 +1,417 @@
//! What a check finds ([`Report`]), and the tallies it is built from ([`Acc`]), each list capped.
use std::collections::{BTreeMap, BTreeSet};
use crate::{Kind, Limits, Need, catalog};
pub(crate) const UNTERMINATED_STRING: &str = "unterminatedString";
pub(crate) const UNTERMINATED_COMMENT: &str = "unterminatedComment";
pub(crate) const UNTERMINATED_REGEXP: &str = "unterminatedRegExp";
pub(crate) const UNTERMINATED_TEMPLATE: &str = "unterminatedTemplate";
pub(crate) const UNTERMINATED_XML: &str = "unterminatedXml";
pub(crate) const UNCLOSED_BRACKET: &str = "unclosedBracket";
pub(crate) const UNEXPECTED_BRACKET: &str = "unexpectedBracket";
pub(crate) const NEWER_SYNTAX: &str = "newerSyntax";
pub(crate) const UNKNOWN_DIRECTIVE: &str = "unknownDirective";
pub(crate) const NOT_UTF8: &str = "notUtf8";
pub(crate) const MIXED_LINE_ENDINGS: &str = "mixedLineEndings";
pub(crate) const TOO_DEEP: &str = "tooDeep";
/// Every problem code a report can list.
pub const PROBLEMS: &[&str] = &[
UNTERMINATED_STRING,
UNTERMINATED_COMMENT,
UNTERMINATED_REGEXP,
UNTERMINATED_TEMPLATE,
UNTERMINATED_XML,
UNCLOSED_BRACKET,
UNEXPECTED_BRACKET,
NEWER_SYNTAX,
UNKNOWN_DIRECTIVE,
NOT_UTF8,
MIXED_LINE_ENDINGS,
TOO_DEEP,
];
/// Every id in [`Report::outside`], in the order the report lists them: files written, moved or
/// deleted; files opened in other apps; network connections; native code; messages to other apps;
/// environment variables; code the checker can't read; the reference app's preferences changed.
pub const OUTSIDE: &[&str] = &["filesWritten", "filesOpened", "network", "nativeCode", "otherApps", "environment", "unseenCode", "preferences"];
/// What [`crate::check`] found in a script. Lists of located things (targets, directives,
/// includes, problems) give a line and, for an included file, its path (`file`); tallies (features,
/// menu strings, outside effects) count the script and its included files together and list the
/// script's own lines.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Report {
pub kind: Kind,
/// What the script needs from the app that runs it, in [`Need`]'s order.
pub needs: Vec<Need>,
/// The script's lines.
pub lines: usize,
/// Each `#target` line's application specifier.
pub targets: Vec<Target>,
pub directives: Vec<Directive>,
pub includes: Vec<Include>,
/// The platform features used, by id ([`crate::FEATURES`]).
pub features: Vec<Feature>,
/// The document-model member chains used, as written (`app.activeDocument.layers[].name`, with
/// `[]` for an index and `()` for a call), a variable assigned from a chain replaced by that
/// chain, and how often each occurs; the most used first.
pub members: Vec<Named>,
/// The type names compared with `.typename` or created with `new`, apart from the language's,
/// the platform's and the script's own functions.
pub types: Vec<Named>,
/// The menu-command strings passed as string literals; the most used first.
pub menu_strings: Vec<MenuString>,
/// Menu-command calls whose argument isn't a string literal.
pub menu_dynamic: usize,
/// What the script does outside the document, in [`OUTSIDE`]'s order.
pub outside: Vec<Outside>,
pub problems: Vec<Problem>,
/// A list of the report, or a table of names the check keeps, reached [`Limits::entries`], and
/// entries were left out.
pub truncated: bool,
/// The steps the check took: the tests check that they grow linearly with the input.
pub(crate) steps: usize,
}
/// A `#target` line's application specifier, split into its parts.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Target {
pub value: String,
pub app: String,
pub instance: Option<String>,
pub version: Option<String>,
pub locale: Option<String>,
pub line: usize,
pub file: Option<String>,
}
/// A preprocessor line of a documented directive.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Directive {
/// The directive (`target`, `include`…).
pub name: &'static str,
pub value: String,
pub line: usize,
/// `#` or `//@`.
pub form: &'static str,
pub file: Option<String>,
}
/// What became of an `#include` line.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IncludeStatus {
/// Read and checked (or checked already, for a file included twice).
Checked,
/// No such file in the script's folder, or in the folders `#includepath` names inside it.
Missing,
/// Not followed: an absolute name, a name longer than 4,096 bytes, a name that leads out of the
/// script's folder, or a file the caller may not read.
NotFollowed,
/// The file it names includes the file the line is in, directly or through other files.
Cycle,
/// Includes weren't read (no loader).
NotRead,
/// Not read: a limit was reached (files, depth, size, paths looked up).
Limit,
/// A compiled file, or not text.
Unreadable,
}
impl IncludeStatus {
pub const ALL: [IncludeStatus; 7] = [
IncludeStatus::Checked,
IncludeStatus::Missing,
IncludeStatus::NotFollowed,
IncludeStatus::Cycle,
IncludeStatus::NotRead,
IncludeStatus::Limit,
IncludeStatus::Unreadable,
];
pub fn id(self) -> &'static str {
match self {
IncludeStatus::Checked => "checked",
IncludeStatus::Missing => "missing",
IncludeStatus::NotFollowed => "notFollowed",
IncludeStatus::Cycle => "cycle",
IncludeStatus::NotRead => "notRead",
IncludeStatus::Limit => "limit",
IncludeStatus::Unreadable => "unreadable",
}
}
}
/// An `#include` line and what became of it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Include {
/// The name as the line writes it.
pub value: String,
pub line: usize,
/// The included file the line is in.
pub file: Option<String>,
pub status: IncludeStatus,
/// The file it names, as a path inside the script's folder, when one was found.
pub path: Option<String>,
}
/// A platform feature: how often it occurs, and the script's first lines with it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Feature {
pub id: &'static str,
pub count: usize,
pub lines: Vec<usize>,
}
/// A name and how often it occurs.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Named {
pub name: String,
pub count: usize,
}
/// A menu-command string: how often it is passed, and the script's first lines with it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MenuString {
pub value: String,
pub count: usize,
pub lines: Vec<usize>,
}
/// Something the script does outside the document ([`OUTSIDE`]).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Outside {
pub id: &'static str,
pub count: usize,
pub lines: Vec<usize>,
/// For `filesWritten`: the open modes used (`w`, `a`, `e`).
pub modes: Vec<&'static str>,
}
/// A problem that would stop the script loading or running as written.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Problem {
pub code: &'static str,
pub line: Option<usize>,
pub file: Option<String>,
/// The text concerned, as written (an unknown directive, a construct of later syntax, a
/// bracket).
pub detail: Option<String>,
}
/// A count and the first lines it was seen on.
#[derive(Clone, Debug, Default)]
struct Tally {
count: usize,
lines: Vec<usize>,
}
impl Tally {
fn add(&mut self, line: Option<usize>, max: usize) {
self.count = self.count.saturating_add(1);
if let Some(line) = line
&& self.lines.len() < max
&& self.lines.last() != Some(&line)
{
self.lines.push(line);
}
}
}
/// What a check has found so far, across the script and its included files.
pub(crate) struct Acc<'l> {
pub limits: &'l Limits,
pub steps: usize,
features: BTreeMap<&'static str, Tally>,
outside: BTreeMap<&'static str, (Tally, Vec<&'static str>)>,
menu: BTreeMap<String, Tally>,
menu_dynamic: usize,
members: BTreeMap<String, usize>,
types: BTreeMap<String, usize>,
/// Functions the script declares: `new` of one is no document-model type.
declared: BTreeSet<String>,
targets: Vec<Target>,
directives: Vec<Directive>,
includes: Vec<Include>,
problems: Vec<Problem>,
truncated: bool,
}
impl<'l> Acc<'l> {
pub fn new(limits: &'l Limits) -> Self {
Self {
limits,
steps: 0,
features: BTreeMap::new(),
outside: BTreeMap::new(),
menu: BTreeMap::new(),
menu_dynamic: 0,
members: BTreeMap::new(),
types: BTreeMap::new(),
declared: BTreeSet::new(),
targets: vec![],
directives: vec![],
includes: vec![],
problems: vec![],
truncated: false,
}
}
/// `s` as the report echoes it: at most [`Limits::echo`] characters, the last one `…` when
/// it was cut.
pub fn echo(&self, s: &str) -> String {
echo(s, self.limits.echo)
}
/// Room for one more entry in a list of the report, or in a table of names the check keeps, of
/// `len` entries: false (and the report marked truncated) when it is full.
pub fn room(&mut self, len: usize) -> bool {
let room = len < self.limits.entries;
self.truncated |= !room;
room
}
pub fn feature(&mut self, id: &'static str, line: Option<usize>) {
let max = self.limits.lines;
self.features.entry(id).or_default().add(line, max);
}
pub fn outside(&mut self, id: &'static str, line: Option<usize>, mode: Option<&'static str>) {
let max = self.limits.lines;
let (tally, modes) = self.outside.entry(id).or_default();
tally.add(line, max);
if let Some(m) = mode
&& !modes.contains(&m)
{
modes.push(m);
}
}
pub fn menu_string(&mut self, value: &str, line: Option<usize>) {
let key = self.echo(value);
if !self.menu.contains_key(&key) && !self.room(self.menu.len()) {
return;
}
let max = self.limits.lines;
self.menu.entry(key).or_default().add(line, max);
}
pub fn menu_dynamic(&mut self) {
self.menu_dynamic = self.menu_dynamic.saturating_add(1);
}
pub fn member(&mut self, path: &str) {
if !self.members.contains_key(path) && !self.room(self.members.len()) {
return;
}
let n = self.members.entry(path.to_string()).or_default();
*n = n.saturating_add(1);
}
pub fn type_name(&mut self, name: &str) {
let key = self.echo(name);
if !self.types.contains_key(&key) && !self.room(self.types.len()) {
return;
}
let n = self.types.entry(key).or_default();
*n = n.saturating_add(1);
}
pub fn declared(&mut self, name: &str) {
if self.declared.len() < self.limits.entries {
self.declared.insert(self.echo(name));
}
}
pub fn problem(&mut self, code: &'static str, line: Option<usize>, file: Option<&str>, detail: Option<&str>) {
if self.room(self.problems.len()) {
let detail = detail.map(|d| self.echo(d));
self.problems.push(Problem { code, line, file: file.map(str::to_string), detail });
}
}
pub fn directive(&mut self, name: &'static str, value: &str, line: usize, form: &'static str, file: Option<&str>) {
if self.room(self.directives.len()) {
let value = self.echo(value);
self.directives.push(Directive { name, value, line, form, file: file.map(str::to_string) });
}
}
pub fn target(&mut self, value: &str, line: usize, file: Option<&str>) {
if !self.room(self.targets.len()) {
return;
}
let t = crate::directives::target(value);
let part = |s: Option<&str>| s.map(|s| echo(s, self.limits.echo));
let target = Target {
value: self.echo(value),
app: self.echo(t.app),
instance: part(t.instance),
version: part(t.version),
locale: part(t.locale),
line,
file: file.map(str::to_string),
};
self.targets.push(target);
}
pub fn include(&mut self, value: &str, line: usize, file: Option<&str>, status: IncludeStatus, path: Option<&str>) {
if self.room(self.includes.len()) {
let include = Include { value: self.echo(value), line, file: file.map(str::to_string), status, path: path.map(|p| self.echo(p)) };
self.includes.push(include);
}
}
/// The report for a file of `kind` with `lines` lines.
pub fn finish(self, kind: Kind, lines: usize) -> Report {
let declared = self.declared;
let mut members: Vec<Named> = self.members.into_iter().map(|(name, count)| Named { name, count }).collect();
members.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.name.cmp(&b.name)));
let mut types: Vec<Named> =
self.types.into_iter().filter(|(name, _)| !declared.contains(name)).map(|(name, count)| Named { name, count }).collect();
types.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.name.cmp(&b.name)));
let mut menu_strings: Vec<MenuString> =
self.menu.into_iter().map(|(value, t)| MenuString { value, count: t.count, lines: t.lines }).collect();
menu_strings.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.value.cmp(&b.value)));
let features: Vec<Feature> = self.features.into_iter().map(|(id, t)| Feature { id, count: t.count, lines: t.lines }).collect();
let mut outside: Vec<Outside> =
self.outside.into_iter().map(|(id, (t, modes))| Outside { id, count: t.count, lines: t.lines, modes }).collect();
outside.sort_by_key(|o| OUTSIDE.iter().position(|id| *id == o.id));
let mut needs: BTreeSet<Need> = features.iter().filter_map(|f| catalog::need(f.id)).collect();
if !members.is_empty() || !types.is_empty() {
needs.insert(Need::Dom);
}
Report {
kind,
needs: needs.into_iter().collect(),
lines,
targets: self.targets,
directives: self.directives,
includes: self.includes,
features,
members,
types,
menu_strings,
menu_dynamic: self.menu_dynamic,
outside,
problems: self.problems,
truncated: self.truncated,
steps: self.steps,
}
}
}
/// `s` cut to at most `max` characters, the last one `…` when it was cut.
pub(crate) fn echo(s: &str, max: usize) -> String {
match s.char_indices().nth(max) {
None => s.to_string(),
Some(_) => {
let keep = s.char_indices().nth(max.saturating_sub(1)).map_or(0, |(i, _)| i);
let mut out = s.get(..keep).unwrap_or_default().to_string();
out.push('…');
out
}
}
}
File diff suppressed because it is too large. Load diff
+2
View File
@@ -78,6 +78,7 @@ pub(crate) mod save_options;
mod save_pdf;
pub mod save_style_library;
pub mod save_swatch_library;
pub mod script_check;
mod shapes;
pub mod slices;
pub mod spot_colors;
@@ -285,6 +286,7 @@ registry! {
PrintPreset: [print::PRESET_KIND] => print::PRESET_SPEC,
PrintPresets: [print_presets::KIND] => print_presets::SPEC,
Plugin: [plugin::KIND] => plugin::SPEC,
ScriptCheck: [script_check::KIND] => script_check::SPEC,
VectorHalftone: [halftone::KIND] => halftone::SPEC,
PerspectiveGrid: [perspective_grid::KIND] => perspective_grid::SPEC,
Envelope: [envelope::WARP, envelope::MESH, envelope::OPTIONS] => envelope::SPEC,
+288
View File
@@ -0,0 +1,288 @@
//! File › Scripts › Check Script…: the report `script.check` gives on a script written for the
//! reference app, read without running it: what kind of file it is and what it needs, what it does
//! outside the document, the menu commands it runs, the document-model members it uses, its
//! directives and includes, and its problems, with Copy Report (the report as JSON) and Close.
//! `ui.checkScript {path? | data? | dataBase64?, name?}` opens it; a path also checks the files
//! the script's `#include` lines name. Without a file it asks for one (the web opens the browser's file
//! picker), and File › Open of a `.jsx`, `.jsxinc` or `.jsxbin` file opens it too.
//!
//! Fields: `report` (`script.check`'s result) and `discard` (set it and confirm for Copy Report;
//! confirming copies the report either way).
use serde_json::{Value, json};
use super::DialogSpec;
use crate::state::Dialog;
use crate::theme::{self, Tokens};
use crate::{FilePick, VectorcraftApp, io, widgets};
/// The dialog kind of a script's report.
pub const KIND: &str = "scriptCheck";
pub(super) const SPEC: DialogSpec = DialogSpec {
heading: |_| tl!("Check Script").into(),
body,
confirm,
// Close, with Copy Report beside it.
ok: None,
discard: Some("Copy Report"),
min_width: 460.0,
max_width: Some(560.0),
..DialogSpec::FORM
};
/// `ui.checkScript`: check the script at `path` (and the files it includes) or given as `data` or
/// `dataBase64`, and show the report; without any of them, pick a `.jsx`, `.jsxinc` or `.jsxbin`
/// file (the web opens File › Open's picker, and [`io::open_bytes`] shows the report).
pub(crate) fn command(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let mut params = p.clone();
if ["path", "data", "dataBase64"].iter().all(|k| p.get(k).is_none()) {
if app.services.open_async.is_some() {
return io::open_dialog(app).map(|_| Value::Null);
}
let pick = FilePick { filters: vec![("Scripts", vectorcraft_engine::cmd::scripts::EXTS)], ..Default::default() };
params = json!({ "path": crate::picks::open(app, &pick).ok_or("cancelled")? });
}
if params.get("path").is_some()
&& let Some(o) = params.as_object_mut()
{
o.entry("includes").or_insert(json!(true));
}
let r = app.run("script.check", params)?;
app.ui.dialog = Some(Dialog::new(KIND, json!({ "report": r })));
Ok(r)
}
/// Copy Report: the report as JSON on the clipboard. The dialog stays open.
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let report = d.fields.get("report").cloned().unwrap_or(Value::Null);
app.clipboard_out = Some(serde_json::to_string_pretty(&report).map_err(|e| e.to_string())?);
if let Some(open) = app.ui.dialog.as_mut().filter(|open| open.kind == KIND) {
open.fields.remove("discard");
}
app.status("Copied the script report");
Ok(report)
}
/// What need `id` means for the person reading the report.
pub(crate) fn need_label(id: &str) -> Option<&'static str> {
Some(match id {
"language" => tl!("Needs the script language's own objects or directives"),
"dom" => tl!("Needs the document model"),
"files" => tl!("Needs files"),
"menuCommands" => tl!("Needs menu commands"),
"dialogs" => tl!("Needs dialogs"),
"hostOnly" => tl!("Needs features only the reference app has"),
_ => return None,
})
}
pub(crate) fn outside_label(id: &str) -> Option<&'static str> {
Some(match id {
"filesWritten" => tl!("Writes, moves or deletes files"),
"filesOpened" => tl!("Opens files in other apps"),
"network" => tl!("Opens network connections"),
"nativeCode" => tl!("Loads native code"),
"otherApps" => tl!("Sends messages to other apps"),
"environment" => tl!("Reads or sets environment variables"),
"unseenCode" => tl!("Runs code the checker can't read"),
"preferences" => tl!("Changes the reference app's preferences"),
_ => return None,
})
}
pub(crate) fn status_label(id: &str) -> Option<&'static str> {
Some(match id {
"checked" => tl!("Checked"),
"missing" => tl!("Not found"),
"notFollowed" => tl!("Not followed: outside the script's folder"),
"cycle" => tl!("Includes itself"),
"notRead" => tl!("Not read"),
"limit" => tl!("Not read: a limit was reached"),
"unreadable" => tl!("Compiled or not text"),
_ => return None,
})
}
pub(crate) fn problem_label(code: &str) -> Option<&'static str> {
Some(match code {
"unterminatedString" => tl!("String not closed"),
"unterminatedComment" => tl!("Comment not closed"),
"unterminatedRegExp" => tl!("Regular expression not closed"),
"unterminatedTemplate" => tl!("Template literal not closed"),
"unterminatedXml" => tl!("XML literal not closed"),
"unclosedBracket" => tl!("Bracket never closed"),
"unexpectedBracket" => tl!("Closing bracket with no opening one"),
"newerSyntax" => tl!("Syntax newer than ES3"),
"unknownDirective" => tl!("Unknown directive"),
"notUtf8" => tl!("Not valid UTF-8: unreadable bytes were replaced"),
"mixedLineEndings" => tl!("Mixed line endings"),
"tooDeep" => tl!("Nesting too deep to follow"),
_ => return None,
})
}
/// Where something is: `line 12`, or `lib/a.jsxinc, line 3` in an included file.
fn place(item: &Value) -> String {
let line = item["line"].as_u64().map(|l| l.to_string());
match (item["file"].as_str(), line) {
(Some(file), Some(line)) => crate::i18n::fmt(tl!("{file}, line {line}"), &[("file", file), ("line", &line)]),
(Some(file), None) => file.to_string(),
(None, Some(line)) => crate::i18n::fmt(tl!("line {line}"), &[("line", &line)]),
(None, None) => String::new(),
}
}
/// The script's lines an entry lists: `line 4`, `lines 4, 9, 12`.
fn lines(item: &Value) -> String {
let lines: Vec<String> = item["lines"].as_array().into_iter().flatten().filter_map(Value::as_u64).map(|l| l.to_string()).collect();
match lines.as_slice() {
[] => String::new(),
[line] => crate::i18n::fmt(tl!("line {line}"), &[("line", line)]),
_ => crate::i18n::fmt(tl!("lines {lines}"), &[("lines", &lines.join(", "))]),
}
}
fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
let Some(r) = d.fields.get("report") else { return true };
let t = Tokens::get(ui.ctx());
ui.label(egui::RichText::new(r["name"].as_str().unwrap_or_default()).font(theme::semibold(12.5)).color(t.text_strong));
ui.add_space(4.0);
match r["kind"].as_str() {
Some("script") => report(ui, r, &t),
Some("compiled") => {
widgets::dim_label(ui, tl!("Compiled script: ask its author for the .jsx file"));
}
Some("tooLarge") => {
widgets::dim_label(ui, tl!("Too large to check"));
}
_ => {
widgets::dim_label(ui, tl!("Not a script for the reference app"));
}
}
false
}
/// A row of a section: `text` (user data in `mono`), then a count and lines, dimmed.
fn row(ui: &mut egui::Ui, t: &Tokens, text: &str, mono: bool, note: &str) {
ui.horizontal_wrapped(|ui| {
let text = egui::RichText::new(text).color(t.text_strong);
ui.label(if mono { text.monospace() } else { text.size(12.5) });
if !note.is_empty() {
ui.label(egui::RichText::new(note).size(11.5).color(t.text_dim));
}
});
}
/// `count` and `lines` of an entry as a note: `3 · lines 4, 9, 12`.
fn tally(item: &Value) -> String {
let count = item["count"].as_u64().unwrap_or_default();
match lines(item) {
l if l.is_empty() => count.to_string(),
l => format!("{count} · {l}"),
}
}
fn section(ui: &mut egui::Ui, heading: &str, rows: impl FnOnce(&mut egui::Ui)) {
ui.add_space(8.0);
widgets::section_header(ui, heading);
rows(ui);
}
fn list<'r>(r: &'r Value, key: &str) -> &'r [Value] {
r[key].as_array().map_or(&[][..], Vec::as_slice)
}
fn report(ui: &mut egui::Ui, r: &Value, t: &Tokens) {
widgets::dim_label(ui, tl!("VectorCraft read this script without running it."));
let needs: Vec<&str> = list(r, "needs").iter().filter_map(Value::as_str).filter_map(need_label).collect();
if needs.is_empty() {
row(ui, t, tl!("Needs nothing beyond plain JavaScript"), false, "");
}
for need in needs {
row(ui, t, need, false, "");
}
widgets::list_box(ui, |ui| {
egui::ScrollArea::vertical().max_height(320.0).show(ui, |ui| {
ui.set_width(ui.available_width());
let outside = list(r, "outside");
if !outside.is_empty() {
section(ui, tl!("Outside the document"), |ui| {
for o in outside {
let Some(label) = o["id"].as_str().and_then(outside_label) else { continue };
let modes: Vec<&str> = list(o, "modes").iter().filter_map(Value::as_str).collect();
let label = if modes.is_empty() { label.to_string() } else { format!("{label} ({})", modes.join(", ")) };
row(ui, t, &label, false, &tally(o));
}
});
}
let menu = &r["menuCommands"];
let dynamic = menu["dynamic"].as_u64().unwrap_or_default();
if !list(menu, "strings").is_empty() || dynamic > 0 {
section(ui, tl!("Menu commands"), |ui| {
for s in list(menu, "strings") {
row(ui, t, s["value"].as_str().unwrap_or_default(), true, &tally(s));
}
if dynamic > 0 {
let count = dynamic.to_string();
row(ui, t, &crate::i18n::fmt(tl!("Names the script builds as it runs: {count}"), &[("count", &count)]), false, "");
}
});
}
let dom = &r["dom"];
let types: Vec<&str> = list(dom, "types").iter().filter_map(|n| n["name"].as_str()).collect();
if !list(dom, "members").is_empty() || !types.is_empty() {
section(ui, tl!("Document model"), |ui| {
for m in list(dom, "members") {
row(ui, t, m["path"].as_str().unwrap_or_default(), true, &m["count"].as_u64().unwrap_or_default().to_string());
}
if !types.is_empty() {
row(ui, t, &crate::i18n::fmt(tl!("Object types: {names}"), &[("names", &types.join(", "))]), false, "");
}
});
}
let directives = list(r, "directives");
if !directives.is_empty() {
section(ui, tl!("Directives"), |ui| {
for d in directives {
let text = format!(
"{}{} {}",
d["form"].as_str().unwrap_or_default(),
d["name"].as_str().unwrap_or_default(),
d["value"].as_str().unwrap_or_default()
);
row(ui, t, text.trim_end(), true, &place(d));
}
});
}
let includes = list(r, "includes");
if !includes.is_empty() {
section(ui, tl!("Includes"), |ui| {
for i in includes {
let status = i["status"].as_str().and_then(status_label).unwrap_or_default();
let note = [status.to_string(), place(i)].into_iter().filter(|s| !s.is_empty()).collect::<Vec<_>>().join(" · ");
row(ui, t, i["value"].as_str().unwrap_or_default(), true, &note);
}
});
}
let problems = list(r, "problems");
if !problems.is_empty() {
section(ui, tl!("Problems"), |ui| {
for p in problems {
let Some(label) = p["code"].as_str().and_then(problem_label) else { continue };
let label = match p["detail"].as_str() {
Some(detail) => format!("{label}: {detail}"),
None => label.to_string(),
};
row(ui, t, &label, false, &place(p));
}
});
}
ui.add_space(6.0);
});
});
if r["truncated"].as_bool() == Some(true) {
ui.add_space(6.0);
widgets::dim_label(ui, tl!("Some lists were cut short at their limit."));
}
}
+54
View File
@@ -61,6 +61,8 @@
Export Selection… Exportovat výběr…
Package… Sbalit…
Scripts Skripty
Check Script… Zkontrolovat skript…
Check Script Zkontrolovat skript
Other Script… Jiný skript…
Document Setup… Nastavení dokumentu…
Document Color Mode Barevný režim dokumentu
@@ -2476,6 +2478,56 @@ lighting Light Světlo
Selected Swatches Only Pouze vybrané položky vzorníku
Selected Swatches Only (0) Pouze vybrané položky vzorníku (0)
User Defined Libraries Uživatelské knihovny
Copy Report Kopírovat zprávu
VectorCraft read this script without running it. VectorCraft tento skript přečetl, aniž by ho spustil.
Needs nothing beyond plain JavaScript Nepotřebuje nic kromě běžného JavaScriptu
Needs the script language's own objects or directives Potřebuje vlastní objekty nebo direktivy skriptovacího jazyka
Needs the document model Potřebuje model dokumentu
Needs files Potřebuje soubory
Needs menu commands Potřebuje příkazy nabídky
Needs dialogs Potřebuje dialogová okna
Needs features only the reference app has Potřebuje funkce, které má jen referenční aplikace
Outside the document Mimo dokument
Writes, moves or deletes files Zapisuje, přesouvá nebo maže soubory
Opens files in other apps Otevírá soubory v jiných aplikacích
Opens network connections Otevírá síťová připojení
Loads native code Načítá nativní kód
Sends messages to other apps Posílá zprávy jiným aplikacím
Reads or sets environment variables Čte nebo nastavuje proměnné prostředí
Runs code the checker can't read Spouští kód, který kontrola nedokáže přečíst
Changes the reference app's preferences Mění předvolby referenční aplikace
Menu commands Příkazy nabídky
Names the script builds as it runs: {count} Názvy, které skript sestaví až za běhu: {count}
Document model Model dokumentu
Object types: {names} Typy objektů: {names}
Directives Direktivy
Includes Vložené soubory
Checked Zkontrolováno
Not followed: outside the script's folder Přeskočeno: mimo složku skriptu
Includes itself Vkládá sám sebe
Not read Nenačteno
Not read: a limit was reached Nenačteno: byl dosažen limit
Compiled or not text Kompilovaný nebo netextový soubor
Problems Problémy
String not closed Neuzavřený řetězec
Comment not closed Neuzavřený komentář
Regular expression not closed Neuzavřený regulární výraz
Template literal not closed Neuzavřený šablonový literál
XML literal not closed Neuzavřený literál XML
Bracket never closed Neuzavřená závorka
Closing bracket with no opening one Uzavírací závorka bez otevírací
Syntax newer than ES3 Syntaxe novější než ES3
Unknown directive Neznámá direktiva
Not valid UTF-8: unreadable bytes were replaced Neplatné UTF-8: nečitelné bajty byly nahrazeny
Mixed line endings Smíšené konce řádků
Nesting too deep to follow Vnoření je příliš hluboké na sledování
line {line} řádek {line}
lines {lines} řádky {lines}
{file}, line {line} {file}, řádek {line}
Compiled script: ask its author for the .jsx file Kompilovaný skript: požádejte jeho autora o soubor .jsx
Too large to check Příliš velký na kontrolu
Not a script for the reference app Nejde o skript pro referenční aplikaci
Some lists were cut short at their limit. Některé seznamy byly zkráceny na svůj limit.
Line Segment Tool Options Možnosti nástroje Úsečka
Alt: Alt:
Divide Horizontally Into Rozdělit vodorovně na
@@ -3705,6 +3757,7 @@ imageTrace Outline Obrys
@msg Convert to Artboards: selection has no area Převést na pracovní plochy: výběr nemá žádnou plochu
@msg Convert to Shape: no rectangles or ellipses selected Převést na tvar: nejsou vybrány žádné obdélníky ani elipsy
@msg Copied the CSS of {rules} object(s) Zkopírováno CSS objektů: {rules}
@msg Copied the script report Zpráva o skriptu zkopírována
@msg Corners edits the corners of a path Rohy upravuje rohy cesty
@msg Couldn't copy to the system clipboard: {e} Nelze kopírovat do systémové schránky: {e}
@msg Couldn't open {name}: {e} {name} nelze otevřít: {e}
@@ -4195,6 +4248,7 @@ imageTrace Outline Obrys
@msg {name}: no such recovery entry {name}: taková položka obnovení neexistuje
@msg {path} can't be found: relink it first {path} nelze najít: nejprve jej znovu propojte
@msg {path}: no file system here (pass dataBase64 to open; omit the path to get dataBase64) {path}: zde není souborový systém (k otevření předejte dataBase64; vynecháním cesty dataBase64 získáte)
@msg {path}: not a file {path}: nejde o soubor
@msg {path}: {e} {path}: {e}
@msg {status} ({e}) {status} ({e})
@msg Couldn't place {name}: {e} {name} nelze umístit: {e}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+55
View File
@@ -351,6 +351,59 @@
Character Style Options Zeichenformatoptionen
Character Styles Zeichenformate
Check Links Verknüpfungen prüfen
# Check Script (File › Scripts) and the report it shows on a script written for the reference app.
Check Script Skript prüfen
Check Script… Skript prüfen…
Copy Report Bericht kopieren
VectorCraft read this script without running it. VectorCraft hat dieses Skript gelesen, ohne es auszuführen.
Needs nothing beyond plain JavaScript Braucht nur einfaches JavaScript
Needs the script language's own objects or directives Braucht eigene Objekte oder Direktiven der Skriptsprache
Needs the document model Braucht das Dokumentmodell
Needs files Braucht Dateien
Needs menu commands Braucht Menübefehle
Needs dialogs Braucht Dialogfelder
Needs features only the reference app has Braucht Funktionen, die nur die Referenzanwendung hat
Outside the document Außerhalb des Dokuments
Writes, moves or deletes files Schreibt, verschiebt oder löscht Dateien
Opens files in other apps Öffnet Dateien in anderen Apps
Opens network connections Öffnet Netzwerkverbindungen
Loads native code Lädt nativen Code
Sends messages to other apps Sendet Nachrichten an andere Apps
Reads or sets environment variables Liest oder setzt Umgebungsvariablen
Runs code the checker can't read Führt Code aus, den die Prüfung nicht lesen kann
Changes the reference app's preferences Ändert die Voreinstellungen der Referenzanwendung
Menu commands Menübefehle
Names the script builds as it runs: {count} Namen, die das Skript bei der Ausführung bildet: {count}
Document model Dokumentmodell
Object types: {names} Objekttypen: {names}
Directives Direktiven
Includes Eingebundene Dateien
Checked Geprüft
Not followed: outside the script's folder Übersprungen: außerhalb des Skriptordners
Includes itself Bindet sich selbst ein
Not read Nicht gelesen
Not read: a limit was reached Nicht gelesen: ein Grenzwert wurde erreicht
Compiled or not text Kompiliert oder kein Text
Problems Probleme
String not closed Zeichenkette nicht geschlossen
Comment not closed Kommentar nicht geschlossen
Regular expression not closed Regulärer Ausdruck nicht geschlossen
Template literal not closed Template-Literal nicht geschlossen
XML literal not closed XML-Literal nicht geschlossen
Bracket never closed Klammer nie geschlossen
Closing bracket with no opening one Schließende Klammer ohne öffnende
Syntax newer than ES3 Neuere Syntax als ES3
Unknown directive Unbekannte Direktive
Not valid UTF-8: unreadable bytes were replaced Kein gültiges UTF-8: unlesbare Bytes wurden ersetzt
Mixed line endings Gemischte Zeilenenden
Nesting too deep to follow Verschachtelung zu tief zum Verfolgen
line {line} Zeile {line}
lines {lines} Zeilen {lines}
{file}, line {line} {file}, Zeile {line}
Compiled script: ask its author for the .jsx file Kompiliertes Skript: Bitten Sie den Autor um die .jsx-Datei
Too large to check Zu groß zum Prüfen
Not a script for the reference app Kein Skript für die Referenzanwendung
Some lists were cut short at their limit. Einige Listen wurden an ihrem Grenzwert abgeschnitten.
Check Spelling… Rechtschreibprüfung…
Choose a folder Ordner wählen
Choose… Wählen…
@@ -3306,6 +3359,7 @@ theme Appearance Erscheinungsbild
@msg Convert to Artboards: selection has no area In Zeichenflächen umwandeln: Die Auswahl hat keine Fläche
@msg Convert to Shape: no rectangles or ellipses selected In Form umwandeln: keine Rechtecke oder Ellipsen ausgewählt
@msg Copied the CSS of {rules} object(s) CSS von {rules} Objekt(en) kopiert
@msg Copied the script report Skriptbericht kopiert
@msg Corners edits the corners of a path „Ecken“ bearbeitet die Ecken eines Pfads
@msg Couldn't copy to the system clipboard: {e} In die Systemzwischenablage konnte nicht kopiert werden: {e}
@msg Couldn't open {name}: {e} {name} konnte nicht geöffnet werden: {e}
@@ -3797,6 +3851,7 @@ theme Appearance Erscheinungsbild
@msg {name}: no such recovery entry {name}: kein solcher Wiederherstellungseintrag
@msg {path} can't be found: relink it first {path} wurde nicht gefunden: Verknüpfen Sie die Datei zuerst neu
@msg {path}: no file system here (pass dataBase64 to open; omit the path to get dataBase64) {path}: Hier gibt es kein Dateisystem (übergeben Sie dataBase64 zum Öffnen; lassen Sie den Pfad weg, um dataBase64 zu erhalten)
@msg {path}: not a file {path}: keine Datei
@msg {path}: {e} {path}: {e}
@msg {status} ({e}) {status} ({e})
@msg Couldn't place {name}: {e} {name} konnte nicht platziert werden: {e}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+55
View File
@@ -369,6 +369,59 @@
Character Style Options Opciones de estilo de carácter
Character Styles Estilos de carácter
Check Links Comprobar vínculos
# Check Script (File › Scripts) and the report it shows on a script written for the reference app.
Check Script Comprobar script
Check Script… Comprobar script…
Copy Report Copiar informe
VectorCraft read this script without running it. VectorCraft leyó este script sin ejecutarlo.
Needs nothing beyond plain JavaScript Solo necesita JavaScript simple
Needs the script language's own objects or directives Necesita objetos o directivas propios del lenguaje del script
Needs the document model Necesita el modelo de documento
Needs files Necesita archivos
Needs menu commands Necesita comandos de menú
Needs dialogs Necesita cuadros de diálogo
Needs features only the reference app has Necesita funciones que solo tiene la aplicación de referencia
Outside the document Fuera del documento
Writes, moves or deletes files Escribe, mueve o elimina archivos
Opens files in other apps Abre archivos en otras aplicaciones
Opens network connections Abre conexiones de red
Loads native code Carga código nativo
Sends messages to other apps Envía mensajes a otras aplicaciones
Reads or sets environment variables Lee o define variables de entorno
Runs code the checker can't read Ejecuta código que la comprobación no puede leer
Changes the reference app's preferences Cambia las preferencias de la aplicación de referencia
Menu commands Comandos de menú
Names the script builds as it runs: {count} Nombres que el script arma al ejecutarse: {count}
Document model Modelo de documento
Object types: {names} Tipos de objeto: {names}
Directives Directivas
Includes Archivos incluidos
Checked Comprobado
Not followed: outside the script's folder Omitido: fuera de la carpeta del script
Includes itself Se incluye a sí mismo
Not read No leído
Not read: a limit was reached No leído: se alcanzó un límite
Compiled or not text Compilado o no es texto
Problems Problemas
String not closed Cadena sin cerrar
Comment not closed Comentario sin cerrar
Regular expression not closed Expresión regular sin cerrar
Template literal not closed Literal de plantilla sin cerrar
XML literal not closed Literal XML sin cerrar
Bracket never closed Delimitador sin cerrar
Closing bracket with no opening one Delimitador de cierre sin apertura
Syntax newer than ES3 Sintaxis más reciente que ES3
Unknown directive Directiva desconocida
Not valid UTF-8: unreadable bytes were replaced UTF-8 no válido: se reemplazaron los bytes ilegibles
Mixed line endings Finales de línea mezclados
Nesting too deep to follow Anidamiento demasiado profundo para seguirlo
line {line} línea {line}
lines {lines} líneas {lines}
{file}, line {line} {file}, línea {line}
Compiled script: ask its author for the .jsx file Script compilado: pídele el archivo .jsx a su autor
Too large to check Demasiado grande para comprobarlo
Not a script for the reference app No es un script para la aplicación de referencia
Some lists were cut short at their limit. Algunas listas se recortaron al llegar a su límite.
Check Spelling… Revisar ortografía…
Choose a folder Elige una carpeta
Choose… Elegir…
@@ -3406,6 +3459,7 @@ axis Both Ambos
@msg Convert to Artboards: selection has no area Convertir en mesas de trabajo: la selección no tiene área
@msg Convert to Shape: no rectangles or ellipses selected Convertir en forma: no hay rectángulos ni elipses seleccionados
@msg Copied the CSS of {rules} object(s) Se copió el CSS de {rules} objeto(s)
@msg Copied the script report Se copió el informe del script
@msg Corners edits the corners of a path Esquinas edita las esquinas de un trazado
@msg Couldn't copy to the system clipboard: {e} No se pudo copiar al portapapeles del sistema: {e}
@msg Couldn't open {name}: {e} No se pudo abrir {name}: {e}
@@ -3897,6 +3951,7 @@ axis Both Ambos
@msg {name}: no such recovery entry {name}: no existe esa entrada de recuperación
@msg {path} can't be found: relink it first no se encuentra {path}: vuelve a vincularlo primero
@msg {path}: no file system here (pass dataBase64 to open; omit the path to get dataBase64) {path}: aquí no hay sistema de archivos (pasa dataBase64 para abrir; omite la ruta para obtener dataBase64)
@msg {path}: not a file {path}: no es un archivo
@msg {path}: {e} {path}: {e}
@msg {status} ({e}) {status} ({e})
@msg Couldn't place {name}: {e} No se pudo colocar {name}: {e}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+55
View File
@@ -370,6 +370,59 @@
Character Style Options Options de style de caractère
Character Styles Styles de caractère
Check Links Vérifier les liens
# Check Script (File › Scripts) and the report it shows on a script written for the reference app.
Check Script Vérifier le script
Check Script… Vérifier le script…
Copy Report Copier le rapport
VectorCraft read this script without running it. VectorCraft a lu ce script sans l’exécuter.
Needs nothing beyond plain JavaScript Ne demande que du JavaScript simple
Needs the script language's own objects or directives Demande les objets ou directives propres au langage du script
Needs the document model Demande le modèle de document
Needs files Demande des fichiers
Needs menu commands Demande des commandes de menu
Needs dialogs Demande des boîtes de dialogue
Needs features only the reference app has Demande des fonctions que seule l’application de référence possède
Outside the document Hors du document
Writes, moves or deletes files Écrit, déplace ou supprime des fichiers
Opens files in other apps Ouvre des fichiers dans d’autres applications
Opens network connections Ouvre des connexions réseau
Loads native code Charge du code natif
Sends messages to other apps Envoie des messages à d’autres applications
Reads or sets environment variables Lit ou définit des variables d’environnement
Runs code the checker can't read Exécute du code que la vérification ne peut pas lire
Changes the reference app's preferences Modifie les préférences de l’application de référence
Menu commands Commandes de menu
Names the script builds as it runs: {count} Noms construits par le script à l’exécution ({count})
Document model Modèle de document
Object types: {names} Types d’objets ({names})
Directives Directives
Includes Fichiers inclus
Checked Vérifié
Not followed: outside the script's folder Ignoré (hors du dossier du script)
Includes itself S’inclut lui-même
Not read Non lu
Not read: a limit was reached Non lu (limite atteinte)
Compiled or not text Compilé ou pas du texte
Problems Problèmes
String not closed Chaîne non fermée
Comment not closed Commentaire non fermé
Regular expression not closed Expression régulière non fermée
Template literal not closed Littéral de gabarit non fermé
XML literal not closed Littéral XML non fermé
Bracket never closed Délimiteur jamais fermé
Closing bracket with no opening one Délimiteur fermant sans ouvrant
Syntax newer than ES3 Syntaxe plus récente qu’ES3
Unknown directive Directive inconnue
Not valid UTF-8: unreadable bytes were replaced UTF-8 non valide (octets illisibles remplacés)
Mixed line endings Fins de ligne mélangées
Nesting too deep to follow Imbrication trop profonde pour être suivie
line {line} ligne {line}
lines {lines} lignes {lines}
{file}, line {line} {file}, ligne {line}
Compiled script: ask its author for the .jsx file Script compilé. Demandez le fichier .jsx à son auteur
Too large to check Trop volumineux pour être vérifié
Not a script for the reference app Ce n’est pas un script pour l’application de référence
Some lists were cut short at their limit. Certaines listes ont été tronquées à leur limite.
Check Spelling… Vérifier l’orthographe…
Choose a folder Choisissez un dossier
Choose… Choisir…
@@ -3403,6 +3456,7 @@ axis Both Les deux
@msg Convert to Artboards: selection has no area Convertir en plans de travail : la sélection n’a pas de surface
@msg Convert to Shape: no rectangles or ellipses selected Convertir en forme : aucun rectangle ni aucune ellipse sélectionné
@msg Copied the CSS of {rules} object(s) CSS de {rules} objet(s) copié
@msg Copied the script report Rapport du script copié
@msg Corners edits the corners of a path Angles modifie les angles d’un tracé
@msg Couldn't copy to the system clipboard: {e} Impossible de copier dans le presse-papiers du système : {e}
@msg Couldn't open {name}: {e} Impossible d’ouvrir {name} : {e}
@@ -3894,6 +3948,7 @@ axis Both Les deux
@msg {name}: no such recovery entry {name} : entrée de récupération inexistante
@msg {path} can't be found: relink it first {path} est introuvable : rétablissez d’abord le lien
@msg {path}: no file system here (pass dataBase64 to open; omit the path to get dataBase64) {path} : aucun système de fichiers ici (transmettez dataBase64 pour ouvrir ; omettez le chemin pour obtenir dataBase64)
@msg {path}: not a file {path} n’est pas un fichier
@msg {path}: {e} {path} : {e}
@msg {status} ({e}) {status} ({e})
@msg Couldn't place {name}: {e} Impossible d’importer {name} : {e}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+55
View File
@@ -531,6 +531,59 @@
Character Styles Karakterstílusok
Character: Karakter:
Check Links Csatolások ellenőrzése
# Check Script (File › Scripts) and the report it shows on a script written for the reference app.
Check Script Szkript ellenőrzése
Check Script… Szkript ellenőrzése…
Copy Report Jelentés másolása
VectorCraft read this script without running it. A VectorCraft futtatás nélkül olvasta be ezt a szkriptet.
Needs nothing beyond plain JavaScript Csak egyszerű JavaScript kell hozzá
Needs the script language's own objects or directives A szkriptnyelv saját objektumai vagy direktívái kellenek hozzá
Needs the document model A dokumentummodell kell hozzá
Needs files Fájlok kellenek hozzá
Needs menu commands Menüparancsok kellenek hozzá
Needs dialogs Párbeszédablakok kellenek hozzá
Needs features only the reference app has Olyan funkciók kellenek hozzá, amelyek csak a referenciaalkalmazásban vannak
Outside the document A dokumentumon kívül
Writes, moves or deletes files Fájlokat ír, áthelyez vagy töröl
Opens files in other apps Fájlokat nyit meg más alkalmazásokban
Opens network connections Hálózati kapcsolatokat nyit
Loads native code Natív kódot tölt be
Sends messages to other apps Üzeneteket küld más alkalmazásoknak
Reads or sets environment variables Környezeti változókat olvas vagy állít be
Runs code the checker can't read Olyan kódot futtat, amelyet az ellenőrzés nem tud beolvasni
Changes the reference app's preferences Módosítja a referenciaalkalmazás beállításait
Menu commands Menüparancsok
Names the script builds as it runs: {count} A szkript által futás közben előállított nevek: {count}
Document model Dokumentummodell
Object types: {names} Objektumtípusok: {names}
Directives Direktívák
Includes Beillesztett fájlok
Checked Ellenőrizve
Not followed: outside the script's folder Kihagyva: a szkript mappáján kívül esik
Includes itself Önmagát illeszti be
Not read Nincs beolvasva
Not read: a limit was reached Nincs beolvasva: korlát elérve
Compiled or not text Lefordított vagy nem szöveg
Problems Problémák
String not closed Lezáratlan karakterlánc
Comment not closed Lezáratlan megjegyzés
Regular expression not closed Lezáratlan reguláris kifejezés
Template literal not closed Lezáratlan sablonliterál
XML literal not closed Lezáratlan XML-literál
Bracket never closed Nyitva maradt zárójel
Closing bracket with no opening one Záró zárójel nyitó pár nélkül
Syntax newer than ES3 Az ES3-nál újabb szintaxis
Unknown directive Ismeretlen direktíva
Not valid UTF-8: unreadable bytes were replaced Érvénytelen UTF-8: az olvashatatlan bájtok lecserélve
Mixed line endings Vegyes sorvégek
Nesting too deep to follow Túl mély egymásba ágyazás
line {line} sor: {line}
lines {lines} sorok: {lines}
{file}, line {line} {file}, sor: {line}
Compiled script: ask its author for the .jsx file Lefordított szkript: kérd el a szerzőjétől a .jsx fájlt
Too large to check Túl nagy az ellenőrzéshez
Not a script for the reference app Nem a referenciaalkalmazáshoz írt szkript
Some lists were cut short at their limit. Egyes listák a korlátjuknál megszakadtak.
Check Spelling… Helyesírás-ellenőrzés…
Chevron Csúcsos nyíl
Choose a folder Válassz mappát
@@ -4093,6 +4146,7 @@ imageTrace Outline Körvonal
@msg Convert to Artboards: selection has no area Átalakítás rajztáblákká: a kijelölésnek nincs területe
@msg Convert to Shape: no rectangles or ellipses selected Átalakítás alakzattá: nincs kijelölt téglalap vagy ellipszis
@msg Copied the CSS of {rules} object(s) CSS másolva, objektumok: {rules}
@msg Copied the script report Szkriptjelentés másolva
@msg Corners edits the corners of a path a Sarkok parancs egy útvonal sarkait szerkeszti
@msg could not decode image: {0} a kép nem dekódolható: {0}
@msg Couldn't copy to the system clipboard: {e} Nem sikerült a rendszer vágólapjára másolni: {e}
@@ -4598,6 +4652,7 @@ imageTrace Outline Körvonal
@msg {path} can't be found: relink it first nem található: {path}; előbb csatold újra
@msg {path} is not a folder nem mappa: {path}
@msg {path}: no file system here (pass dataBase64 to open; omit the path to get dataBase64) {path}: itt nincs fájlrendszer (megnyitáshoz add meg a dataBase64-et; az útvonal elhagyásával dataBase64-et kapsz)
@msg {path}: not a file {path}: nem fájl
@msg {path}: {e} {path}: {e}
@msg {status} ({e}) {status} ({e})
@msg {tool} isn't in a toolbar group of several tools az eszköz nincs többeszközös eszköztár-csoportban: {tool}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+55
View File
@@ -369,6 +369,59 @@
Character Style Options Opzioni stile carattere
Character Styles Stili carattere
Check Links Verifica collegamenti
# Check Script (File › Scripts) and the report it shows on a script written for the reference app.
Check Script Controlla script
Check Script… Controlla script…
Copy Report Copia rapporto
VectorCraft read this script without running it. VectorCraft ha letto questo script senza eseguirlo.
Needs nothing beyond plain JavaScript Richiede solo JavaScript semplice
Needs the script language's own objects or directives Richiede oggetti o direttive propri del linguaggio dello script
Needs the document model Richiede il modello del documento
Needs files Richiede file
Needs menu commands Richiede comandi di menu
Needs dialogs Richiede finestre di dialogo
Needs features only the reference app has Richiede funzioni che ha solo l'applicazione di riferimento
Outside the document Fuori dal documento
Writes, moves or deletes files Scrive, sposta o elimina file
Opens files in other apps Apre file in altre app
Opens network connections Apre connessioni di rete
Loads native code Carica codice nativo
Sends messages to other apps Invia messaggi ad altre app
Reads or sets environment variables Legge o imposta variabili d'ambiente
Runs code the checker can't read Esegue codice che il controllo non può leggere
Changes the reference app's preferences Modifica le preferenze dell'applicazione di riferimento
Menu commands Comandi di menu
Names the script builds as it runs: {count} Nomi che lo script compone durante l'esecuzione: {count}
Document model Modello del documento
Object types: {names} Tipi di oggetto: {names}
Directives Direttive
Includes File inclusi
Checked Controllato
Not followed: outside the script's folder Ignorato: fuori dalla cartella dello script
Includes itself Include se stesso
Not read Non letto
Not read: a limit was reached Non letto: è stato raggiunto un limite
Compiled or not text Compilato o non testo
Problems Problemi
String not closed Stringa non chiusa
Comment not closed Commento non chiuso
Regular expression not closed Espressione regolare non chiusa
Template literal not closed Letterale modello non chiuso
XML literal not closed Letterale XML non chiuso
Bracket never closed Parentesi mai chiusa
Closing bracket with no opening one Parentesi di chiusura senza apertura
Syntax newer than ES3 Sintassi più recente di ES3
Unknown directive Direttiva sconosciuta
Not valid UTF-8: unreadable bytes were replaced UTF-8 non valido: i byte illeggibili sono stati sostituiti
Mixed line endings Fine riga miste
Nesting too deep to follow Annidamento troppo profondo da seguire
line {line} riga {line}
lines {lines} righe {lines}
{file}, line {line} {file}, riga {line}
Compiled script: ask its author for the .jsx file Script compilato: chiedi il file .jsx al suo autore
Too large to check Troppo grande da controllare
Not a script for the reference app Non è uno script per l'applicazione di riferimento
Some lists were cut short at their limit. Alcuni elenchi sono stati troncati al loro limite.
Check Spelling… Controlla ortografia…
Choose a folder Scegli una cartella
Choose… Scegli…
@@ -3406,6 +3459,7 @@ axis Both Entrambi
@msg Convert to Artboards: selection has no area Converti in tavole da disegno: la selezione non ha area
@msg Convert to Shape: no rectangles or ellipses selected Converti in forma: nessun rettangolo o ellisse selezionato
@msg Copied the CSS of {rules} object(s) Copiato il CSS di {rules} oggetto/i
@msg Copied the script report Rapporto dello script copiato
@msg Corners edits the corners of a path Angoli modifica gli angoli di un tracciato
@msg Couldn't copy to the system clipboard: {e} Impossibile copiare negli appunti di sistema: {e}
@msg Couldn't open {name}: {e} Impossibile aprire {name}: {e}
@@ -3897,6 +3951,7 @@ axis Both Entrambi
@msg {name}: no such recovery entry {name}: voce di recupero inesistente
@msg {path} can't be found: relink it first impossibile trovare {path}: ricollegalo prima
@msg {path}: no file system here (pass dataBase64 to open; omit the path to get dataBase64) {path}: nessun file system disponibile qui (passa dataBase64 per aprire; ometti il percorso per ottenere dataBase64)
@msg {path}: not a file {path}: non è un file
@msg {path}: {e} {path}: {e}
@msg {status} ({e}) {status} ({e})
@msg Couldn't place {name}: {e} Impossibile inserire {name}: {e}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+55
View File
@@ -937,6 +937,59 @@
Character Set: 文字セット:
Character Style Options 文字スタイルオプション
Check Links リンクを確認
# Check Script (File › Scripts) and the report it shows on a script written for the reference app.
Check Script スクリプトをチェック
Check Script… スクリプトをチェック…
Copy Report レポートをコピー
VectorCraft read this script without running it. VectorCraft はこのスクリプトを実行せずに読み取りました。
Needs nothing beyond plain JavaScript 通常の JavaScript 以外は不要
Needs the script language's own objects or directives スクリプト言語固有のオブジェクトまたはディレクティブが必要
Needs the document model ドキュメントモデルが必要
Needs files ファイルが必要
Needs menu commands メニューコマンドが必要
Needs dialogs ダイアログが必要
Needs features only the reference app has 参照アプリだけにある機能が必要
Outside the document ドキュメントの外
Writes, moves or deletes files ファイルを書き込み、移動、削除
Opens files in other apps ほかのアプリでファイルを開く
Opens network connections ネットワーク接続を開く
Loads native code ネイティブコードを読み込む
Sends messages to other apps ほかのアプリにメッセージを送る
Reads or sets environment variables 環境変数を読み取りまたは設定
Runs code the checker can't read チェックで読み取れないコードを実行
Changes the reference app's preferences 参照アプリの環境設定を変更
Menu commands メニューコマンド
Names the script builds as it runs: {count} スクリプトが実行時に組み立てる名前:{count}
Document model ドキュメントモデル
Object types: {names} オブジェクトの種類:{names}
Directives ディレクティブ
Includes インクルード
Checked チェック済み
Not followed: outside the script's folder 読み込み対象外:スクリプトのフォルダーの外
Includes itself 自身をインクルードしています
Not read 読み込んでいません
Not read: a limit was reached 読み込んでいません:上限に達しました
Compiled or not text コンパイル済みかテキストではありません
Problems 問題
String not closed 文字列が閉じていません
Comment not closed コメントが閉じていません
Regular expression not closed 正規表現が閉じていません
Template literal not closed テンプレートリテラルが閉じていません
XML literal not closed XML リテラルが閉じていません
Bracket never closed 括弧が閉じていません
Closing bracket with no opening one 対応する開き括弧のない閉じ括弧
Syntax newer than ES3 ES3 より新しい構文
Unknown directive 不明なディレクティブ
Not valid UTF-8: unreadable bytes were replaced 有効な UTF-8 ではありません:読み取れないバイトを置き換えました
Mixed line endings 改行コードが混在しています
Nesting too deep to follow 入れ子が深すぎてたどれません
line {line} {line} 行目
lines {lines} {lines} 行目
{file}, line {line} {file}、{line} 行目
Compiled script: ask its author for the .jsx file コンパイル済みスクリプト:作成者に .jsx ファイルを依頼してください
Too large to check 大きすぎてチェックできません
Not a script for the reference app 参照アプリ用のスクリプトではありません
Some lists were cut short at their limit. 一部の一覧は上限で打ち切られました。
Choose a folder フォルダーを選択
Choose… 選択…
Circle 円
@@ -3558,6 +3611,7 @@ theme Appearance 外観
@msg the layer to flatten into is hidden 統合先のレイヤーが非表示になっています
@msg there are no objects to release レイヤーに分配するオブジェクトがありません
@msg no library `{id}` ライブラリ `{id}` が見つかりません
@msg {path}: not a file {path}: ファイルではありません
@msg {path}: {e} {path}: {e}
@msg can't write .{ext} images .{ext} 画像は書き出せません
@msg Replace Spine: select a blend and a path ブレンド軸を置き換え: ブレンドとパスを選択してください
@@ -3917,6 +3971,7 @@ theme Appearance 外観
@msg {_1} font families available {_1} 個のフォントファミリーを使用できます
@msg {_1} has no {_2} style {_1} には {_2} スタイルがありません
@msg Copied the CSS of {rules} object(s) {rules} 個のオブジェクトの CSS をコピーしました
@msg Copied the script report スクリプトのレポートをコピーしました
@msg Exported {path} {path} を書き出しました
@msg no CSS to copy: select objects with CSS of their own コピーする CSS がありません: 独自の CSS を持つオブジェクトを選択してください
@msg unknown highlight `{h}` `{h}` は不明なハイライトの指定です
Can't render this file because it contains an unexpected character in line 2 and column 73.
+55
View File
@@ -932,6 +932,59 @@
Character Set: 문자 집합:
Character Style Options 문자 스타일 옵션
Check Links 링크 확인
# Check Script (File › Scripts) and the report it shows on a script written for the reference app.
Check Script 스크립트 확인
Check Script… 스크립트 확인…
Copy Report 보고서 복사
VectorCraft read this script without running it. VectorCraft가 이 스크립트를 실행하지 않고 읽었습니다.
Needs nothing beyond plain JavaScript 일반 JavaScript 외에는 필요 없음
Needs the script language's own objects or directives 스크립트 언어 자체의 개체 또는 지시문 필요
Needs the document model 문서 모델 필요
Needs files 파일 필요
Needs menu commands 메뉴 명령 필요
Needs dialogs 대화상자 필요
Needs features only the reference app has 참조 앱에만 있는 기능 필요
Outside the document 문서 바깥
Writes, moves or deletes files 파일 쓰기, 이동 또는 삭제
Opens files in other apps 다른 앱에서 파일 열기
Opens network connections 네트워크 연결 열기
Loads native code 네이티브 코드 로드
Sends messages to other apps 다른 앱에 메시지 보내기
Reads or sets environment variables 환경 변수 읽기 또는 설정
Runs code the checker can't read 검사기가 읽을 수 없는 코드 실행
Changes the reference app's preferences 참조 앱의 환경 설정 변경
Menu commands 메뉴 명령
Names the script builds as it runs: {count} 스크립트가 실행 중에 만드는 이름: {count}
Document model 문서 모델
Object types: {names} 개체 유형: {names}
Directives 지시문
Includes 포함 파일
Checked 확인됨
Not followed: outside the script's folder 따라가지 않음: 스크립트 폴더 바깥
Includes itself 자기 자신을 포함
Not read 읽지 않음
Not read: a limit was reached 읽지 않음: 한도에 도달함
Compiled or not text 컴파일되었거나 텍스트가 아님
Problems 문제
String not closed 문자열이 닫히지 않음
Comment not closed 주석이 닫히지 않음
Regular expression not closed 정규식이 닫히지 않음
Template literal not closed 템플릿 리터럴이 닫히지 않음
XML literal not closed XML 리터럴이 닫히지 않음
Bracket never closed 괄호가 닫히지 않음
Closing bracket with no opening one 여는 괄호가 없는 닫는 괄호
Syntax newer than ES3 ES3보다 새로운 구문
Unknown directive 알 수 없는 지시문
Not valid UTF-8: unreadable bytes were replaced 유효한 UTF-8이 아님: 읽을 수 없는 바이트를 바꿨습니다
Mixed line endings 줄 끝 형식이 섞여 있음
Nesting too deep to follow 중첩이 너무 깊어 따라갈 수 없음
line {line} {line}행
lines {lines} {lines}행
{file}, line {line} {file}, {line}행
Compiled script: ask its author for the .jsx file 컴파일된 스크립트: 작성자에게 .jsx 파일을 요청하세요
Too large to check 너무 커서 확인할 수 없음
Not a script for the reference app 참조 앱용 스크립트가 아님
Some lists were cut short at their limit. 일부 목록은 한도에서 잘렸습니다.
Choose a folder 폴더 선택
Choose… 선택…
Circle 원
@@ -3536,6 +3589,7 @@ theme Appearance 외관
@msg the layer to flatten into is hidden 병합 대상 레이어가 숨겨져 있습니다
@msg there are no objects to release 해제할 개체가 없습니다
@msg no library `{id}` `{id}` 라이브러리가 없습니다
@msg {path}: not a file {path}: 파일이 아닙니다
@msg {path}: {e} {path}: {e}
@msg can't write .{ext} images .{ext} 이미지는 쓸 수 없습니다
@msg Replace Spine: select a blend and a path 스파인 교체: 블렌드와 패스를 선택하세요
@@ -3895,6 +3949,7 @@ theme Appearance 외관
@msg {_1} font families available 사용 가능한 글꼴 패밀리 {_1}개
@msg {_1} has no {_2} style {_1}에는 {_2} 스타일이 없습니다
@msg Copied the CSS of {rules} object(s) 개체 {rules}개의 CSS를 복사했습니다
@msg Copied the script report 스크립트 보고서를 복사했습니다
@msg Exported {path} {path}을(를) 내보냈습니다
@msg no CSS to copy: select objects with CSS of their own 복사할 CSS가 없습니다: 자체 CSS가 있는 개체를 선택하세요
@msg unknown highlight `{h}` 알 수 없는 강조 `{h}`
Can't render this file because it contains an unexpected character in line 2 and column 73.
+54
View File
@@ -64,6 +64,8 @@
Export Selection… Eksportuj zaznaczenie…
Package… Spakuj…
Scripts Skrypty
Check Script… Sprawdź skrypt…
Check Script Sprawdź skrypt
Other Script… Inny skrypt…
Document Setup… Ustawienia dokumentu…
Document Color Mode Tryb koloru dokumentu
@@ -2391,6 +2393,56 @@ lighting Light Światło
Saved to User Defined: {} Zapisano w zdefiniowanych przez użytkownika: {}
Selected Swatches Only Tylko zaznaczone próbki
User Defined Libraries Biblioteki zdefiniowane przez użytkownika
Copy Report Kopiuj raport
VectorCraft read this script without running it. VectorCraft odczytał ten skrypt, nie uruchamiając go.
Needs nothing beyond plain JavaScript Wymaga tylko zwykłego JavaScriptu
Needs the script language's own objects or directives Wymaga własnych obiektów lub dyrektyw języka skryptowego
Needs the document model Wymaga modelu dokumentu
Needs files Wymaga plików
Needs menu commands Wymaga poleceń menu
Needs dialogs Wymaga okien dialogowych
Needs features only the reference app has Wymaga funkcji, które ma tylko aplikacja referencyjna
Outside the document Poza dokumentem
Writes, moves or deletes files Zapisuje, przenosi lub usuwa pliki
Opens files in other apps Otwiera pliki w innych aplikacjach
Opens network connections Otwiera połączenia sieciowe
Loads native code Wczytuje kod natywny
Sends messages to other apps Wysyła komunikaty do innych aplikacji
Reads or sets environment variables Odczytuje lub ustawia zmienne środowiskowe
Runs code the checker can't read Uruchamia kod, którego nie da się odczytać podczas sprawdzania
Changes the reference app's preferences Zmienia preferencje aplikacji referencyjnej
Menu commands Polecenia menu
Names the script builds as it runs: {count} Nazwy tworzone przez skrypt w trakcie działania: {count}
Document model Model dokumentu
Object types: {names} Typy obiektów: {names}
Directives Dyrektywy
Includes Dołączone pliki
Checked Sprawdzono
Not followed: outside the script's folder Pominięto: poza folderem skryptu
Includes itself Dołącza samego siebie
Not read Nie odczytano
Not read: a limit was reached Nie odczytano: osiągnięto limit
Compiled or not text Plik skompilowany lub nietekstowy
Problems Problemy
String not closed Niezamknięty ciąg znaków
Comment not closed Niezamknięty komentarz
Regular expression not closed Niezamknięte wyrażenie regularne
Template literal not closed Niezamknięty literał szablonowy
XML literal not closed Niezamknięty literał XML
Bracket never closed Niezamknięty nawias
Closing bracket with no opening one Nawias zamykający bez otwierającego
Syntax newer than ES3 Składnia nowsza niż ES3
Unknown directive Nieznana dyrektywa
Not valid UTF-8: unreadable bytes were replaced Nieprawidłowe kodowanie UTF-8: nieczytelne bajty zostały zastąpione
Mixed line endings Mieszane znaki końca wiersza
Nesting too deep to follow Zagnieżdżenie zbyt głębokie, aby je śledzić
line {line} wiersz {line}
lines {lines} wiersze {lines}
{file}, line {line} {file}, wiersz {line}
Compiled script: ask its author for the .jsx file Skompilowany skrypt: poproś autora o plik .jsx
Too large to check Zbyt duży, aby go sprawdzić
Not a script for the reference app To nie jest skrypt dla aplikacji referencyjnej
Some lists were cut short at their limit. Niektóre listy zostały obcięte po osiągnięciu limitu.
Line Segment Tool Options Opcje narzędzia Odcinek
Alt: Alt:
Divide Horizontally Into Podziel poziomo na
@@ -3571,6 +3623,7 @@ imageTrace Outline Kontur
@msg Convert to Artboards: selection has no area Konwertuj na obszary kompozycji: zaznaczenie nie ma powierzchni
@msg Convert to Shape: no rectangles or ellipses selected Konwertuj na kształt: nie zaznaczono prostokątów ani elips
@msg Copied the CSS of {rules} object(s) Skopiowano CSS obiektów: {rules}
@msg Copied the script report Skopiowano raport skryptu
@msg Corners edits the corners of a path Narożniki edytuje narożniki ścieżki
@msg Couldn't copy to the system clipboard: {e} Nie udało się skopiować do schowka systemowego: {e}
@msg Couldn't open {name}: {e} Nie udało się otworzyć {name}: {e}
@@ -4061,6 +4114,7 @@ imageTrace Outline Kontur
@msg {name}: no such recovery entry {name}: brak takiego wpisu odzyskiwania
@msg {path} can't be found: relink it first nie można znaleźć {path}: najpierw połącz go ponownie
@msg {path}: no file system here (pass dataBase64 to open; omit the path to get dataBase64) {path}: brak systemu plików (przekaż dataBase64, aby otworzyć; pomiń ścieżkę, aby otrzymać dataBase64)
@msg {path}: not a file {path}: nie jest plikiem
@msg {path}: {e} {path}: {e}
@msg {status} ({e}) {status} ({e})
@msg Couldn't place {name}: {e} Nie udało się umieścić {name}: {e}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+53
View File
@@ -349,6 +349,59 @@
Character Style Options Opções de Estilo de Caractere
Character Styles Estilos de Caractere
Check Links Verificar Links
# Check Script (File › Scripts) and the report it shows on a script written for the reference app.
Check Script Verificar Script
Check Script… Verificar Script…
Copy Report Copiar Relatório
VectorCraft read this script without running it. O VectorCraft leu este script sem executá-lo.
Needs nothing beyond plain JavaScript Precisa apenas de JavaScript simples
Needs the script language's own objects or directives Precisa de objetos ou diretivas próprios da linguagem do script
Needs the document model Precisa do modelo de documento
Needs files Precisa de arquivos
Needs menu commands Precisa de comandos de menu
Needs dialogs Precisa de caixas de diálogo
Needs features only the reference app has Precisa de recursos que só o aplicativo de referência tem
Outside the document Fora do documento
Writes, moves or deletes files Grava, move ou exclui arquivos
Opens files in other apps Abre arquivos em outros aplicativos
Opens network connections Abre conexões de rede
Loads native code Carrega código nativo
Sends messages to other apps Envia mensagens a outros aplicativos
Reads or sets environment variables Lê ou define variáveis de ambiente
Runs code the checker can't read Executa código que a verificação não consegue ler
Changes the reference app's preferences Altera as preferências do aplicativo de referência
Menu commands Comandos de menu
Names the script builds as it runs: {count} Nomes que o script monta durante a execução: {count}
Document model Modelo de documento
Object types: {names} Tipos de objeto: {names}
Directives Diretivas
Includes Arquivos incluídos
Checked Verificado
Not followed: outside the script's folder Ignorado: fora da pasta do script
Includes itself Inclui a si mesmo
Not read Não lido
Not read: a limit was reached Não lido: um limite foi atingido
Compiled or not text Compilado ou não é texto
Problems Problemas
String not closed String não fechada
Comment not closed Comentário não fechado
Regular expression not closed Expressão regular não fechada
Template literal not closed Literal de modelo não fechado
XML literal not closed Literal XML não fechado
Bracket never closed Delimitador não fechado
Closing bracket with no opening one Delimitador de fechamento sem abertura
Syntax newer than ES3 Sintaxe mais recente que ES3
Unknown directive Diretiva desconhecida
Not valid UTF-8: unreadable bytes were replaced UTF-8 inválido: bytes ilegíveis foram substituídos
Mixed line endings Quebras de linha misturadas
Nesting too deep to follow Aninhamento profundo demais para seguir
line {line} linha {line}
lines {lines} linhas {lines}
{file}, line {line} {file}, linha {line}
Compiled script: ask its author for the .jsx file Script compilado: peça o arquivo .jsx ao autor
Too large to check Grande demais para verificar
Not a script for the reference app Não é um script para o aplicativo de referência
Some lists were cut short at their limit. Algumas listas foram cortadas no limite.
Check Spelling… Verificar Ortografia…
Choose a folder Escolher uma pasta
Choose… Escolher…
Can't render this file because it contains an unexpected character in line 6 and column 47.
+55
View File
@@ -367,6 +367,59 @@
Character Style Options Параметры стиля символа
Character Styles Стили символов
Check Links Проверить связи
# Check Script (File › Scripts) and the report it shows on a script written for the reference app.
Check Script Проверить сценарий
Check Script… Проверить сценарий…
Copy Report Копировать отчёт
VectorCraft read this script without running it. VectorCraft прочитал этот сценарий, не запуская его.
Needs nothing beyond plain JavaScript Нужен только обычный JavaScript
Needs the script language's own objects or directives Нужны собственные объекты или директивы языка сценариев
Needs the document model Нужна модель документа
Needs files Нужны файлы
Needs menu commands Нужны команды меню
Needs dialogs Нужны диалоговые окна
Needs features only the reference app has Нужны функции, которые есть только в эталонном приложении
Outside the document Вне документа
Writes, moves or deletes files Записывает, перемещает или удаляет файлы
Opens files in other apps Открывает файлы в других приложениях
Opens network connections Открывает сетевые подключения
Loads native code Загружает нативный код
Sends messages to other apps Отправляет сообщения другим приложениям
Reads or sets environment variables Читает или задаёт переменные среды
Runs code the checker can't read Выполняет код, который проверка не может прочитать
Changes the reference app's preferences Изменяет настройки эталонного приложения
Menu commands Команды меню
Names the script builds as it runs: {count} Имена, которые сценарий составляет во время выполнения: {count}
Document model Модель документа
Object types: {names} Типы объектов: {names}
Directives Директивы
Includes Подключаемые файлы
Checked Проверен
Not followed: outside the script's folder Пропущен: вне папки сценария
Includes itself Подключает сам себя
Not read Не прочитан
Not read: a limit was reached Не прочитан: достигнут предел
Compiled or not text Скомпилирован или не является текстом
Problems Проблемы
String not closed Строка не закрыта
Comment not closed Комментарий не закрыт
Regular expression not closed Регулярное выражение не закрыто
Template literal not closed Шаблонная строка не закрыта
XML literal not closed XML-литерал не закрыт
Bracket never closed Скобка не закрыта
Closing bracket with no opening one Закрывающая скобка без открывающей
Syntax newer than ES3 Синтаксис новее ES3
Unknown directive Неизвестная директива
Not valid UTF-8: unreadable bytes were replaced Недопустимый UTF-8: нечитаемые байты заменены
Mixed line endings Смешанные концы строк
Nesting too deep to follow Слишком глубокая вложенность
line {line} строка {line}
lines {lines} строки {lines}
{file}, line {line} {file}, строка {line}
Compiled script: ask its author for the .jsx file Скомпилированный сценарий: попросите у автора файл .jsx
Too large to check Слишком большой для проверки
Not a script for the reference app Это не сценарий для эталонного приложения
Some lists were cut short at their limit. Некоторые списки обрезаны по пределу.
Check Spelling… Проверить орфографию…
Choose a folder Выберите папку
Choose… Выбрать…
@@ -3402,6 +3455,7 @@ axis Both Оба
@msg Convert to Artboards: selection has no area Преобразование в монтажные области: у выделения нет области
@msg Convert to Shape: no rectangles or ellipses selected Преобразование в фигуру: не выделены прямоугольники или эллипсы
@msg Copied the CSS of {rules} object(s) Скопирован CSS для {rules} объекта(ов)
@msg Copied the script report Отчёт о сценарии скопирован
@msg Corners edits the corners of a path «Углы» редактирует углы контура
@msg Couldn't copy to the system clipboard: {e} Не удалось скопировать в системный буфер обмена: {e}
@msg Couldn't open {name}: {e} Не удалось открыть {name}: {e}
@@ -3893,6 +3947,7 @@ axis Both Оба
@msg {name}: no such recovery entry {name}: такой записи восстановления нет
@msg {path} can't be found: relink it first {path} не найден: сначала восстановите связь
@msg {path}: no file system here (pass dataBase64 to open; omit the path to get dataBase64) {path}: здесь нет файловой системы (передайте dataBase64 для открытия; опустите путь, чтобы получить dataBase64)
@msg {path}: not a file {path}: не файл
@msg {path}: {e} {path}: {e}
@msg {status} ({e}) {status} ({e})
@msg Couldn't place {name}: {e} Не удалось поместить {name}: {e}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+55
View File
@@ -363,6 +363,59 @@
Character Style Options Параметри стилю символу
Character Styles Стилі символів
Check Links Перевірити зв’язки
# Check Script (File › Scripts) and the report it shows on a script written for the reference app.
Check Script Перевірити сценарій
Check Script… Перевірити сценарій…
Copy Report Копіювати звіт
VectorCraft read this script without running it. VectorCraft прочитав цей сценарій, не запускаючи його.
Needs nothing beyond plain JavaScript Потрібен лише звичайний JavaScript
Needs the script language's own objects or directives Потрібні власні об’єкти або директиви мови сценаріїв
Needs the document model Потрібна модель документа
Needs files Потрібні файли
Needs menu commands Потрібні команди меню
Needs dialogs Потрібні діалогові вікна
Needs features only the reference app has Потрібні функції, які має лише еталонна програма
Outside the document Поза документом
Writes, moves or deletes files Записує, переміщує або видаляє файли
Opens files in other apps Відкриває файли в інших програмах
Opens network connections Відкриває мережеві з’єднання
Loads native code Завантажує нативний код
Sends messages to other apps Надсилає повідомлення іншим програмам
Reads or sets environment variables Читає або задає змінні середовища
Runs code the checker can't read Виконує код, який перевірка не може прочитати
Changes the reference app's preferences Змінює налаштування еталонної програми
Menu commands Команди меню
Names the script builds as it runs: {count} Імена, які сценарій складає під час виконання: {count}
Document model Модель документа
Object types: {names} Типи об’єктів: {names}
Directives Директиви
Includes Підключені файли
Checked Перевірено
Not followed: outside the script's folder Пропущено: поза папкою сценарію
Includes itself Підключає сам себе
Not read Не прочитано
Not read: a limit was reached Не прочитано: досягнуто межі
Compiled or not text Скомпільований або не текст
Problems Проблеми
String not closed Рядок не закрито
Comment not closed Коментар не закрито
Regular expression not closed Регулярний вираз не закрито
Template literal not closed Шаблонний рядок не закрито
XML literal not closed XML-літерал не закрито
Bracket never closed Дужку не закрито
Closing bracket with no opening one Закривна дужка без відкривної
Syntax newer than ES3 Синтаксис, новіший за ES3
Unknown directive Невідома директива
Not valid UTF-8: unreadable bytes were replaced Недійсний UTF-8: нечитабельні байти замінено
Mixed line endings Змішані кінці рядків
Nesting too deep to follow Надто глибока вкладеність
line {line} рядок {line}
lines {lines} рядки {lines}
{file}, line {line} {file}, рядок {line}
Compiled script: ask its author for the .jsx file Скомпільований сценарій: попросіть в автора файл .jsx
Too large to check Завеликий для перевірки
Not a script for the reference app Це не сценарій для еталонної програми
Some lists were cut short at their limit. Деякі списки обрізано на межі.
Check Spelling… Перевірити орфографію…
Choose a folder Виберіть папку
Choose… Вибрати…
@@ -3390,6 +3443,7 @@ axis Both Обидві
@msg Convert to Artboards: selection has no area Перетворення на монтажні області: виділення не має області
@msg Convert to Shape: no rectangles or ellipses selected Перетворення на фігуру: не виділено прямокутники або еліпси
@msg Copied the CSS of {rules} object(s) Скопійовано CSS об’єктів: {rules}
@msg Copied the script report Звіт про сценарій скопійовано
@msg Corners edits the corners of a path «Кути» редагує кути контуру
@msg Couldn't copy to the system clipboard: {e} Не вдалося скопіювати до системного буфера обміну: {e}
@msg Couldn't open {name}: {e} Не вдалося відкрити {name}: {e}
@@ -3881,6 +3935,7 @@ axis Both Обидві
@msg {name}: no such recovery entry {name}: такого запису відновлення немає
@msg {path} can't be found: relink it first {path} не знайдено: спочатку відновіть зв’язок
@msg {path}: no file system here (pass dataBase64 to open; omit the path to get dataBase64) {path}: тут немає файлової системи (передайте dataBase64 для відкриття; опустіть шлях, щоб отримати dataBase64)
@msg {path}: not a file {path}: не файл
@msg {path}: {e} {path}: {e}
@msg {status} ({e}) {status} ({e})
@msg Couldn't place {name}: {e} Не вдалося помістити {name}: {e}
Can't render this file because it contains an unexpected character in line 713 and column 40.
+55
View File
@@ -446,6 +446,59 @@
Character Styles Các kiểu ký tự
Character: Ký tự:
Check Links Kiểm tra liên kết
# Check Script (File › Scripts) and the report it shows on a script written for the reference app.
Check Script Kiểm tra script
Check Script… Kiểm tra script…
Copy Report Sao chép báo cáo
VectorCraft read this script without running it. VectorCraft đã đọc script này mà không chạy nó.
Needs nothing beyond plain JavaScript Chỉ cần JavaScript thông thường
Needs the script language's own objects or directives Cần các đối tượng hoặc chỉ thị riêng của ngôn ngữ script
Needs the document model Cần mô hình tài liệu
Needs files Cần tệp
Needs menu commands Cần lệnh menu
Needs dialogs Cần hộp thoại
Needs features only the reference app has Cần tính năng mà chỉ ứng dụng tham chiếu có
Outside the document Bên ngoài tài liệu
Writes, moves or deletes files Ghi, di chuyển hoặc xóa tệp
Opens files in other apps Mở tệp trong ứng dụng khác
Opens network connections Mở kết nối mạng
Loads native code Tải mã gốc
Sends messages to other apps Gửi thông điệp đến ứng dụng khác
Reads or sets environment variables Đọc hoặc đặt biến môi trường
Runs code the checker can't read Chạy mã mà trình kiểm tra không đọc được
Changes the reference app's preferences Thay đổi tùy chọn của ứng dụng tham chiếu
Menu commands Lệnh menu
Names the script builds as it runs: {count} Tên do script tạo ra khi chạy: {count}
Document model Mô hình tài liệu
Object types: {names} Kiểu đối tượng: {names}
Directives Chỉ thị
Includes Tệp được bao gồm
Checked Đã kiểm tra
Not followed: outside the script's folder Bỏ qua: nằm ngoài thư mục của script
Includes itself Tự bao gồm chính nó
Not read Chưa đọc
Not read: a limit was reached Chưa đọc: đã đạt giới hạn
Compiled or not text Đã biên dịch hoặc không phải văn bản
Problems Vấn đề
String not closed Chuỗi chưa đóng
Comment not closed Chú thích chưa đóng
Regular expression not closed Biểu thức chính quy chưa đóng
Template literal not closed Chuỗi mẫu chưa đóng
XML literal not closed Hằng XML chưa đóng
Bracket never closed Dấu ngoặc không được đóng
Closing bracket with no opening one Dấu ngoặc đóng không có dấu ngoặc mở
Syntax newer than ES3 Cú pháp mới hơn ES3
Unknown directive Chỉ thị không xác định
Not valid UTF-8: unreadable bytes were replaced UTF-8 không hợp lệ: các byte không đọc được đã bị thay thế
Mixed line endings Kết thúc dòng lẫn lộn
Nesting too deep to follow Lồng nhau quá sâu để theo dõi
line {line} dòng {line}
lines {lines} các dòng {lines}
{file}, line {line} {file}, dòng {line}
Compiled script: ask its author for the .jsx file Script đã biên dịch: hãy xin tác giả tệp .jsx
Too large to check Quá lớn để kiểm tra
Not a script for the reference app Không phải script cho ứng dụng tham chiếu
Some lists were cut short at their limit. Một số danh sách đã bị cắt ngắn ở giới hạn của chúng.
Check Spelling… Kiểm tra chính tả…
Choose a folder Chọn thư mục
Choose the file to relink each linked file to. Files with the size and contents saved with the link come first. Chọn tệp để liên kết lại từng tệp liên kết. Các tệp có kích thước và nội dung đã lưu cùng liên kết được hiển thị trước.
@@ -3723,6 +3776,7 @@ theme Appearance Giao diện
@msg Convert to Artboards: selection has no area Chuyển thành bảng vẽ: vùng chọn không có diện tích
@msg Convert to Shape: no rectangles or ellipses selected Chuyển thành hình dạng: chưa chọn hình chữ nhật hay hình elip
@msg Copied the CSS of {rules} object(s) Đã sao chép CSS của {rules} đối tượng
@msg Copied the script report Đã sao chép báo cáo script
@msg Corners edits the corners of a path Corners chỉnh sửa các góc của đường dẫn
@msg could not decode image: {0} không thể giải mã ảnh: {0}
@msg Couldn't copy to the system clipboard: {e} Không thể sao chép vào khay nhớ tạm hệ thống: {e}
@@ -4237,6 +4291,7 @@ theme Appearance Giao diện
@msg {path} can't be found: relink it first {path} không tìm thấy: hãy liên kết lại trước
@msg {path} is not a folder {path} không phải là thư mục
@msg {path}: no file system here (pass dataBase64 to open; omit the path to get dataBase64) {path}: không có hệ thống tệp ở đây (truyền dataBase64 để mở; bỏ path để nhận dataBase64)
@msg {path}: not a file {path}: không phải là tệp
@msg {path}: {e} {path}: {e}
@msg {status} ({e}) {status} ({e})
@msg {tool} isn't in a toolbar group of several tools {tool} không nằm trong nhóm thanh công cụ có nhiều công cụ
Can't render this file because it contains an unexpected character in line 3643 and column 10.
+53
View File
@@ -330,6 +330,59 @@
Character Style Options 字符样式选项
Character Styles 字符样式
Check Links 检查链接
# Check Script (File › Scripts) and the report it shows on a script written for the reference app.
Check Script 检查脚本
Check Script… 检查脚本…
Copy Report 复制报告
VectorCraft read this script without running it. VectorCraft 读取了此脚本,但没有运行它。
Needs nothing beyond plain JavaScript 只需要普通的 JavaScript
Needs the script language's own objects or directives 需要脚本语言自身的对象或指令
Needs the document model 需要文档模型
Needs files 需要文件
Needs menu commands 需要菜单命令
Needs dialogs 需要对话框
Needs features only the reference app has 需要只有参考应用才有的功能
Outside the document 文档之外
Writes, moves or deletes files 写入、移动或删除文件
Opens files in other apps 在其他应用中打开文件
Opens network connections 打开网络连接
Loads native code 加载本机代码
Sends messages to other apps 向其他应用发送消息
Reads or sets environment variables 读取或设置环境变量
Runs code the checker can't read 运行检查无法读取的代码
Changes the reference app's preferences 更改参考应用的首选项
Menu commands 菜单命令
Names the script builds as it runs: {count} 脚本在运行时组成的名称:{count}
Document model 文档模型
Object types: {names} 对象类型:{names}
Directives 指令
Includes 包含的文件
Checked 已检查
Not followed: outside the script's folder 已跳过:在脚本文件夹之外
Includes itself 包含自身
Not read 未读取
Not read: a limit was reached 未读取:已达到上限
Compiled or not text 已编译或不是文本
Problems 问题
String not closed 字符串未闭合
Comment not closed 注释未闭合
Regular expression not closed 正则表达式未闭合
Template literal not closed 模板字面量未闭合
XML literal not closed XML 字面量未闭合
Bracket never closed 括号未闭合
Closing bracket with no opening one 闭合括号没有对应的左括号
Syntax newer than ES3 比 ES3 更新的语法
Unknown directive 未知指令
Not valid UTF-8: unreadable bytes were replaced 不是有效的 UTF-8:无法读取的字节已被替换
Mixed line endings 换行符混用
Nesting too deep to follow 嵌套过深,无法跟踪
line {line} 第 {line} 行
lines {lines} 第 {lines} 行
{file}, line {line} {file},第 {line} 行
Compiled script: ask its author for the .jsx file 已编译的脚本:请向作者索取 .jsx 文件
Too large to check 太大,无法检查
Not a script for the reference app 不是参考应用的脚本
Some lists were cut short at their limit. 部分列表已在上限处截断。
Check Spelling… 检查拼写…
Choose a folder 选择文件夹
Choose… 选择…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+53
View File
@@ -342,6 +342,59 @@
Character Style Options 字元樣式選項
Character Styles 字元樣式
Check Links 檢查連結
# Check Script (File › Scripts) and the report it shows on a script written for the reference app.
Check Script 檢查指令碼
Check Script… 檢查指令碼…
Copy Report 複製報告
VectorCraft read this script without running it. VectorCraft 已讀取此指令碼,但沒有執行。
Needs nothing beyond plain JavaScript 只需要一般的 JavaScript
Needs the script language's own objects or directives 需要指令碼語言本身的物件或指示詞
Needs the document model 需要文件模型
Needs files 需要檔案
Needs menu commands 需要選單指令
Needs dialogs 需要對話框
Needs features only the reference app has 需要只有參考應用程式才有的功能
Outside the document 文件以外
Writes, moves or deletes files 寫入、移動或刪除檔案
Opens files in other apps 在其他應用程式中開啟檔案
Opens network connections 開啟網路連線
Loads native code 載入原生程式碼
Sends messages to other apps 傳送訊息給其他應用程式
Reads or sets environment variables 讀取或設定環境變數
Runs code the checker can't read 執行檢查無法讀取的程式碼
Changes the reference app's preferences 變更參考應用程式的偏好設定
Menu commands 選單指令
Names the script builds as it runs: {count} 指令碼在執行時組成的名稱:{count}
Document model 文件模型
Object types: {names} 物件類型:{names}
Directives 指示詞
Includes 引入的檔案
Checked 已檢查
Not followed: outside the script's folder 已略過:在指令碼資料夾以外
Includes itself 引入自身
Not read 未讀取
Not read: a limit was reached 未讀取:已達上限
Compiled or not text 已編譯或不是文字
Problems 問題
String not closed 字串未結束
Comment not closed 註解未結束
Regular expression not closed 規則運算式未結束
Template literal not closed 樣板字面值未結束
XML literal not closed XML 字面值未結束
Bracket never closed 括號未關閉
Closing bracket with no opening one 結束括號沒有對應的開始括號
Syntax newer than ES3 比 ES3 更新的語法
Unknown directive 不明的指示詞
Not valid UTF-8: unreadable bytes were replaced 不是有效的 UTF-8:無法讀取的位元組已被取代
Mixed line endings 換行字元混用
Nesting too deep to follow 巢狀層次太深,無法追蹤
line {line} 第 {line} 行
lines {lines} 第 {lines} 行
{file}, line {line} {file},第 {line} 行
Compiled script: ask its author for the .jsx file 已編譯的指令碼:請向作者索取 .jsx 檔案
Too large to check 太大,無法檢查
Not a script for the reference app 不是參考應用程式的指令碼
Some lists were cut short at their limit. 部分清單已在上限處截斷。
Check Spelling… 拼字檢查…
Choose a folder 選擇資料夾
Choose… 選擇…
Can't render this file because it contains an unexpected character in line 6 and column 47.
+27 -8
View File
@@ -16,8 +16,10 @@ const TEMPLATE_EXTS: &[&str] = &["vctemplate", "ait", "vectorcraft", "drawcraft"
/// Open bytes of any readable format as a new document (templates open untitled); swatch and
/// graphic style library files open in the library panel, Libraries panel files are added to it,
/// and flattener, PDF, print and perspective grid presets files are imported. → `document.open`'s result for a document opened now (its
/// `warnings` say what didn't come in as it was), else null.
/// flattener, PDF, print and perspective grid presets files are imported, and a script written for
/// the reference app (`.jsx`, `.jsxinc`, `.jsxbin`) shows its check (Check Script…). →
/// `document.open`'s result for a document opened now (its `warnings` say what didn't come in as it
/// was), else null.
pub fn open_bytes(app: &mut VectorcraftApp, name: &str, bytes: &[u8], path: Option<String>) -> Result<Value, String> {
let opens = opens_as(name);
// A WebAssembly plug-in is installed.
@@ -27,6 +29,13 @@ pub fn open_bytes(app: &mut VectorcraftApp, name: &str, bytes: &[u8], path: Opti
app.status(format!("Installed plug-in {}", r["name"].as_str().unwrap_or(name)));
return Ok(Value::Null);
}
if opens == Some(OpensAs::Script) {
let p = match path {
Some(path) => json!({ "path": path }),
None => json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(bytes)}),
};
return dialogs::script_check::command(app, &p).map(|_| Value::Null);
}
if let Some(OpensAs::Presets { import, what }) = opens {
let r = app.run(import, serde_json::json!({"data": String::from_utf8_lossy(bytes)}))?;
let names: Vec<&str> = r["imported"].as_array().into_iter().flatten().filter_map(Value::as_str).collect();
@@ -57,12 +66,14 @@ pub fn open_bytes(app: &mut VectorcraftApp, name: &str, bytes: &[u8], path: Opti
open_document(app, name, bytes, path, &Value::Null)
}
/// The kinds of file File → Open installs, imports or shows in the library panel or the Libraries
/// panel ([`opens_as`]).
/// The kinds of file File → Open installs, imports, checks or shows in the library panel or the
/// Libraries panel ([`opens_as`]).
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum OpensAs {
/// A WebAssembly plug-in: installed.
Plugin,
/// A script written for the reference app: checked without running it (Check Script…).
Script,
/// Flattener, PDF, print or perspective grid presets: imported with command `import`, and
/// `what` names them in the status bar.
Presets { import: &'static str, what: &'static str },
@@ -74,9 +85,9 @@ pub enum OpensAs {
StyleLibrary,
}
/// The kind of file named `name` (by its extension) that File → Open installs, imports or shows in
/// the library panel or the Libraries panel ([`open_bytes`]), else `None`. A drop opens these files
/// the same way, wherever they are dropped ([`VectorcraftApp::drop_target`]).
/// The kind of file named `name` (by its extension) that File → Open installs, imports, checks or
/// shows in the library panel or the Libraries panel ([`open_bytes`]), else `None`. A drop opens
/// these files the same way, wherever they are dropped ([`VectorcraftApp::drop_target`]).
pub fn opens_as(name: &str) -> Option<OpensAs> {
let ext = fileio::extension(name);
let presets = [
@@ -87,6 +98,8 @@ pub fn opens_as(name: &str) -> Option<OpensAs> {
];
if vectorcraft_engine::cmd::plugin::EXTS.contains(&ext.as_str()) {
Some(OpensAs::Plugin)
} else if vectorcraft_engine::cmd::scripts::EXTS.contains(&ext.as_str()) {
Some(OpensAs::Script)
} else if let Some(&(_, import, what)) = presets.iter().find(|(exts, ..)| exts.contains(&ext.as_str())) {
Some(OpensAs::Presets { import, what })
} else if vectorcraft_engine::cmd::library::LIBRARY_EXTS.contains(&ext.as_str()) {
@@ -177,7 +190,13 @@ pub fn open_path(app: &mut VectorcraftApp, path: &str) -> Result<Value, String>
/// [`open_path`]; with `background`, a document (not a file that installs, imports or asks
/// first) is decoded off the UI thread ([`crate::opening::start`]).
fn open_file(app: &mut VectorcraftApp, path: &str, background: bool) -> Result<Value, String> {
pub(crate) fn open_file(app: &mut VectorcraftApp, path: &str, background: bool) -> Result<Value, String> {
// A script is read by `script.check`, up to its limit.
if opens_as(path) == Some(OpensAs::Script) {
dialogs::script_check::command(app, &json!({ "path": path }))?;
note_recent(app, path);
return Ok(Value::Null);
}
let mut bytes = read(app, path)?;
if background && opens_as(path).is_none() {
// A PDF with several pages or a password, or a DXF drawing, asks first, as at once.
+2
View File
@@ -130,6 +130,8 @@ mod tests_saveext;
#[cfg(test)]
mod tests_screenmode;
#[cfg(test)]
mod tests_script_check;
#[cfg(test)]
mod tests_scrub;
#[cfg(test)]
mod tests_selectall;
+8 -1
View File
@@ -763,6 +763,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{path?: a .wasm file} install a WebAssembly plug-in (plugin.install); without a path pick one (the web opens the browser's file picker; File › Open of a .wasm installs it too)",
),
(
"ui.checkScript",
"Check Script…",
"",
"{path? | data? | dataBase64?, name?, includes?: true with a path} check a script written for the reference app without running it (script.check; with a path, the files its #include lines name are checked too) and show the report (dialog `scriptCheck`: `report`, script.check's result; Copy Report copies it as JSON); without a file pick one (the web opens the browser's file picker; File › Open of a .jsx, .jsxinc or .jsxbin file shows the report too)",
),
(
"ui.perspectiveGridDialog",
"Define Perspective Grid…",
@@ -1434,6 +1440,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
"ui.printPresetDialog" => crate::dialogs::print::open_preset(app, p),
"plugin.dialog" => crate::dialogs::plugin::open(app, p),
"ui.installPlugin" => io::install_plugin(app, s("path")),
"ui.checkScript" => crate::dialogs::script_check::command(app, p),
"ui.perspectiveGridDialog" => crate::dialogs::perspective_grid::open(app),
"ui.perspectivePresetsDialog" => {
crate::dialogs::perspective_presets::open(app, s("selected").as_deref());
@@ -2087,7 +2094,7 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
c("Export Selection…", "file.exportSelection"),
Sep,
c("Package…", "ui.packageDialog"),
sub("Scripts", vec![todos("Other Script…", "Cmd+F12")]),
sub("Scripts", vec![c("Check Script…", "ui.checkScript"), todos("Other Script…", "Cmd+F12")]),
Sep,
c("Document Setup…", "file.documentSetup"),
sub(
+1
View File
@@ -61,6 +61,7 @@ const DOCUMENT_FREE: &[&str] = &[
"file.open",
"file.newFromTemplate",
"ui.installPlugin",
"ui.checkScript",
"window.swatchLibrary.other",
"window.graphicStyleLibrary.other",
"shortcuts.import",
+2 -2
View File
@@ -301,8 +301,8 @@ impl VectorcraftApp {
/// Where file `name` dropped with the pointer at `pos` (screen points) goes: as in Illustrator,
/// onto the canvas of the open document (Shift embeds it), else opened. Where the platform
/// doesn't say (`None`: desktop drags carry no position), a document opens as a tab of its
/// own and a picture or text is placed in the middle of the view. A library, a presets file or
/// a plug-in opens wherever it is dropped, as File → Open opens it ([`io::opens_as`]).
/// own and a picture or text is placed in the middle of the view. A library, a presets file, a
/// plug-in or a script opens wherever it is dropped, as File → Open opens it ([`io::opens_as`]).
pub fn drop_target(&self, name: &str, pos: Option<egui::Pos2>, shift: bool) -> DropTarget {
let (Some(rect), Some(st), Some(v)) = (self.canvas_rect, self.session.active(), self.view()) else { return DropTarget::Open };
let at = match pos {
+22 -3
View File
@@ -141,8 +141,9 @@ fn a_drop_without_a_position_opens_documents_and_places_pictures() {
}
}
/// Swatch libraries and color books, graphic style libraries, Libraries panel files, presets and
/// plug-ins open as File → Open opens them, dropped with or without a position, on the canvas too.
/// Swatch libraries and color books, graphic style libraries, Libraries panel files, presets,
/// plug-ins and scripts open as File → Open opens them, dropped with or without a position, on the
/// canvas too.
#[test]
fn libraries_presets_and_plugins_open_wherever_they_are_dropped() {
let mut app = app();
@@ -150,7 +151,7 @@ fn libraries_presets_and_plugins_open_wherever_they_are_dropped() {
let rect = laid_out(&mut app, &ctx);
let on_canvas = rect.center() + vec2(60.0, -40.0);
let libraries = ["a.vcswatches", "a.gpl", "a.ase", "a.acb", "a.vcstyles", "a.vclibrary"];
let presets_and_plugins = ["a.vcflattener", "a.vcpdfpresets", "a.vcprintpresets", "a.vcperspective", "a.wasm"];
let presets_and_plugins = ["a.vcflattener", "a.vcpdfpresets", "a.vcprintpresets", "a.vcperspective", "a.wasm", "a.jsx", "a.jsxinc", "a.jsxbin"];
for name in libraries.into_iter().chain(presets_and_plugins) {
assert_eq!(app.drop_target(name, None, false), DropTarget::Open, "{name}");
assert_eq!(app.drop_target(name, Some(on_canvas), true), DropTarget::Open, "{name} on the canvas");
@@ -175,6 +176,24 @@ fn a_dropped_swatch_library_opens_in_the_library_panel() {
assert_eq!(app.ui.recent_files.first(), Some(&gpl), "added to Open Recent Files as File → Open adds it");
}
/// A script dropped on the window while a document is open shows its check, as File → Open shows
/// it, and the document is left as it was.
#[test]
fn a_dropped_script_shows_its_check() {
let mut app = app();
let ctx = egui::Context::default();
laid_out(&mut app, &ctx);
let path = temp_file("dropped.jsx", b"#target acme\napp.executeMenuCommand('acmecopyall');\n");
frame(&mut app, &ctx, vec![], &[&path], false);
let d = app.ui.dialog.as_ref().expect("the check is shown");
assert_eq!(d.kind, crate::dialogs::script_check::KIND, "{}", app.ui.status);
assert_eq!(
(d.fields["report"]["name"].as_str(), d.fields["report"]["needs"].clone()),
(Some("dropped.jsx"), json!(["language", "menuCommands"]))
);
assert_eq!((app.session.documents().len(), images(&app, 0)), (1, 0), "neither opened as a document nor placed");
}
/// A Libraries panel file dropped on the window while a document is open is added to the
/// Libraries panel as a new library, as File → Open adds it.
#[test]
+200
View File
@@ -0,0 +1,200 @@
//! File › Scripts › Check Script… (`ui.checkScript`): the report on a script written for the reference
//! app, read without running it, and its Copy Report; File › Open of `.jsx`, `.jsxinc` and `.jsxbin`
//! files.
use serde_json::{Value, json};
use vectorcraft_engine::Session;
use crate::{VectorcraftApp, dialogs, io, menus, theme};
/// An app reading files from disk.
fn app() -> VectorcraftApp {
let services = crate::Services { read: Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))), ..Default::default() };
VectorcraftApp::new(Session::new(), services)
}
/// One headless frame of the dialog layer.
fn frame(app: &mut VectorcraftApp) {
let ctx = egui::Context::default();
theme::install_fonts(&ctx);
theme::apply(&ctx, Default::default());
let mut out = ctx.run_ui(Default::default(), |ui| dialogs::show(app, ui.ctx()));
out.textures_delta.clear();
}
/// A script with a little of everything the report shows.
const SCRIPT: &str = concat!(
"#target acme\n",
"#include 'lib/missing.jsxinc'\n",
"var doc = app.activeDocument;\n",
"app.executeMenuCommand('acmecopyall');\n",
"app.executeMenuCommand(name);\n",
"var f = new File(p); f.open('w');\n",
"if (x.typename == 'AcmeShape') {}\n",
"var s = 'open;\n",
);
fn report(app: &VectorcraftApp) -> &Value {
let d = app.ui.dialog.as_ref().expect("the report is shown");
assert_eq!(d.kind, dialogs::script_check::KIND);
&d.fields["report"]
}
#[test]
fn file_scripts_holds_check_script_above_other_script() {
let tree = menus::menu_tree();
let (_, file) = tree.iter().find(|(t, _)| *t == "File").unwrap();
let scripts = file.iter().find_map(|i| if let menus::Item::Sub("Scripts", items) = i { Some(items) } else { None }).unwrap();
assert!(
matches!(scripts.as_slice(), [menus::Item::Cmd("Check Script…", "ui.checkScript", _), menus::Item::Todo("Other Script…", _)]),
"{scripts:?}"
);
}
#[test]
fn check_script_shows_the_report_and_copy_report_copies_it() {
let mut app = app();
let p = json!({"dataBase64": vectorcraft_format::base64_encode(SCRIPT.as_bytes()), "name": "Tidy.jsx"});
let r = menus::run_ui_command(&mut app, "ui.checkScript", &p).unwrap().unwrap();
assert_eq!(report(&app), &r);
assert_eq!((r["name"].as_str(), r["kind"].as_str()), (Some("Tidy.jsx"), Some("script")));
assert_eq!(r["needs"], json!(["language", "dom", "files", "menuCommands"]));
// The window draws every section.
frame(&mut app);
assert_eq!(report(&app), &r);
// Copy Report (the button beside Close) copies the report as JSON and leaves the window open.
app.ui.dialog.as_mut().unwrap().fields.insert("discard".into(), json!(true));
dialogs::confirm(&mut app).unwrap();
let copied: Value = serde_json::from_str(app.clipboard_out.as_deref().unwrap()).unwrap();
assert_eq!(copied, r);
assert!(app.ui.dialog.as_ref().unwrap().fields.get("discard").is_none());
assert_eq!(app.ui.status, "Copied the script report");
// Close closes it.
dialogs::cancel(&mut app);
assert!(app.ui.dialog.is_none());
// The script's text does as well as its bytes.
let by_text = menus::run_ui_command(&mut app, "ui.checkScript", &json!({"data": SCRIPT, "name": "Tidy.jsx"})).unwrap().unwrap();
assert_eq!(by_text, r);
}
#[test]
fn each_kind_of_file_draws_its_verdict() {
for (name, bytes, kind) in [
("a.jsxbin", b"anything".as_slice(), "compiled"),
("a.jsx", b"import a from 'a';\n".as_slice(), "module"),
("a.jsx", b"\0\0\0".as_slice(), "notText"),
("a.jsx", b"".as_slice(), "script"),
] {
let mut app = app();
io::open_bytes(&mut app, name, bytes, None).unwrap();
assert_eq!(report(&app)["kind"], kind, "{name}");
frame(&mut app);
assert!(app.ui.dialog.is_some());
}
}
#[test]
fn file_open_of_a_script_checks_it_with_what_it_includes() {
let dir = std::env::temp_dir().join(format!("vc-ui-script-check-{}", std::process::id()));
std::fs::create_dir_all(dir.join("lib")).unwrap();
std::fs::write(dir.join("main.jsx"), "#include 'lib/net.jsxinc'\n").unwrap();
std::fs::write(dir.join("lib/net.jsxinc"), "var s = new Socket();\n").unwrap();
let path = dir.join("main.jsx").to_string_lossy().into_owned();
let mut opened = app();
let reads = std::rc::Rc::new(std::cell::Cell::new(0));
let counted = reads.clone();
opened.services.read = Some(Box::new(move |p: &str| {
counted.set(counted.get() + 1);
std::fs::read(p).map_err(|e| e.to_string())
}));
io::open_path(&mut opened, &path).unwrap();
let r = report(&opened);
assert_eq!(r["includes"], json!([{"value": "lib/net.jsxinc", "line": 1, "status": "checked", "path": "lib/net.jsxinc"}]));
assert_eq!(r["needs"], json!(["language", "hostOnly"]));
assert!(opened.session.documents().is_empty(), "no document opened");
// `script.check` reads the script up to its limit, and the app's reader, which reads a whole file,
// isn't asked.
assert_eq!(reads.get(), 0);
assert_eq!(opened.ui.recent_files.first(), Some(&path), "File › Open lists it in Open Recent Files");
// Check Script… with a path does the same; without one it asks for a script.
let mut app = app();
menus::run_ui_command(&mut app, "ui.checkScript", &json!({ "path": path })).unwrap().unwrap();
assert_eq!(report(&app)["includes"][0]["status"], "checked");
let asked = std::rc::Rc::new(std::cell::RefCell::new(vec![]));
let seen = asked.clone();
let picked = path.clone();
app.services.pick_open = Some(Box::new(move |pick: &crate::FilePick| {
seen.borrow_mut().push(pick.filters.clone());
Some(picked.clone())
}));
app.ui.dialog = None;
menus::run_ui_command(&mut app, "ui.checkScript", &json!({})).unwrap().unwrap();
assert_eq!(asked.borrow().as_slice(), [vec![("Scripts", &["jsx", "jsxinc", "jsxbin"][..])]]);
assert_eq!(report(&app)["name"], "main.jsx");
let _ = std::fs::remove_dir_all(dir);
}
/// Where a document opens off the UI thread (File › Open, Open Recent and drops on the desktop), a
/// script shows its report at once, and the app's reader isn't asked for it.
#[test]
fn a_script_opens_at_once_where_documents_open_off_the_ui_thread() {
let dir = std::env::temp_dir().join(format!("vc-ui-script-background-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("later.jsx");
std::fs::write(&path, "alert('later');\n").unwrap();
let path = path.to_string_lossy().into_owned();
let mut opened = app();
let reads = std::rc::Rc::new(std::cell::Cell::new(0));
let counted = reads.clone();
opened.services.read = Some(Box::new(move |p: &str| {
counted.set(counted.get() + 1);
std::fs::read(p).map_err(|e| e.to_string())
}));
assert_eq!(io::open_file(&mut opened, &path, true).unwrap(), Value::Null);
assert!(opened.opening.pending.is_empty(), "nothing is read off the UI thread");
assert_eq!(report(&opened)["name"], "later.jsx");
assert_eq!(reads.get(), 0);
assert_eq!(opened.ui.recent_files.first(), Some(&path));
let _ = std::fs::remove_dir_all(dir);
}
/// Check Script… asks for its script off the UI thread, and acts on no document: the answer shows the
/// report even when the document that was active closed meanwhile.
#[test]
fn check_script_asks_off_the_ui_thread_and_needs_no_document() {
let dir = std::env::temp_dir().join(format!("vc-ui-script-pick-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("picked.jsx");
std::fs::write(&path, "alert('picked');\n").unwrap();
let mut services = crate::Services { dialogs_are_sheets: false, ..Default::default() };
let (shown, _) = crate::tests_picks::off_the_ui_thread(&mut services);
let mut app = VectorcraftApp::new(Session::new(), services);
app.run("file.new", json!({"width": 100, "height": 80})).unwrap();
assert!(app.run("ui.checkScript", json!({})).is_err(), "nothing yet: the dialog is open");
assert!(
matches!(shown.requests().as_slice(), [crate::picks::PickRequest::Open(pick)] if pick.filters == [("Scripts", &["jsx", "jsxinc", "jsxbin"][..])])
);
app.run("file.close", json!({})).unwrap();
shown.answer(&[&path.to_string_lossy()]);
crate::picks::poll(&mut app, &egui::Context::default());
assert_eq!(report(&app)["name"], "picked.jsx", "{}", app.ui.status);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn every_id_a_report_holds_has_a_label() {
use dialogs::script_check::{need_label, outside_label, problem_label, status_label};
use vectorcraft_engine::cmd::scripts::{IncludeStatus, Need, OUTSIDE, PROBLEMS};
for need in Need::ALL {
assert!(need_label(need.id()).is_some(), "{need:?}");
}
for status in IncludeStatus::ALL {
assert!(status_label(status.id()).is_some(), "{status:?}");
}
for id in OUTSIDE {
assert!(outside_label(id).is_some(), "{id}");
}
for code in PROBLEMS {
assert!(problem_label(code).is_some(), "{code}");
}
}
+72 -1
View File
@@ -65,7 +65,8 @@ the suite) keep the agent to the folders you give it:
- Every file a command reads must lie inside the **read root**: `open_file`, `file.place`, relinking and
updating links, the images an SVG links to, swatch library, graphic style library and colour-profile loads, plug-ins,
`file.newFromTemplate`, preset imports, the folders `text.findFontFiles` and `links.findFiles` search.
`file.newFromTemplate`, preset imports, the folders `text.findFontFiles` and `links.findFiles` search, the
scripts `script.check` reads and the files they include.
- Every file a command writes must lie inside the **write root**: `save_file`, `export`, the save and
export commands, Export for Screens, Package, Export for Web, print to file, `screenshot {path}`,
library and preset saves to a path, the copies `links.relink {copy}` makes.
@@ -704,6 +705,76 @@ reference that names no value fails the step, so a command never runs with one l
vectorcraft-cli run --cmd file.new --params '{"width":600,"height":200}' --cmd text.create --params '{"x":10,"y":60,"text":"plain and bold words","size":30}' --cmd text.setRangeStyle --params '{"id":"$1.id","start":10,"end":14,"style":"Bold"}'
```
## Checking scripts written for the reference app
`script.check {path? | data? | dataBase64?, name?, includes?}` reads a script written for the reference app
(`.jsx`, `.jsxinc`, `.js`, or a compiled `.jsxbin`) and reports what it uses. The check does not run any of the
script, and it does not change the document or the preferences. The reply's `kind` is `script`, `compiled` (a
`.jsxbin` file, or text that starts with `@JSXBIN@`, which the check doesn't read), `module` (a file with a
top-level `import` or `export`, such as a React `.jsx` file), `notText` (a NUL byte, or more than one byte in ten a
control character, in the first 8 KiB) or `tooLarge` (over 8 MiB). For a script, the reply holds these lists:
- `needs` lists what the script needs from the app that runs it: `language` (the script language's own objects
and directives, such as `$`, `alert`, `UnitValue` and `#target`), `dom` (the reference app's document model),
`files`, `menuCommands`, `dialogs`, and `hostOnly` (features only the reference app has: palettes,
`#targetengine`, BridgeTalk, sockets, native code, XMP, the XML object and E4X, operator overloading,
reflection, actions and `selectTool`).
- `directives` lists the `#include`, `#includepath`, `#script`, `#strict`, `#target` and `#targetengine` lines,
and `targets` splits each `#target` line's application specifier into `app`, `instance`, `version` and
`locale`. A directive is read where it starts a line, written `#name` or `//@name`.
- `features` lists the platform features the script uses by id (such as `file.write`, `scriptui.dialog`,
`menuCommand` or `e4x`), each with a count and its lines.
- `dom.members` lists the document-model member chains as the script writes them
(`app.activeDocument.layers[].name`, with `[]` for an index and `()` for a call); a variable assigned from a chain
is replaced by that chain, and a name written with `\u` escapes is read as the name it spells. `dom.types` lists
the type names the script compares with `.typename` or creates with `new`, apart from the language's, the platform's
and the script's own functions.
- `menuCommands.strings` lists the string literals the script passes to `executeMenuCommand`
(`app.executeMenuCommand('…')`), each with a count and its lines, and `menuCommands.dynamic` counts the calls
whose argument isn't a string literal.
- `outside` lists what the script does outside the document: `filesWritten` (with the open modes used),
`filesOpened` (files opened in other apps), `network`, `nativeCode`, `otherApps` (messages to other apps),
`environment` (environment variables), `unseenCode` (`eval`, `new Function` and `$.evalFile`) and
`preferences` (the reference app's preferences changed).
- `problems` lists what would stop the script loading as written (strings, comments, regular expressions,
template literals and XML literals that aren't closed, brackets that don't match, syntax newer than ES3 and
unknown directives) and notes on its text (bytes that aren't valid UTF-8, mixed line endings, and nesting deeper
than the check follows: more than 4,096 open brackets, or XML literals and the code in their braces nested more
than 4,096 levels deep).
Each entry of `targets`, `directives`, `includes` and `problems` has the `line` it is on (a problem with the whole
text has none) and, when it is in an included file, that file's path as `file`. `features`, `menuCommands.strings`
and `outside` count the script and its included files together and list up to 20 of the script's own lines.
With a path and `includes: true`, the check also reads the files the `#include` lines name, and the files their own
`#include` lines name. When the script or one of these files assigns a member chain or a File object to a variable,
the check treats the variable as holding it in every file. The check looks for a name in the folder of the file that
holds the `#include` line, then in each
folder that the last `#includepath` line before it names, taken from that same folder. It reads only files inside
the script's folder: it doesn't follow an absolute name, a name, folder or link that leads out of the script's
folder, a name or folder longer than 4,096 bytes, a file outside the automation read root, or the `JSINCLUDE`
environment variable, and it reads each file once. An include's `status` is `checked`, `missing`, `notFollowed`,
`cycle` (the file it names includes the file the line is in, directly or through other files), `notRead` (includes
weren't read: no `includes`, no path, or the web app), `limit` (one of the limits below was reached) or `unreadable`
(a compiled file, or not text), and its `path` is the file found, relative to the script's folder. The check reads
at most 8 MiB of a file and 32 MiB in all, follows 64 included files to a depth of 16 and looks up at most 1,024
paths for them, keeps 1,000 entries in each list, follows at most 1,000 variables that hold member chains and 1,000
that hold File objects (`truncated` is true when an entry or a variable was left out), and cuts the text it echoes
from the script to 120 characters.
File › Scripts › Check Script… picks a `.jsx`, `.jsxinc` or `.jsxbin` file and shows its report, and File › Open and
drag-and-drop show the report for those files too; the web app checks the script without its included files. Over
the control channel, `ui.checkScript {path? | data? | dataBase64?, name?}` does the same, and with a path it checks
the included files too.
```json
{"name":"run_command","arguments":{"command":"script.check","params":{"path":"/Users/me/Scripts/Tidy Layers.jsx","includes":true}}}
```
```sh
vectorcraft-cli run --cmd script.check --params '{"path": "Tidy Layers.jsx"}'
```
## Transparency and opacity masks
Transparency and opacity-mask commands take `ids` (or `id`), so they need no selection. Opacity is a percentage
+1
View File
@@ -386,6 +386,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- MCP protocol: five prompts (`prompts/list` / `prompts/get`: poster, icon set, recolour, trace-and-style, export set), `completion/complete` for prompt arguments and template variables read from the live catalogues (formats, effect ids, command ids, trace presets, swatch and object ids), four resource templates (`vectorcraft://object/{id}`, `//command/{id}`, `//effect/{id}`, `//swatch/{name}`) so an agent reads one thing instead of the whole document, and `logging/setLevel` with `notifications/message`. `window.panel` ids are case-insensitive (display labels work too), `vectorcraft://object/{id}` reads through `document.node {summary: true}` (the same reply bytes from one lookup instead of summarizing the whole document: a leaf read on a 2,000-object document drops from ~20 ms to ~0.06 ms), large containers slice with `document.node {depth?, childLimit?}` (truncated levels report `childCount`), `document.inspect` and the `inspect_document` tool slice the same way so a depth-0 read is the layer skeleton (~9.7 KB instead of ~2 MB on a 2,000-object document), `document.find` locates nodes by name/kind/text with ancestor paths, and the CLI sweeps the protocol surface over stdio against the compiled binary. `docs/mcp.md` has a Protocol section covering the capability table and what is deliberately absent (subscriptions, progress, cancellation, sampling, elicitation, Streamable HTTP — all of which need the transport to interleave reads).
- Headless CLI (`vectorcraft-cli run`, `convert`, `info`, `bench`, `perf`, `mcp`). A `run` step, like a `command_batch` step over MCP, can use an earlier step's result (`"$1.id"`, #785).
- Actions panel that records and plays back commands.
- File › Scripts › Check Script… reads a script written for Illustrator (`.jsx`, `.jsxinc` or a compiled `.jsxbin`) without running any of it and reports what it needs (the document model, files, dialogs, menu commands, features only Illustrator has), what it would do outside the document, the menu commands and document-model members it uses, its directives and included files, and the problems that would stop it loading (`script.check`, which also reads `.js` files); File › Open and drag-and-drop show the report for `.jsx`, `.jsxinc` and `.jsxbin` files.
- **UI:** Illustrator 2026 layout restyled to measured values:
- Medium Dark theme, categorized and Advanced toolbars (a tool group's flyout opens on a right press, a long press or a press on its corner triangle), 35 pt document tabs, 33 pt panel tabs.
- Hint bar, contextual task bar, 19 dock panels with ≡ menus.
+1
View File
@@ -62,6 +62,7 @@ pub const TABLE: &[(&str, Class)] = &[
("metafile", Class::Layer(3)),
("plugins", Class::Layer(2)),
("affinity", Class::Standalone),
("scriptcheck", Class::Standalone),
];
/// Explicit orderings *within* a layer (earlier may be used by later).