mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-29 04:56:58 -06:00
Merge pull request #1118
This commit is contained in:
@@ -6,6 +6,7 @@ use deckcraft_model::resolve;
|
||||
use deckcraft_model::text::{Align, Anchor, AutoFit, Bullet, Caps, ParaProps, RunProps, Spacing, Strike, TabStop, TextDir};
|
||||
use deckcraft_model::{ColorRef, ColorTransform, Effects, Fill, Line, SchemeSlot, Shape, ShapeKind};
|
||||
use serde_json::{Value, json};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::text::{format_body, format_para, format_run};
|
||||
use super::{
|
||||
@@ -64,6 +65,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_text_or_shapes,
|
||||
language
|
||||
),
|
||||
cmd!(
|
||||
"format.languageAll",
|
||||
"Language for All Text",
|
||||
["Review", "Language"],
|
||||
None,
|
||||
"{lang?, noProof?: bool} (proofing language of every text in the presentation: slides, tables, notes, masters and layouts, and the default for new text; one undo step)",
|
||||
has_doc,
|
||||
language_all
|
||||
),
|
||||
cmd!("format.highlight", "Text Highlight Color", ["Home", "Font"], None, "{color? (absent = none)}", has_text_or_shapes, highlight),
|
||||
cmd!(
|
||||
"format.case",
|
||||
@@ -410,16 +420,17 @@ fn text_transparency(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
r.fill = Some(f);
|
||||
})
|
||||
}
|
||||
fn language(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// The `lang` and `noProof` params of the language commands, checked: a run-props setter.
|
||||
fn language_edit(id: &str, p: &Value) -> Result<impl Fn(&mut RunProps) + Send + Sync + 'static> {
|
||||
let lang = str_param(p, "lang").map(str::to_string);
|
||||
let no_proof = bool_param(p, "noProof");
|
||||
if lang.is_none() && no_proof.is_none() {
|
||||
return Err(bad("format.language", "give `lang` or `noProof`"));
|
||||
return Err(bad(id, "give `lang` or `noProof`"));
|
||||
}
|
||||
if lang.as_deref().is_some_and(|l| l.is_empty() || l.len() > 35 || !l.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')) {
|
||||
return Err(bad("format.language", "`lang` is a language tag such as en-US"));
|
||||
return Err(bad(id, "`lang` is a language tag such as en-US"));
|
||||
}
|
||||
format_run(s, &move |r| {
|
||||
Ok(move |r: &mut RunProps| {
|
||||
if let Some(l) = &lang {
|
||||
r.lang = Some(l.clone());
|
||||
}
|
||||
@@ -428,6 +439,61 @@ fn language(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
})
|
||||
}
|
||||
/// Review › Language on the selected text (or the selected shapes' text).
|
||||
fn language(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let f = language_edit("format.language", p)?;
|
||||
format_run(s, &f)
|
||||
}
|
||||
/// Every run in the deck, and the text styles new text inherits from, in one language.
|
||||
fn language_all(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let f = language_edit("format.languageAll", p)?;
|
||||
/// Runs, end marks and the body's own list style levels (never creating one).
|
||||
fn body(t: &mut deckcraft_model::text::TextBody, f: &dyn Fn(&mut RunProps)) {
|
||||
ed::format_all(t, f);
|
||||
t.list_style.levels.iter_mut().flatten().for_each(|lv| f(&mut lv.run));
|
||||
}
|
||||
/// Every text body in `list`: shapes, table cells and groups (nested at most 64 deep).
|
||||
fn shapes(list: &mut [Shape], f: &dyn Fn(&mut RunProps), depth: usize) {
|
||||
if depth > 64 {
|
||||
return;
|
||||
}
|
||||
for sh in list {
|
||||
if let Some(t) = &mut sh.text {
|
||||
body(t, f);
|
||||
}
|
||||
match &mut sh.kind {
|
||||
deckcraft_model::ShapeKind::Table(t) => t.rows.iter_mut().flat_map(|r| &mut r.cells).for_each(|c| body(&mut c.text, f)),
|
||||
deckcraft_model::ShapeKind::Group { children, .. } => shapes(children, f, depth + 1),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A master or presentation text style's levels: what new text inherits.
|
||||
fn styles(l: &mut deckcraft_model::text::ListStyle, f: &dyn Fn(&mut RunProps)) {
|
||||
// Level 1 always, so a deck without a default text style gets one.
|
||||
if l.level(0).is_none() {
|
||||
l.set(0, Default::default());
|
||||
}
|
||||
l.levels.iter_mut().flatten().for_each(|lv| f(&mut lv.run));
|
||||
}
|
||||
s.edit(|doc, _| {
|
||||
for sl in &mut doc.slides {
|
||||
let sl = Arc::make_mut(sl);
|
||||
shapes(&mut sl.shapes, &f, 0);
|
||||
body(&mut sl.notes, &f);
|
||||
}
|
||||
let extra = [doc.notes_master.as_mut(), doc.handout_master.as_mut()];
|
||||
for m in doc.masters.iter_mut().chain(extra.into_iter().flatten()) {
|
||||
let m = Arc::make_mut(m);
|
||||
shapes(&mut m.shapes, &f, 0);
|
||||
m.layouts.iter_mut().for_each(|l| shapes(&mut l.shapes, &f, 0));
|
||||
[&mut m.title_style, &mut m.body_style, &mut m.other_style].into_iter().for_each(|l| styles(l, &f));
|
||||
}
|
||||
styles(&mut doc.default_text_style, &f);
|
||||
Ok(())
|
||||
})?;
|
||||
ok()
|
||||
}
|
||||
fn highlight(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let c = color_param(p, "color");
|
||||
format_run(s, &move |r| r.highlight = c.clone())
|
||||
|
||||
@@ -4475,6 +4475,64 @@ fn language_marks_text_and_no_proof_and_repeats_are_reported() {
|
||||
assert!(s.execute("format.language", &json!({"lang": "en US!"})).is_err());
|
||||
}
|
||||
|
||||
/// Review › Language › Whole presentation: every run, placeholder list style and default text style
|
||||
/// takes the language, with nothing selected, in one undo step.
|
||||
#[test]
|
||||
fn language_all_marks_every_text_in_the_deck_with_nothing_selected() {
|
||||
crate::spell::install("xx-AL", crate::spell::Dictionary::parse(b"", b"2\nhello\nworld\n"));
|
||||
let mut s = session();
|
||||
let a =
|
||||
s.execute("shape.insert", &json!({"preset": "rect", "rect": [100, 100, 300, 100], "text": "hello wurld"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("edit.select", &json!({"ids": [a]})).unwrap();
|
||||
s.execute("format.language", &json!({"lang": "de-DE"})).unwrap();
|
||||
s.execute("slide.new", &json!({"title": "world hellow"})).unwrap();
|
||||
s.execute("insert.table", &json!({"rows": 1, "cols": 1})).unwrap();
|
||||
s.execute("edit.select", &json!({"ids": []})).unwrap();
|
||||
// A layout placeholder with its own list style (a:lstStyle) in another language.
|
||||
let placeholder = s
|
||||
.edit(|doc, _| {
|
||||
let m = std::sync::Arc::make_mut(doc.masters.first_mut().unwrap());
|
||||
let sh = m.layouts[0].shapes.iter_mut().find(|sh| sh.text.is_some()).unwrap();
|
||||
let lv = deckcraft_model::text::LevelStyle {
|
||||
run: deckcraft_model::RunProps { lang: Some("de-DE".into()), ..Default::default() },
|
||||
..Default::default()
|
||||
};
|
||||
sh.text.as_mut().unwrap().list_style.set(0, lv);
|
||||
Ok(sh.id)
|
||||
})
|
||||
.unwrap();
|
||||
s.execute("format.languageAll", &json!({"lang": "xx-AL"})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let ph = deckcraft_model::find_shape(&st.doc.masters[0].layouts[0].shapes, placeholder).unwrap();
|
||||
assert_eq!(ph.text.as_ref().unwrap().list_style.level(0).unwrap().run.lang.as_deref(), Some("xx-AL"));
|
||||
// A body without a list style doesn't get one.
|
||||
let boxed = st.doc.slides.iter().find_map(|sl| deckcraft_model::find_shape(&sl.shapes, deckcraft_model::ShapeId(a as u32))).unwrap();
|
||||
assert!(boxed.text.as_ref().unwrap().list_style.is_empty());
|
||||
let mut langs = vec![];
|
||||
for sl in &st.doc.slides {
|
||||
deckcraft_model::walk(&sl.shapes, &mut |sh, _| {
|
||||
langs.extend(sh.text.iter().flat_map(|t| &t.paragraphs).flat_map(|p| &p.runs).map(|r| r.props.lang.clone()));
|
||||
if let deckcraft_model::ShapeKind::Table(t) = &sh.kind {
|
||||
langs.extend(t.rows.iter().flat_map(|r| &r.cells).flat_map(|c| &c.text.paragraphs).map(|p| p.end_props.lang.clone()));
|
||||
}
|
||||
});
|
||||
}
|
||||
assert!(!langs.is_empty() && langs.iter().all(|l| l.as_deref() == Some("xx-AL")), "{langs:?}");
|
||||
// The defaults new text inherits.
|
||||
assert_eq!(st.doc.default_text_style.level(0).unwrap().run.lang.as_deref(), Some("xx-AL"));
|
||||
assert!(st.doc.masters.iter().all(|m| m.body_style.level(0).is_some_and(|l| l.run.lang.as_deref() == Some("xx-AL"))));
|
||||
let words: Vec<String> =
|
||||
s.execute("review.spelling", &json!({})).unwrap().as_array().unwrap().iter().map(|w| w["word"].as_str().unwrap().to_string()).collect();
|
||||
assert_eq!(words, vec!["wurld", "hellow"]);
|
||||
// One undo step puts the per-box language back.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let sh = st.doc.slides.iter().find_map(|sl| deckcraft_model::find_shape(&sl.shapes, deckcraft_model::ShapeId(a as u32))).unwrap();
|
||||
assert_eq!(sh.text.as_ref().unwrap().paragraphs[0].runs[0].props.lang.as_deref(), Some("de-DE"));
|
||||
assert!(s.execute("format.languageAll", &json!({})).is_err());
|
||||
assert!(s.execute("format.languageAll", &json!({"lang": "en US!"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_caret_takes_the_colour_of_the_text() {
|
||||
let caret = |theme: &str| {
|
||||
|
||||
Reference in New Issue
Block a user