M4.12: one loader and encoder for every frontend — engine fileio (load/encode/export/save, append-only FORMATS table, OPEN_EXTS, document.formats query, typed per-format options, parse_range); document.open reads native/legacy, SVG/SVGZ, PDF/.ai, .ait (opens untitled like native templates) and PNG/JPEG/GIF/WebP/TIFF/BMP; desktop, web, control channel, headless MCP and the CLI open and encode through it; open dialogs list "All readable files" plus one filter per format

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-04 11:36:51 +07:00
co-authored by Claude Opus 5.5
parent d99853ed2a
commit 4d503c0327
25 changed files with 1293 additions and 559 deletions
+37 -2
View File
@@ -12,7 +12,7 @@ mod docmenu;
mod draw2;
mod edit;
mod effectcmd;
mod file;
pub mod fileio;
mod fonts;
pub(crate) mod graph;
pub mod help;
@@ -141,7 +141,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
v.extend(edit::specs());
v.extend(clipboard::specs());
v.extend(recolor::specs());
v.extend(file::specs());
v.extend(fileio::specs());
v.extend(create::specs());
v.extend(object::specs());
v.extend(path::specs());
@@ -260,3 +260,38 @@ pub(crate) fn targets(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
pub(crate) fn ok() -> Result<Value> {
Ok(Value::Null)
}
/// A 1-based page/artboard range such as `"1-3, 5"` → 0-based indices in the order given (repeats
/// dropped). Each number must lie in `1..=count`; `"3-"` runs to the last one, `"-2"` from the first.
pub(crate) fn parse_range(s: &str, count: usize) -> std::result::Result<Vec<usize>, String> {
let num = |t: &str, open: usize| -> std::result::Result<usize, String> {
let t = t.trim();
if t.is_empty() {
return Ok(open);
}
t.parse().ok().filter(|n| (1..=count).contains(n)).ok_or_else(|| format!("range `{s}`: `{t}` is not a number from 1 to {count}"))
};
let mut out = vec![];
for part in s.split(',').map(str::trim).filter(|p| !p.is_empty()) {
let (a, b) = match part.replace('\u{2013}', "-").split_once('-') {
Some((a, b)) if a.trim().is_empty() && b.trim().is_empty() => return Err(format!("range `{s}`: `{part}` names no number")),
Some((a, b)) => (num(a, 1)?, num(b, count)?),
None => {
let n = num(part, 0)?;
(n, n)
}
};
if a > b {
return Err(format!("range `{s}`: `{part}` runs backwards"));
}
for i in a - 1..b {
if !out.contains(&i) {
out.push(i);
}
}
}
if out.is_empty() {
return Err(format!("range `{s}` is empty"));
}
Ok(out)
}