mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-25 11:07:01 -06:00
Notes from a .pptx were read and written with their formatting, but the notes pane and Notes Page drew them as plain text (Slide::notes_text in a plain TextEdit), and any edit there saved the text back through slide.notes as TextBody::from_text, dropping every bold run, level, indent and bullet of the slide's notes. - ui-egui: a notes editor (notes.rs) shared by the notes pane and Notes Page. It draws bold, italic, underline, strikethrough and size from the runs, and each paragraph's margin, hanging indent and bullet or number (Wingdings/Symbol bullets mapped as on slides); paragraphs are laid out one by one so wrapped lines align with the text, not the bullet. Each edit is applied to a formatted draft, so typed text takes the formatting where it lands (Enter in a bulleted point starts another), and the draft is saved once the field loses focus or the slide changes. - engine: slide.notes takes `body` (formatted notes, as slide.inspect's new `notesBody`), and `text` now changes only what was retyped (model edit::replace_text); text sharing nothing with the old notes is still plain. - text: map_symbol_bullet is public for the notes editor. PDF notes pages still print the notes as plain text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
5.9 KiB
Rust
83 lines
5.9 KiB
Rust
//! Speaker notes keep their run and paragraph formatting: a bold heading, indented points with
|
|
//! custom bullets, plain lines.
|
|
|
|
use std::io::{Cursor, Read, Write};
|
|
use std::sync::Arc;
|
|
|
|
use deckcraft_model::defaults;
|
|
use deckcraft_model::text::{Bullet, ParaProps, TextBody};
|
|
|
|
const NOTES: &str = "ppt/notesSlides/notesSlide1.xml";
|
|
|
|
/// A notes slide written the way PowerPoint writes one (Slovak text): a bold heading, a plain
|
|
/// line, two level-2 points with a hanging indent and a Wingdings square bullet (one mixing a bold
|
|
/// label and plain text), a level-2 paragraph with buNone, and a closing plain line.
|
|
const NOTES_XML: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
<p:notes xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr><p:sp><p:nvSpPr><p:cNvPr id="2" name="Slide Image Placeholder 1"/><p:cNvSpPr><a:spLocks noGrp="1" noRot="1" noChangeAspect="1"/></p:cNvSpPr><p:nvPr><p:ph type="sldImg"/></p:nvPr></p:nvSpPr><p:spPr/></p:sp><p:sp><p:nvSpPr><p:cNvPr id="3" name="Notes Placeholder 2"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="body" idx="1"/></p:nvPr></p:nvSpPr><p:spPr/><p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:rPr lang="sk-SK" b="1" dirty="0"/><a:t>Metodický prehľad</a:t></a:r></a:p><a:p><a:r><a:rPr lang="sk-SK" dirty="0"/><a:t>Žiaci pracujú vo dvojiciach.</a:t></a:r></a:p><a:p><a:pPr marL="285750" lvl="1" indent="-285750"><a:buFont typeface="Wingdings" panose="05000000000000000000" pitchFamily="2" charset="2"/><a:buChar char="§"/></a:pPr><a:r><a:rPr lang="sk-SK" b="1" dirty="0"/><a:t>Cieľ: </a:t></a:r><a:r><a:rPr lang="sk-SK" dirty="0"/><a:t>vysvetliť cyklus</a:t></a:r></a:p><a:p><a:pPr marL="285750" lvl="1" indent="-285750"><a:buFont typeface="Wingdings" panose="05000000000000000000" pitchFamily="2" charset="2"/><a:buChar char="§"/></a:pPr><a:r><a:rPr lang="sk-SK" i="1" dirty="0"/><a:t>Reflexia</a:t></a:r></a:p><a:p><a:pPr marL="285750" lvl="1" indent="-285750"><a:buNone/></a:pPr><a:r><a:rPr lang="sk-SK" dirty="0"/><a:t>Bez odrážky</a:t></a:r></a:p><a:p><a:r><a:rPr lang="sk-SK" dirty="0"/><a:t>Pomôcky: počítač</a:t></a:r></a:p></p:txBody></p:sp></p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:notes>"#;
|
|
|
|
/// A deck with one notes slide, its notes replaced by [`NOTES_XML`].
|
|
fn deck() -> Vec<u8> {
|
|
let mut p = defaults::new_presentation(None);
|
|
Arc::make_mut(&mut p.slides[0]).notes = TextBody::from_text("placeholder");
|
|
let bytes = deckcraft_pptx::export(&p).expect("export");
|
|
let mut src = zip::ZipArchive::new(Cursor::new(bytes)).expect("zip");
|
|
let mut out = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
|
let mut found = false;
|
|
for i in 0..src.len() {
|
|
let mut f = src.by_index(i).expect("entry");
|
|
let name = f.name().to_string();
|
|
let mut data = vec![];
|
|
f.read_to_end(&mut data).expect("read");
|
|
if name == NOTES {
|
|
data = NOTES_XML.as_bytes().to_vec();
|
|
found = true;
|
|
}
|
|
out.start_file(name, zip::write::SimpleFileOptions::default()).expect("start");
|
|
out.write_all(&data).expect("write");
|
|
}
|
|
assert!(found, "the export has no {NOTES}");
|
|
out.finish().expect("finish").into_inner()
|
|
}
|
|
|
|
fn assert_formatted(notes: &TextBody) {
|
|
assert_eq!(notes.text(), "Metodický prehľad\nŽiaci pracujú vo dvojiciach.\nCieľ: vysvetliť cyklus\nReflexia\nBez odrážky\nPomôcky: počítač");
|
|
let [head, plain, goal, reflect, unbulleted, closing] = notes.paragraphs.as_slice() else { panic!("six paragraphs: {notes:?}") };
|
|
assert_eq!(head.runs[0].props.bold, Some(true), "bold heading");
|
|
for para in [head, plain, closing] {
|
|
assert_eq!((para.level, ¶.props), (0, &ParaProps::default()), "{:?}", para.text());
|
|
}
|
|
assert_eq!(plain.runs[0].props.bold, None);
|
|
for para in [goal, reflect] {
|
|
assert_eq!(para.level, 1, "level 2");
|
|
assert_eq!(para.props.margin_left, Some(22.5), "marL 285750 EMU");
|
|
assert_eq!(para.props.indent, Some(-22.5), "hanging indent");
|
|
assert_eq!(para.props.bullet, Some(Bullet::Char { char: "§".into() }));
|
|
assert_eq!(para.props.bullet_font.as_deref(), Some("Wingdings"));
|
|
}
|
|
assert_eq!((goal.runs[0].text.as_str(), goal.runs[0].props.bold), ("Cieľ: ", Some(true)), "bold label");
|
|
assert_eq!((goal.runs[1].text.as_str(), goal.runs[1].props.bold), ("vysvetliť cyklus", None), "then plain");
|
|
assert_eq!(reflect.runs[0].props.italic, Some(true));
|
|
assert_eq!((unbulleted.level, unbulleted.props.bullet.clone(), unbulleted.props.margin_left), (1, Some(Bullet::None), Some(22.5)));
|
|
}
|
|
|
|
#[test]
|
|
fn notes_import_keeps_bold_levels_and_indents() {
|
|
let p = deckcraft_pptx::import(&deck()).expect("import");
|
|
assert_formatted(&p.slides[0].notes);
|
|
}
|
|
|
|
#[test]
|
|
fn notes_formatting_survives_a_round_trip() {
|
|
let p = deckcraft_pptx::import(&deck()).expect("import");
|
|
let again = deckcraft_pptx::import(&deckcraft_pptx::export(&p).expect("export")).expect("reimport");
|
|
assert_formatted(&again.slides[0].notes);
|
|
// And the written XML says so, for PowerPoint.
|
|
let bytes = deckcraft_pptx::export(&again).expect("export");
|
|
let mut z = zip::ZipArchive::new(Cursor::new(bytes)).expect("zip");
|
|
let mut xml = String::new();
|
|
z.by_name(NOTES).expect("notes part").read_to_string(&mut xml).expect("utf-8");
|
|
assert!(xml.contains(r#"b="1""#), "{xml}");
|
|
assert!(xml.contains(r#"lvl="1""#) && xml.contains(r#"marL="285750""#) && xml.contains(r#"indent="-285750""#), "{xml}");
|
|
assert!(xml.contains(r#"<a:buFont typeface="Wingdings""#) && xml.contains("<a:buChar") && xml.contains("<a:buNone/>"), "{xml}");
|
|
}
|