diff --git a/crates/automation/README.md b/crates/automation/README.md index 7a9de516c0..92fd2775c9 100644 --- a/crates/automation/README.md +++ b/crates/automation/README.md @@ -21,6 +21,9 @@ them with the image's current orientation. Full turns retain its appearance, rec and flips. Angles that are not multiples of 90 are refused without changing the document or its undo/redo history. +`page_add_text`, `content_update` and `text_edit` take per-line `aligns`. An `"inherit"` entry +follows the box-level `align`; legacy `null` entries remain accepted with the same meaning. + `Content` is `Json(Value)` or `Png { data, width, height }`. Errors are `UnknownTool`, `InvalidArgs` (the call didn't match the schema) or `Failed` (a readable message for the agent). ## Conventions diff --git a/crates/automation/src/content.rs b/crates/automation/src/content.rs index 7518072d1c..6b25c31f61 100644 --- a/crates/automation/src/content.rs +++ b/crates/automation/src/content.rs @@ -83,10 +83,10 @@ pub(crate) fn parse_align(al: &str) -> Result { /// Most entries `aligns` takes: one per paragraph, and no text has this many. const MAX_ALIGNS: usize = 4096; -/// `aligns`: each paragraph's (line's) own alignment, or null where it follows `align`. +/// `aligns`: each paragraph's (line's) own alignment, or inherit/null where it follows `align`. pub(crate) fn opt_aligns(a: &Args) -> Result>>> { let Some(v) = a.get("aligns") else { return Ok(None) }; - let items = v.as_array().ok_or_else(|| bad("aligns must be an array of left, center, right, justify or null"))?; + let items = v.as_array().ok_or_else(|| bad("aligns must be an array of left, center, right, justify, inherit or null"))?; if items.len() > MAX_ALIGNS { return Err(bad(format!("aligns takes at most {MAX_ALIGNS} entries"))); } @@ -94,8 +94,9 @@ pub(crate) fn opt_aligns(a: &Args) -> Result>>> { .iter() .map(|v| match v { Value::Null => Ok(None), + Value::String(s) if s == "inherit" => Ok(None), Value::String(s) => parse_align(s).map(Some), - _ => Err(bad("each entry of aligns is left, center, right, justify or null")), + _ => Err(bad("each entry of aligns is left, center, right, justify, inherit or null")), }) .collect::>>() .map(Some) diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 427bdc72f6..233bb101cd 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -637,7 +637,7 @@ pub fn tools() -> Vec { "font": { "type": "string", "enum": ["helvetica", "times", "courier"] }, "size": { "type": "number", "minimum": 1, "maximum": 500 }, "bold": { "type": "boolean" }, "italic": { "type": "boolean" }, "color": { "type": "string" }, "align": { "type": "string", "enum": ["left", "center", "right", "justify"] }, - "aligns": { "type": "array", "maxItems": 4096, "items": { "enum": ["left", "center", "right", "justify", null] }, "description": "Each paragraph's (line's) own alignment, by index; null follows align." }, + "aligns": { "type": "array", "maxItems": 4096, "items": { "type": "string", "enum": ["left", "center", "right", "justify", "inherit"] }, "description": "Each paragraph's (line's) own alignment, by index; inherit (or legacy null) follows align." }, "char_spacing": { "type": "number", "minimum": -500, "maximum": 500, "description": "Points after each character." }, }), &["doc", "page", "text"], @@ -660,7 +660,7 @@ pub fn tools() -> Vec { "font": { "type": "string", "enum": ["helvetica", "times", "courier"] }, "size": { "type": "number", "minimum": 1, "maximum": 500 }, "bold": { "type": "boolean" }, "italic": { "type": "boolean" }, "color": { "type": "string" }, "align": { "type": "string", "enum": ["left", "center", "right", "justify"] }, - "aligns": { "type": "array", "maxItems": 4096, "items": { "enum": ["left", "center", "right", "justify", null] }, "description": "Each paragraph's (line's) own alignment, by index; null follows align." }, + "aligns": { "type": "array", "maxItems": 4096, "items": { "type": "string", "enum": ["left", "center", "right", "justify", "inherit"] }, "description": "Each paragraph's (line's) own alignment, by index; inherit (or legacy null) follows align." }, "char_spacing": { "type": "number", "minimum": -500, "maximum": 500, "description": "Points after each character." }, "rotate": { "type": "integer" }, "flip_h": { "type": "boolean" }, "flip_v": { "type": "boolean" }, "crop": { "type": "array", "items": { "type": "number", "minimum": 0, "maximum": 0.95 }, "minItems": 4, "maxItems": 4, "description": "Fractions trimmed: left, bottom, right, top. Left and right together, and bottom and top together, trim at most 0.95." }, @@ -1429,7 +1429,7 @@ pub fn tools() -> Vec { "size": { "type": "number", "exclusiveMinimum": 0 }, "color": { "type": "string", "description": "#rrggbb" }, "align": { "type": "string", "enum": ["left", "center", "right", "justify"] }, - "aligns": { "type": "array", "maxItems": 4096, "items": { "enum": ["left", "center", "right", "justify", null] }, "description": "Each paragraph's (line's) own alignment, by index; null follows align." }, + "aligns": { "type": "array", "maxItems": 4096, "items": { "type": "string", "enum": ["left", "center", "right", "justify", "inherit"] }, "description": "Each paragraph's (line's) own alignment, by index; inherit (or legacy null) follows align." }, "underline": { "type": "boolean" }, "line_spacing": { "type": "number", "description": "Multiple of the font size (1.2 is ordinary)." }, "dx": { "type": "number", "description": "Paragraph only: move right by this many points (negative: left)." }, diff --git a/crates/automation/tests/it/automation.rs b/crates/automation/tests/it/automation.rs index aa778273da..8cff9fc4aa 100644 --- a/crates/automation/tests/it/automation.rs +++ b/crates/automation/tests/it/automation.rs @@ -664,6 +664,44 @@ fn tool_table_is_well_formed() { } } +fn assert_homogeneous_enums(schema: &Value, path: &str) { + fn json_kind(value: &Value) -> &'static str { + match value { + Value::Null => "null", + Value::Bool(_) => "boolean", + Value::Number(_) => "number", + Value::String(_) => "string", + Value::Array(_) => "array", + Value::Object(_) => "object", + } + } + + if let Some(values) = schema.get("enum").and_then(Value::as_array) { + let first = values.first().map(json_kind); + assert!(values.iter().map(json_kind).all(|kind| Some(kind) == first), "mixed enum types at {path}: {values:?}"); + } + match schema { + Value::Object(object) => { + for (key, child) in object { + assert_homogeneous_enums(child, &format!("{path}.{key}")); + } + } + Value::Array(array) => { + for (index, child) in array.iter().enumerate() { + assert_homogeneous_enums(child, &format!("{path}[{index}]")); + } + } + _ => {} + } +} + +#[test] +fn tool_schema_enums_have_one_json_type() { + for tool in tools() { + assert_homogeneous_enums(&tool.input_schema, tool.name); + } +} + #[test] fn explicit_font_and_disjoint_ranges_save_reopen_through_tools() { let dir = workdir("font-ranges"); @@ -9878,7 +9916,11 @@ fn added_text_lines_take_their_own_alignments() { let dir = workdir("paragraph-aligns"); let mut a = auto(&dir); let id = ok(&mut a, "doc_create", json!({"from":"blank","width":600,"height":400}))["doc"].as_u64().unwrap(); - ok(&mut a, "page_add_text", json!({"doc":id,"page":1,"text":"Left line\nRight line","at":[40,50],"width":500,"size":12,"aligns":[null,"right"]})); + ok( + &mut a, + "page_add_text", + json!({"doc":id,"page":1,"text":"Left line\nRight line\nInherited line","at":[40,50],"width":500,"size":12,"aligns":[null,"right","inherit"]}), + ); let rect = |a: &mut Automation, text: &str| -> Vec { let lines = ok(a, "text_lines", json!({"doc":id,"page":1})); let line = lines["lines"].as_array().unwrap().iter().find(|l| l["text"] == text).cloned().unwrap(); @@ -9886,10 +9928,13 @@ fn added_text_lines_take_their_own_alignments() { }; assert!((rect(&mut a, "Left line")[0] - 40.0).abs() < 1.0); assert!((rect(&mut a, "Right line")[2] - 540.0).abs() < 1.0, "{:?}", rect(&mut a, "Right line")); + assert!((rect(&mut a, "Inherited line")[0] - 40.0).abs() < 1.0); // The whole box centred: the right-aligned line goes with it. ok(&mut a, "content_update", json!({"doc":id,"page":1,"index":1,"align":"center"})); let right = rect(&mut a, "Right line"); assert!(((right[0] + right[2]) / 2.0 - 290.0).abs() < 1.0, "{right:?}"); + let inherited = rect(&mut a, "Inherited line"); + assert!(((inherited[0] + inherited[2]) / 2.0 - 290.0).abs() < 1.0, "{inherited:?}"); assert!(a.call("content_update", &json!({"doc":id,"page":1,"index":1,"aligns":["sideways"]})).is_err()); assert!(a.call("content_update", &json!({"doc":id,"page":1,"index":1,"aligns":"right"})).is_err()); }