Merge PR #218 from LloydNicholson: Read Markdown outlines as Markdown (#211)

# Conflicts:
#	crates/engine/src/tests.rs
This commit is contained in:
Brandon Thomas
2026-10-14 12:58:00 -04:00
6 changed files with 183 additions and 31 deletions
+5 -1
View File
@@ -89,7 +89,11 @@ pub fn open_presentation(name: &str, bytes: &[u8]) -> Result<Presentation> {
if lower.ends_with(".txt") || lower.ends_with(".md") {
let text = String::from_utf8_lossy(bytes);
let mut p = defaults::blank_presentation(defaults::WIDE, Default::default(), false);
deckcraft_format::outline_to_slides(&mut p, &text);
if lower.ends_with(".md") {
deckcraft_format::markdown_to_slides(&mut p, &text);
} else {
deckcraft_format::outline_to_slides(&mut p, &text);
}
if p.slides.is_empty() {
p = defaults::new_presentation(None);
}
+12 -3
View File
@@ -47,7 +47,15 @@ pub fn specs() -> Vec<CommandSpec> {
),
cmd!("section.removeAll", "Remove All Sections", ["Home", "Slides", "Section"], None, "{}", has_slide, section_remove_all),
cmd!("section.move", "Move Section", [], None, "{index, to}", has_slide, section_move),
cmd!("slide.fromOutline", "Slides from Outline…", ["Insert", "Slides From"], None, "{text}", has_doc, from_outline),
cmd!(
"slide.fromOutline",
"Slides from Outline…",
["Insert", "Slides From"],
None,
"{text, markdown?: bool (headings are slides, list items bullets)}",
has_doc,
from_outline
),
]
}
@@ -584,10 +592,11 @@ fn section_move(s: &mut Session, p: &Value) -> Result<Value> {
fn from_outline(s: &mut Session, p: &Value) -> Result<Value> {
let text = str_param(p, "text").unwrap_or("").to_string();
let markdown = bool_param(p, "markdown").unwrap_or(false);
s.edit(|doc, sel| {
let before = doc.slides.len();
let n = deckcraft_format::outline_to_slides(doc, &text);
// outline_to_slides appends; move them after the current slide.
let n = if markdown { deckcraft_format::markdown_to_slides(doc, &text) } else { deckcraft_format::outline_to_slides(doc, &text) };
// both append; move them after the current slide.
let at = (sel.slide + 1).min(before);
let new: Vec<_> = doc.slides.drain(before..).collect();
for (k, sl) in new.into_iter().enumerate() {
+55
View File
@@ -903,3 +903,58 @@ fn cell_fill_walks_only_the_cells_a_huge_selection_covers() {
let fills: Vec<bool> = [(0, 0), (0, 1), (1, 0), (1, 1)].iter().map(|&(r, c)| t.cell(r, c).is_some_and(|x| x.fill.is_some())).collect();
assert_eq!(fills, [false, false, false, true]);
}
/// Each slide as (title, [(level, bullet text)]).
fn outline_of(p: &deckcraft_model::Presentation) -> Vec<(String, Vec<(u8, String)>)> {
p.slides
.iter()
.map(|s| {
let body =
s.shapes.get(1).and_then(|b| b.text.as_ref()).map(|t| t.paragraphs.iter().map(|p| (p.level, p.text())).collect()).unwrap_or_default();
(s.title(), body)
})
.collect()
}
const MD_OUTLINE: &str = "# Intro\n- Point one\n- Point two\n - Sub point\n\n# Second\n- More\n";
fn md_expected() -> Vec<(String, Vec<(u8, String)>)> {
vec![
("Intro".into(), vec![(0, "Point one".into()), (0, "Point two".into()), (1, "Sub point".into())]),
("Second".into(), vec![(0, "More".into())]),
]
}
#[test]
fn markdown_outlines_take_headings_as_slides_and_lists_as_bullets() {
let p = crate::cmd::file::open_presentation("outline.md", MD_OUTLINE.as_bytes()).unwrap();
assert_eq!(outline_of(&p), md_expected());
// numbered lists, `*` / `+` bullets, text under a heading, and closing #s
let p = crate::cmd::file::open_presentation("o.md", b"## Plan ##\n1. First\n2) Second\n* star\n+ plus\nA plain line\n").unwrap();
assert_eq!(
outline_of(&p),
vec![("Plan".into(), vec![(0, "First".into()), (0, "Second".into()), (0, "star".into()), (0, "plus".into()), (0, "A plain line".into())])]
);
}
#[test]
fn slides_from_a_markdown_outline_file() {
let mut s = session();
s.execute("slide.fromOutline", &json!({"text": MD_OUTLINE, "markdown": true})).unwrap();
let doc = &s.doc().unwrap().doc;
assert_eq!(outline_of(doc)[1..], md_expected()[..]);
}
#[test]
fn plain_outlines_are_unchanged() {
// .txt keeps the indentation format, and so does a .md without headings
let indented = "Intro\n\tPoint one\n\t\tSub point\nSecond\n\tMore\n";
let want =
vec![("Intro".to_string(), vec![(0, "Point one".to_string()), (1, "Sub point".to_string())]), ("Second".into(), vec![(0, "More".into())])];
for name in ["outline.txt", "outline.md"] {
let p = crate::cmd::file::open_presentation(name, indented.as_bytes()).unwrap();
assert_eq!(outline_of(&p), want, "{name}");
}
let p = crate::cmd::file::open_presentation("outline.txt", MD_OUTLINE.as_bytes()).unwrap();
assert_eq!(p.slides.first().map(|s| s.title()).as_deref(), Some("# Intro"));
}