//! File → Document Setup (`document.setup`): validation, one undo step, round-trips, and the //! options other commands read (typographer's quotes, superscript/subscript, SVG text export). use serde_json::{Value, json}; use vectorcraft_doc::{CharPosition, DocSetup, ExportText, GridSize, NodeId, NodeKind, ScriptMetrics, TextObject, TextStyleDef, Unit}; use vectorcraft_tools::{PointerEvent, PointerKind, ToolKey}; use super::*; fn session() -> Session { let mut s = Session::new(); s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); s } fn setup(s: &Session) -> &DocSetup { &s.doc().unwrap().doc.setup } fn text(s: &Session, id: NodeId) -> TextObject { match &s.doc().unwrap().doc.node(id).unwrap().kind { NodeKind::Text(t) => (**t).clone(), _ => panic!("not text"), } } fn create_text(s: &mut Session, t: &str) -> NodeId { NodeId(s.execute("text.create", &json!({"x": 10, "y": 50, "text": t, "size": 20})).unwrap()["id"].as_u64().unwrap()) } /// Click with the Type tool at (x, y) and type `keys` one at a time; returns the new text object. fn type_at(s: &mut Session, x: f64, y: f64, keys: &[&str]) -> NodeId { let v = ViewInfo::default(); s.select_tool("type", v).unwrap(); for k in [PointerKind::Down, PointerKind::Up] { s.pointer(&PointerEvent::new(k, x, y), v).unwrap(); } let id = s.doc().unwrap().selection.objects[0]; for k in keys { s.tool_text(k, v).unwrap(); } s.tool_key(ToolKey::Escape, Default::default(), v).unwrap(); id } #[test] fn query_reports_the_defaults_and_accepts_them_back() { let mut s = session(); let q = s.execute("document.setup", &json!({})).unwrap(); assert_eq!(q["units"], "Points"); assert_eq!(q["bleed"], json!([0.0, 0.0, 0.0, 0.0])); assert_eq!(q["gridColors"], json!(["#ffffff", "#cccccc"])); assert_eq!(q["gridColorsName"], "Light"); assert_eq!(q["flattenerPreset"], "Medium Resolution"); assert_eq!(q["backgroundContents"], "transparent"); assert_eq!(q["quotes"], json!({"double": "“”", "single": "‘’"})); assert_eq!(q["typographersQuotes"], true); assert_eq!(q["superscript"], json!({"size": 58.3, "position": 33.3})); // Feeding the report back changes nothing (and records no undo step). assert_eq!(s.execute("document.setup", &q).unwrap(), q); assert!(s.doc().unwrap().history.undo.is_empty()); assert_eq!(*setup(&s), DocSetup::default()); } #[test] fn many_settings_are_one_undo_step() { let mut s = session(); let before = s.doc().unwrap().doc.clone(); let r = s .execute( "document.setup", &json!({ "units": "Millimeters", "bleed": {"top": 9, "right": 4.5}, "gridSize": "large", "gridColors": "Blue", "simulatePaper": true, "flattenerPreset": "high", "discardWhiteOverprint": false, "outlineImages": true, "highlightSubstitutedGlyphs": true, "exportText": "appearance", "superscript": {"size": 60}, "subscript": {"position": 20}, "smallCapsSize": 75, }), ) .unwrap(); assert_eq!(r["gridColorsName"], "Blue"); let d = &s.doc().unwrap().doc; assert_eq!(d.units, Unit::Millimeters); let st = &d.setup; assert_eq!(st.bleed, [9.0, 0.0, 0.0, 4.5]); assert_eq!((st.grid_size, st.simulate_paper, st.outline_images, st.highlight_substituted_glyphs), (GridSize::Large, true, true, true)); assert_eq!((st.flattener(), st.discard_white_overprint, st.export_text), ("High Resolution", false, ExportText::Appearance)); assert_eq!((st.superscript, st.subscript), (ScriptMetrics { size: 60.0, position: 33.3 }, ScriptMetrics { size: 58.3, position: 20.0 })); assert_eq!(st.small_caps_size, 75.0); let undo: Vec<&str> = s.doc().unwrap().history.undo.iter().map(|e| e.label.as_str()).collect(); assert_eq!(undo, ["Document Setup"]); s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(*s.doc().unwrap().doc, *before); } #[test] fn bad_values_are_rejected_and_change_nothing() { let mut s = session(); for p in [ json!({"bleed": 73}), json!({"bleed": [1, 2, 3]}), json!({"bleed": {"middle": 2}}), json!({"bleed": -1}), json!({"gridSize": "huge"}), json!({"gridColors": ["#ffffff"]}), json!({"gridColors": "Plaid"}), json!({"language": "Klingon"}), json!({"quotes": {"double": "“"}}), json!({"quotes": {"triple": "“”"}}), json!({"superscript": {"size": 0}}), json!({"exportText": "pdf"}), json!({"flattenerPreset": "Ultra"}), json!({"backgroundContents": "grey"}), json!({"simulatePaper": "yes"}), json!({"units": "furlongs"}), json!({"bleed": 9, "nonsense": true}), json!([1, 2]), ] { assert!(s.execute("document.setup", &p).is_err(), "{p}"); } assert_eq!(*setup(&s), DocSetup::default()); assert!(s.doc().unwrap().history.undo.is_empty()); } #[test] fn set_units_is_an_alias() { let mut s = session(); s.execute("document.setUnits", &json!({"units": "inches"})).unwrap(); assert_eq!(s.doc().unwrap().doc.units, Unit::Inches); assert_eq!(s.execute("document.setup", &json!({})).unwrap()["units"], "Inches"); assert!(s.execute("document.setUnits", &json!({"units": "parsecs"})).is_err()); } #[test] fn language_picks_its_quotes_unless_quotes_are_given() { let mut s = session(); s.execute("document.setup", &json!({"language": "german"})).unwrap(); assert_eq!(setup(&s).language, "German"); assert_eq!(setup(&s).quotes.double, ['„', '“']); s.execute("document.setup", &json!({"language": "French", "quotes": {"double": ["“", "”"]}})).unwrap(); assert_eq!((setup(&s).quotes.double, setup(&s).quotes.single), (['“', '”'], ['‹', '›'])); } #[test] fn typographers_quotes_setting_changes_typed_quotes() { let mut s = session(); let id = type_at(&mut s, 50.0, 50.0, &["\"", "H", "i", "\"", " ", "i", "t", "'", "s"]); assert_eq!(text(&s, id).plain_text(), "“Hi” it’s"); // The document's quote style. s.execute("document.setup", &json!({"language": "German"})).unwrap(); let id = type_at(&mut s, 50.0, 150.0, &["\"", "a", "\""]); assert_eq!(text(&s, id).plain_text(), "„a“"); // Straight quotes with the option off. s.execute("document.setup", &json!({"typographersQuotes": false})).unwrap(); let id = type_at(&mut s, 50.0, 250.0, &["\"", "b", "'"]); assert_eq!(text(&s, id).plain_text(), "\"b'"); } #[test] fn smart_punctuation_uses_the_document_quotes() { let mut s = session(); s.execute("document.setup", &json!({"language": "French"})).unwrap(); let id = create_text(&mut s, "\"oui\" 'non'"); s.execute("type.smartPunctuation", &json!({"scope": "document"})).unwrap(); assert_eq!(text(&s, id).plain_text(), "«oui» ‹non›"); } #[test] fn superscript_and_small_caps_use_and_follow_the_setup_sizes() { let mut s = session(); let id = create_text(&mut s, "E=mc2"); s.execute("document.setup", &json!({"superscript": {"size": 50, "position": 40}})).unwrap(); s.execute("text.setRangeStyle", &json!({"id": id.0, "start": 4, "end": 5, "position": "superscript"})).unwrap(); s.execute("text.setFormat", &json!({"ids": [id.0], "smallCaps": true})).unwrap(); let t = text(&s, id); assert_eq!(t.runs.last().unwrap().style.position, CharPosition::Superscript(ScriptMetrics { size: 50.0, position: 40.0 })); assert!(t.runs.iter().all(|r| r.style.small_caps == Some(70.0))); let narrow = t.cached_bounds.unwrap(); // A Document Setup change restyles the text in the same undo step. let undo = s.doc().unwrap().history.undo.len(); s.execute("document.setup", &json!({"superscript": {"size": 100}, "smallCapsSize": 100})).unwrap(); assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1); let t = text(&s, id); assert_eq!(t.runs.last().unwrap().style.position, CharPosition::Superscript(ScriptMetrics { size: 100.0, position: 40.0 })); assert!(t.runs.iter().all(|r| r.style.small_caps == Some(100.0))); assert!(t.cached_bounds.unwrap().width() > narrow.width(), "bounds follow the bigger glyphs"); s.execute("text.setFormat", &json!({"ids": [id.0], "position": "normal", "smallCaps": false})).unwrap(); assert!(text(&s, id).runs.iter().all(|r| r.style.position == CharPosition::Normal && r.style.small_caps.is_none())); assert!(s.execute("text.setFormat", &json!({"ids": [id.0], "position": "sideways"})).is_err()); } #[test] fn character_styles_follow_the_setup_sizes() { let mut s = session(); let attrs = json!({"position": {"kind": "subscript", "size": 58.3, "position": 33.3}, "small_caps": 70.0}); Arc::make_mut(&mut s.doc_mut().unwrap().doc).char_styles.push(TextStyleDef { name: "Note".into(), attrs: attrs.as_object().unwrap().clone() }); s.execute("document.setup", &json!({"subscript": {"size": 40}, "smallCapsSize": 80})).unwrap(); let def = &s.doc().unwrap().doc.char_styles[0]; assert_eq!(def.attrs["position"], json!({"kind": "subscript", "size": 40.0, "position": 33.3})); assert_eq!(def.attrs["small_caps"], json!(80.0)); } #[test] fn setup_survives_save_and_load() { let mut s = session(); s.execute( "document.setup", &json!({"bleed": [9, 9, 18, 18], "gridColors": ["#ff0000", "#00ff00"], "language": "Japanese", "exportText": "appearance"}), ) .unwrap(); let d = &s.doc().unwrap().doc; let back = vectorcraft_format::load(&vectorcraft_format::save_file(d)).unwrap(); assert_eq!(back.setup, d.setup); assert_eq!(back.setup.grid_colors_name(), "Custom"); } #[test] fn svg_text_export_follows_the_export_text_setting() { let mut s = session(); create_text(&mut s, "Hello"); let svg = |s: &mut Session, p: Value| { let mut p = p; p["format"] = json!("svg"); let r = s.execute("document.export", &p).unwrap(); String::from_utf8(vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap()).unwrap() }; assert!(svg(&mut s, json!({})).contains(" 200 && g < 60 && b < 60, "a white overprint vanishes: {:?}", [r, g, b]); } #[test] fn saved_flattener_presets_and_background_contents() { let mut s = session(); s.execute("flattener.presets.save", &json!({"name": "Proofs", "preset": "high"})).unwrap(); assert_eq!(s.execute("document.setup", &json!({"flattenerPreset": "proofs"})).unwrap()["flattenerPreset"], "Proofs"); assert_eq!(s.execute("document.setup", &json!({"flattenerPreset": "Medium Resolution"})).unwrap()["flattenerPreset"], "Medium Resolution"); assert_eq!(setup(&s).flattener_preset, None, "the default is not stored"); let r = s.execute("document.setup", &json!({"backgroundContents": "white"})).unwrap(); assert_eq!(r["backgroundContents"], "white"); assert_eq!(setup(&s).background, vectorcraft_doc::Background::White); }