1use std::borrow::Cow;
5
6use crate::Warning;
7use crate::content::{Attributes, Book, Metadata, Section};
8use crate::fonts::FontSource;
9use crate::images::Added;
10use crate::style::Stylesheets;
11
12use super::invalidate::{has_images, hyphenation};
13use super::{AddFontError, AddImageError, Session, Stale};
14
15impl Session<'_> {
16 pub fn set_content(&mut self, mut book: Book) {
22 book.assign_node_ids();
23 self.book = Cow::Owned(book);
24 self.images = has_images(&self.book);
25 self.recompile();
26 self.stale = Stale::Trace;
27 }
28
29 pub fn replace_source(&mut self, name: &str, sections: Vec<Section>) {
36 let mut sections = sections;
37 let book = self.book.to_mut();
38 let mut rebuilt = Vec::with_capacity(book.sections.len() + sections.len());
39 let mut placed = false;
40 for section in std::mem::take(&mut book.sections) {
41 if section.source.as_deref() == Some(name) {
42 if !placed {
43 rebuilt.append(&mut sections);
44 placed = true;
45 }
46 } else {
47 rebuilt.push(section);
48 }
49 }
50 if !placed {
51 rebuilt.append(&mut sections);
52 }
53 book.sections = rebuilt;
54 book.assign_node_ids();
55 self.images = has_images(&self.book);
56 self.recompile();
57 self.stale = Stale::Trace;
58 }
59
60 pub fn set_source_attributes(&mut self, name: &str, attributes: &Attributes) {
67 let book = self.book.to_mut();
68 for section in &mut book.sections {
69 if section.source.as_deref() == Some(name) {
70 section.attributes = attributes.clone();
71 }
72 }
73 self.recompile();
74 }
75
76 pub fn set_source_warnings(&mut self, warnings: Vec<Warning>) {
86 self.source_warnings = warnings;
87 }
88
89 pub fn set_style(&mut self, sheets: Stylesheets) {
92 self.sheets = Some(sheets);
93 self.load_faces();
94 self.recompile();
95 }
96
97 pub fn set_metadata(&mut self, metadata: Metadata) {
105 if hyphenation(&metadata) != hyphenation(&self.book.metadata) {
106 self.stale = self.stale.max(Stale::Break);
107 }
108 self.book.to_mut().metadata = metadata;
109 }
110
111 pub fn add_font(&mut self, source: FontSource) -> Result<Vec<u16>, AddFontError> {
121 let added = self.register_font(source);
122 self.output = None;
125 self.stale = Stale::Break;
126 self.recompile();
127 added
128 }
129
130 pub fn add_image(&mut self, url: &str, bytes: Vec<u8>) -> Result<Option<u32>, AddImageError> {
145 let added = self
146 .assets
147 .get_mut()
148 .ok_or(AddImageError::Borrowed)?
149 .add(url, bytes);
150 Ok(match added {
151 Added::Unchanged(index) => Some(index),
152 Added::Replaced {
153 index,
154 previous,
155 current,
156 } => {
157 if previous != Some(current) {
158 self.output = None;
166 self.lines.clear();
167 self.settled.clear();
168 self.stale = Stale::Trace;
169 } else if self.contours.traces(index) {
170 self.stale = self.stale.max(Stale::Trace);
171 }
172 Some(index)
173 }
174 Added::Refused => None,
175 })
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::content::{Attributes, Block, Book, NodeId, Section};
183 use crate::pages::DrawItem;
184 use crate::session::Session;
185 use crate::session::testing::{
186 MAP, book, book_with_image, gif, heading, paragraph, placed, placed_image_size, prose,
187 registry, runs, section, sheets, spelled, three_chapters,
188 };
189 use crate::style::{Source, Stylesheets};
190
191 #[test]
195 fn an_image_pushed_after_the_book_is_placed_and_indexed() {
196 let mut book = Book {
197 sections: vec![Section {
198 attributes: Default::default(),
199 blocks: vec![Block::Image {
200 id: NodeId::UNASSIGNED,
201 url: "plate.jpg".into(),
202 alt: "a map".into(),
203 attributes: Attributes::default(),
204 position: None,
205 span: None,
206 }],
207 ..Default::default()
208 }],
209 ..Default::default()
210 };
211 book.assign_node_ids();
212 let mut session =
213 Session::owning(crate::fonts::bundled_registry().expect("bundled font parses"));
214 session.set_content(book);
215 assert!(
216 session.preview().assets.is_empty(),
217 "a book whose images nobody supplied has no assets",
218 );
219
220 assert_eq!(
221 session.add_image("plate.jpg", MAP.to_vec()).unwrap(),
222 Some(0),
223 );
224 let output = session.preview();
225 assert_eq!(output.assets.len(), 1);
226 assert_eq!(output.assets[0].url, "plate.jpg");
227 let placed: Vec<&DrawItem> = output
228 .pages
229 .iter()
230 .flat_map(|page| &page.items)
231 .filter(|item| matches!(item, DrawItem::Image { .. }))
232 .collect();
233 assert_eq!(placed.len(), 1, "the map was not placed");
234 assert!(
235 output.warnings.is_empty(),
236 "a supplied image still complains: {:?}",
237 output.warnings,
238 );
239 }
240
241 #[test]
245 fn identical_bytes_at_a_registered_url_cost_nothing() {
246 let mut session = Session::owning(crate::fonts::bundled_registry().unwrap());
247 session.set_content(book_with_image("pic.gif"));
248 session.add_image("pic.gif", gif(64, 32, 0)).unwrap();
249 session.preview();
250 let before = session.stages();
251
252 assert_eq!(
253 session.add_image("pic.gif", gif(64, 32, 0)).unwrap(),
254 Some(0)
255 );
256 session.preview();
257 assert_eq!(session.stages(), before, "identical bytes cost a stage");
258 }
259
260 #[test]
264 fn a_same_size_replacement_breaks_nothing() {
265 let mut session = Session::owning(crate::fonts::bundled_registry().unwrap());
266 session.set_content(book_with_image("pic.gif"));
267 session.add_image("pic.gif", gif(64, 32, 0)).unwrap();
268 session.preview();
269 let before = session.stages();
270
271 assert_eq!(
272 session.add_image("pic.gif", gif(64, 32, 1)).unwrap(),
273 Some(0)
274 );
275 session.preview();
276 assert_eq!(
277 session.stages().lines,
278 before.lines,
279 "a same-size replacement broke lines it did not need to"
280 );
281 }
282
283 #[test]
286 fn a_resized_replacement_re_breaks_and_reaches_the_page() {
287 let mut session = Session::owning(crate::fonts::bundled_registry().unwrap());
288 session.set_content(book_with_image("pic.gif"));
289 session.add_image("pic.gif", gif(64, 32, 0)).unwrap();
290 let before_size = placed_image_size(session.preview());
291 let before_stages = session.stages();
292
293 assert_eq!(
294 session.add_image("pic.gif", gif(640, 320, 0)).unwrap(),
295 Some(0)
296 );
297 let after_size = placed_image_size(session.preview());
298 assert!(
299 session.stages().lines > before_stages.lines,
300 "a resize did not re-break the lines around it"
301 );
302 assert_ne!(
303 after_size, before_size,
304 "the new size did not reach the page"
305 );
306 }
307
308 #[test]
311 fn a_borrowed_asset_table_refuses_an_image() {
312 let mut session = Session::new(registry());
313 assert!(matches!(
314 session.add_image("plate.jpg", MAP.to_vec()),
315 Err(AddImageError::Borrowed),
316 ));
317 }
318
319 #[test]
322 fn replacing_a_source_re_breaks_only_its_own_sections() {
323 let mut session = three_chapters();
324 assert_eq!(
325 session.stages().lines,
326 3,
327 "the first pass broke every section"
328 );
329 session.replace_source("two.md", vec![section("two.md", prose("delta", 9))]);
330 session.preview();
331 assert_eq!(
332 session.stages().lines,
333 4,
334 "a section other than two.md was broken again"
335 );
336 }
337
338 #[test]
341 fn a_source_that_split_into_several_sections_is_replaced_whole() {
342 let mut session = Session::new(registry());
343 session.set_content(book(vec![
344 section("one.md", vec![heading("One"), paragraph("first")]),
345 section("one.md", vec![heading("Two"), paragraph("second")]),
346 section("two.md", vec![heading("Three"), paragraph("third")]),
347 ]));
348 session.preview();
349 session.replace_source("one.md", vec![section("one.md", vec![heading("Only")])]);
350 session.preview();
351 let sources: Vec<Option<&str>> = session
352 .book()
353 .sections
354 .iter()
355 .map(|section| section.source.as_deref())
356 .collect();
357 assert_eq!(sources, vec![Some("one.md"), Some("two.md")]);
358 assert_eq!(session.book().sections.len(), 2);
359 }
360
361 fn page_of(session: &Session, source: &str) -> Option<String> {
364 let at = session
365 .book()
366 .sections
367 .iter()
368 .position(|section| section.source.as_deref() == Some(source))
369 .expect("the book carries the source");
370 let tree = session.styles();
371 let node = tree
372 .nodes()
373 .iter()
374 .filter(|node| node.element == "section")
375 .nth(at)
376 .expect("every section is styled");
377 tree.styles()[node.style as usize].page.clone()
378 }
379
380 #[test]
383 fn a_host_names_a_section_beside_its_source() {
384 let mut session = three_chapters();
385 session.set_style(sheets(
386 "section.front { page: front } section#closing { page: back }",
387 ));
388 session.preview();
389 let before = session.book().sections.clone();
390
391 session.set_source_attributes(
392 "one.md",
393 &Attributes {
394 id: None,
395 classes: vec!["front".into()],
396 },
397 );
398 session.set_source_attributes(
399 "three.md",
400 &Attributes {
401 id: Some("closing".into()),
402 classes: Vec::new(),
403 },
404 );
405 session.preview();
406
407 assert_eq!(page_of(&session, "one.md").as_deref(), Some("front"));
408 assert_eq!(page_of(&session, "two.md").as_deref(), Some("chapter"));
409 assert_eq!(page_of(&session, "three.md").as_deref(), Some("back"));
410 for (was, is) in before.iter().zip(&session.book().sections) {
411 assert_eq!(was.blocks, is.blocks, "naming a section changed its text");
412 assert_eq!(was.span, is.span);
413 }
414
415 session.set_source_attributes("one.md", &Attributes::default());
416 assert_eq!(page_of(&session, "one.md").as_deref(), Some("chapter"));
417 }
418
419 #[test]
421 fn a_source_the_book_does_not_carry_is_appended() {
422 let mut session = Session::new(registry());
423 session.set_content(book(vec![section("one.md", vec![paragraph("first")])]));
424 session.replace_source("two.md", vec![section("two.md", vec![paragraph("second")])]);
425 session.preview();
426 assert_eq!(session.book().sections.len(), 2);
427 }
428
429 #[test]
432 fn a_moved_section_keeps_its_breaks_and_takes_new_coordinates() {
433 let mut session = Session::new(registry());
434 session.set_style(sheets("section { break-before: auto }"));
438 session.set_content(book(vec![
439 section("one.md", prose("alpha", 2)),
440 section("two.md", prose("beta", 12)),
441 ]));
442 let before = runs(session.preview(), "beta");
443 let broke = session.stages().lines;
444
445 session.replace_source("one.md", vec![section("one.md", prose("alpha", 24))]);
446 let after = runs(session.preview(), "beta");
447
448 assert_eq!(
449 session.stages().lines,
450 broke + 1,
451 "the untouched section was broken again"
452 );
453 assert!(!before.is_empty(), "the section painted nothing");
454 assert_eq!(spelled(&before), spelled(&after), "the breaks moved");
455 assert_ne!(placed(&before), placed(&after), "nothing moved");
456 }
457
458 #[test]
462 fn identical_content_set_again_breaks_nothing() {
463 let mut session = three_chapters();
464 let before = session.stages();
465 session.set_content(book(vec![
466 section("one.md", prose("alpha", 8)),
467 section("two.md", prose("beta", 8)),
468 section("three.md", prose("gamma", 8)),
469 ]));
470 session.preview();
471 assert_eq!(
472 session.stages().lines,
473 before.lines,
474 "renumbering alone cost a re-break"
475 );
476 }
477
478 #[test]
482 fn an_owning_session_takes_a_face_and_uses_it() {
483 let mut session = Session::owning(crate::fonts::bundled_registry().unwrap());
484 session.set_content(book(vec![section("one.md", prose("alpha", 2))]));
485 let faces = session.preview().fonts.len();
486
487 let mut source = crate::fonts::FontSource::from_bytes(crate::fonts::BUNDLED_FONT.to_vec())
488 .expect("the bundled face parses");
489 source.family = "borrowed garamond".into();
490 source.declared = Some(crate::fonts::FaceAttributes::REGULAR);
491 let ids = session
492 .add_font(source)
493 .expect("an owning session registers");
494 assert_eq!(ids.len(), 1);
495
496 let css = "book { font-family: 'borrowed garamond' }";
497 session.set_style(Stylesheets::parse(&[Source::author("faces.css", css)]));
498 let output = session.preview();
499 assert_eq!(
500 output.fonts.len(),
501 faces + 1,
502 "the font table did not grow with the registry"
503 );
504 assert!(
505 output
506 .pages
507 .iter()
508 .flat_map(|page| &page.items)
509 .any(|item| matches!(
510 item,
511 DrawItem::Text { font_id, .. } if *font_id == ids[0]
512 )),
513 "the face that was registered set nothing"
514 );
515 }
516
517 #[test]
521 fn a_borrowed_registry_refuses_a_face() {
522 let mut session = Session::new(registry());
523 let source = crate::fonts::FontSource::from_bytes(crate::fonts::BUNDLED_FONT.to_vec())
524 .expect("the bundled face parses");
525 assert!(matches!(
526 session.add_font(source),
527 Err(AddFontError::Borrowed)
528 ));
529 }
530
531 #[test]
534 fn reordering_the_book_breaks_nothing() {
535 let mut session = three_chapters();
536 let before = session.stages();
537 session.set_content(book(vec![
538 section("three.md", prose("gamma", 8)),
539 section("one.md", prose("alpha", 8)),
540 section("two.md", prose("beta", 8)),
541 ]));
542 session.preview();
543 assert_eq!(
544 session.stages().lines,
545 before.lines,
546 "a section that only moved was broken again"
547 );
548 }
549}