Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_file/src/office/deck.rs

5.7 KiB, 21 runs

created by r1870400018:22869, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! A neutral deck: what a presentation *is*, free of the format it will be stored in.
2//!
3//! The third of the neutral models, beside [`oxedyne_fe2o3_text::doc`] for prose and
4//! [`crate::office::sheet`] for a grid. It is the smallest of the three, because a presentation
5//! carries the least: a sequence of slides, each a title and some bullets.
6//!
7//! # Deliberately small, and it is not an oversight
8//!
9//! There is no position, no size, no colour, no picture, no transition and no animation here. A deck
10//! that carried those would be a deck this had to lay out, and laying out a slide is the job the
11//! reader does when it opens the file -- the same argument that keeps a layout engine out of the
12//! document side. What a generator has to decide is what goes on which slide; where it sits on the
13//! slide is the template's business.
14//!
15//! # A deck is made from prose, and the shape of the prose decides the slides
16//!
17//! [`Deck::from_doc`] splits a document at its headings: each heading starts a slide and is its
18//! title, and everything until the next heading becomes the bullets. That is the convention every
19//! Markdown-to-slides tool uses, and it is a convention rather than a rule because it is the one
20//! authors already write to.
21//!
22//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
23//! Anthropic Claude
24
25use oxedyne_fe2o3_text::doc::{
26 Block,
27 Doc,
28 Inline,
29 text_of,
30};
31
32// How deep a bullet may be indented. Beyond this a reader stops distinguishing levels, and a deck
33// nested deeper than this has stopped being a deck.
34pub const MAX_LEVEL: usize = 4;
35
36/// One line of a slide's body.
37#[derive(Clone, Debug, Default, PartialEq)]
38pub struct Bullet {
39 pub level: usize, // zero being the outermost
40 pub content: Vec<Inline>,
41}
42
43#[derive(Clone, Debug, Default, PartialEq)]
44pub struct Slide {
45 pub title: Option<Vec<Inline>>,
46 pub bullets: Vec<Bullet>, // the body
47 pub notes: Option<String>, // the speaker's, which are not on the slide and are not lost either
48}
49
50impl Slide {
51
52 pub fn titled(title: &str) -> Self {
53 Self {
54 title: Some(vec![Inline::Text(title.to_string())]),
55 bullets: Vec::new(),
56 notes: None,
57 }
58 }
59
60 /// The slide's words, title and bullets alike.
61 pub fn text_of(&self) -> String {
62 let mut out = String::new();
63 if let Some(t) = &self.title {
64 out.push_str(&text_of(t));
65 }
66 for b in &self.bullets {
67 if !out.is_empty() {
68 out.push('\n');
69 }
70 out.push_str(&text_of(&b.content));
71 }
72 out
73 }
74
75 /// A slide carrying only speaker's notes is empty, because nothing is on it.
76 pub fn is_empty(&self) -> bool {
77 self.title.is_none() && self.bullets.is_empty()
78 }
79}
80
81/// A presentation: the slides it holds, in order.
82#[derive(Clone, Debug, Default, PartialEq)]
83pub struct Deck {
84 pub slides: Vec<Slide>,
85}
86
87impl Deck {
88
89 pub fn new() -> Self {
90 Self::default()
91 }
92
93 /// The deck a document makes, split at its headings.
94 ///
95 /// **Every heading starts a slide**, whatever its level, and is that slide's title. Not the
96 /// shallowest level, which is what this did first and which was wrong: a document written
97 /// `# Title` and then `## Section` twice is one slide holding everything, and the author who
98 /// wrote three headings expected three slides.
99 ///
100 /// The rule that borrowed the shallowest level was
101 /// [`Doc::top_heading`](oxedyne_fe2o3_text::doc::Doc::top_heading)'s, and it is right there and
102 /// wrong here. That one asks "which heading is the document's TITLE", where the level says where
103 /// the prose came from rather than what it means. This asks "where does a slide end", and the
104 /// answer an author intends is: at the next heading. Predictable beats clever, and a deck of
105 /// slightly too many slides is a deck somebody merges in a minute.
106 pub fn from_doc(doc: &Doc) -> Self {
107 let mut deck = Self::new();
108 let mut slide = Slide::default();
109 for block in &doc.blocks {
110 match block {
111 Block::Heading { content, .. } => {
112 if !slide.is_empty() {
113 deck.slides.push(std::mem::take(&mut slide));
114 }
115 slide.title = Some(content.clone());
116 }
117 other => gather(other, 0, &mut slide.bullets),
118 }
119 }
120 if !slide.is_empty() {
121 deck.slides.push(slide);
122 }
123 deck
124 }
125}
126
127/// Adds a block's lines to a slide's bullets, at a depth.
128fn gather(block: &Block, level: usize, out: &mut Vec<Bullet>) {
129 let level = level.min(MAX_LEVEL);
130 match block {
131 Block::Para(content) => out.push(Bullet { level, content: content.clone() }),
132 // A heading nested inside a list or a quotation is a line on the slide. One at the top of
133 // the document is a new slide, and `from_doc` takes those before they reach here.
134 Block::Heading { content, .. } => {
135 out.push(Bullet { level, content: content.clone() })
136 }
137 Block::List { items, .. } => {
138 for item in items {
139 for (i, b) in item.iter().enumerate() {
140 // The first block of an item is the item; anything after it is nested under it.
141 gather(b, level + usize::from(i > 0), out);
142 }
143 }
144 }
145 Block::Quote(inner) => {
146 for b in inner {
147 gather(b, level, out);
148 }
149 }
150 Block::Div { content, .. } => {
151 for b in content {
152 gather(b, level, out);
153 }
154 }
155 // A listing goes on a slide as its lines. It is not prose and it is not nothing, and a deck
156 // generated from a technical document is mostly this.
157 Block::Code { text, .. } => {
158 for line in text.lines() {
159 out.push(Bullet {
160 level,
161 content: vec![Inline::Code(line.to_string())],
162 });
163 }
164 }
165 // A table on a slide would need a table on a slide, which is layout. Its rows go on as lines,
166 // which says what it says and is honest about not being a table.
167 Block::Table { head, rows, .. } => {
168 for row in head.iter().chain(rows) {
169 out.push(Bullet {
170 level,
171 content: vec![Inline::Text(row.text_of())],
172 });
173 }
174 }
175 Block::Rule => {}
176 }
177}