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:
46 files changed
+7845
-19
No files matched your search
Generated
+8
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -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"}))?;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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"]);
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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, ¬e);
|
||||
}
|
||||
});
|
||||
}
|
||||
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."));
|
||||
}
|
||||
}
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -61,6 +61,7 @@ const DOCUMENT_FREE: &[&str] = &[
|
||||
"file.open",
|
||||
"file.newFromTemplate",
|
||||
"ui.installPlugin",
|
||||
"ui.checkScript",
|
||||
"window.swatchLibrary.other",
|
||||
"window.graphicStyleLibrary.other",
|
||||
"shortcuts.import",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in new issue
Block a user