From bb44b1d7a8a9d9a09fd045424504f14df69f1973 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Fri, 19 Apr 2024 09:38:26 -0400 Subject: [PATCH] Use tables to summarize course content (#2005) This is more friendly to translation (as it can share the translation of the title). This fixes #1982. --- mdbook-course/src/course.rs | 65 +++++++++++-------------------- mdbook-course/src/markdown.rs | 52 +++++++++++++++++++++++++ mdbook-course/src/replacements.rs | 8 ++-- 3 files changed, 78 insertions(+), 47 deletions(-) diff --git a/mdbook-course/src/course.rs b/mdbook-course/src/course.rs index 0e77d933..354b7e24 100644 --- a/mdbook-course/src/course.rs +++ b/mdbook-course/src/course.rs @@ -36,10 +36,10 @@ //! top-level item are treated as further slides in the same segment. use crate::frontmatter::{split_frontmatter, Frontmatter}; -use crate::markdown::{duration, relative_link}; +use crate::markdown::{duration, Table}; use mdbook::book::{Book, BookItem, Chapter}; use std::fmt::Write; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; /// Duration, in minutes, of breaks between segments in the course. const BREAK_DURATION: u64 = 10; @@ -245,32 +245,26 @@ impl Course { /// Generate a Markdown schedule for this course, for placement at the given /// path. - pub fn schedule(&self, at_source_path: impl AsRef) -> String { + pub fn schedule(&self) -> String { let mut outline = String::from("Course schedule:\n"); for session in self { writeln!( &mut outline, - " * {} ({}, including breaks)", + " * {} ({}, including breaks)\n", session.name, duration(session.minutes()) ) .unwrap(); + let mut segments = Table::new(["Segment".into(), "Duration".into()]); for segment in session { + // Skip short segments (welcomes, wrap-up, etc.) if segment.minutes() == 0 { continue; } - writeln!( - &mut outline, - " * [{}]({}) ({})", - segment.name, - relative_link( - &at_source_path, - &segment.slides[0].source_paths[0] - ), - duration(segment.minutes()) - ) - .unwrap(); + segments + .add_row([segment.name.clone(), duration(segment.minutes())]); } + writeln!(&mut outline, "{}\n", segments).unwrap(); } outline } @@ -313,24 +307,18 @@ impl Session { /// Generate a Markdown outline for this session, for placement at the given /// path. - pub fn outline(&self, at_source_path: impl AsRef) -> String { - let mut outline = String::from("In this session:\n"); + pub fn outline(&self) -> String { + let mut segments = Table::new(["Segment".into(), "Duration".into()]); for segment in self { // Skip short segments (welcomes, wrap-up, etc.) if segment.minutes() == 0 { continue; } - writeln!( - &mut outline, - " * [{}]({}) ({})", - segment.name, - relative_link(&at_source_path, &segment.slides[0].source_paths[0]), - duration(segment.minutes()) - ) - .unwrap(); + segments.add_row([segment.name.clone(), duration(segment.minutes())]); } - writeln!(&mut outline,"\nIncluding {BREAK_DURATION} minute breaks, this session should take about {}", duration(self.minutes())).unwrap(); - outline + format!( + "Including {BREAK_DURATION} minute breaks, this session should take about {}. It contains:\n\n{}", + duration(self.minutes()), segments) } /// Return the total duration of this session. @@ -394,28 +382,19 @@ impl Segment { self.into_iter().map(|s| s.minutes()).sum() } - pub fn outline(&self, at_source_path: impl AsRef) -> String { - let mut outline = String::from("In this segment:\n"); + pub fn outline(&self) -> String { + let mut slides = Table::new(["Slide".into(), "Duration".into()]); for slide in self { if slide.minutes() == 0 { continue; } - writeln!( - &mut outline, - " * [{}]({}) ({})", - slide.name, - relative_link(&at_source_path, &slide.source_paths[0]), - duration(slide.minutes()) - ) - .unwrap(); + slides.add_row([slide.name.clone(), duration(slide.minutes())]); } - writeln!( - &mut outline, - "\nThis segment should take about {}", - duration(self.minutes()) + format!( + "This segment should take about {}. It contains:\n\n{}", + duration(self.minutes()), + slides, ) - .unwrap(); - outline } } diff --git a/mdbook-course/src/markdown.rs b/mdbook-course/src/markdown.rs index c5726f93..d22ccc90 100644 --- a/mdbook-course/src/markdown.rs +++ b/mdbook-course/src/markdown.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fmt; use std::path::Path; /// Given a source_path for the markdown file being rendered and a source_path @@ -57,6 +58,46 @@ pub fn duration(mut minutes: u64) -> String { } } +/// Table implements Display to format a two-dimensional table as markdown, +/// following https://github.github.com/gfm/#tables-extension-. +pub struct Table { + header: [String; N], + rows: Vec<[String; N]>, +} + +impl Table { + pub fn new(header: [String; N]) -> Self { + Self { header, rows: Vec::new() } + } + + pub fn add_row(&mut self, row: [String; N]) { + self.rows.push(row); + } + + fn write_row<'a, I: Iterator>( + &self, + f: &mut fmt::Formatter<'_>, + iter: I, + ) -> fmt::Result { + write!(f, "|")?; + for cell in iter { + write!(f, " {} |", cell)?; + } + write!(f, "\n") + } +} + +impl fmt::Display for Table { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.write_row(f, self.header.iter().map(|s| s.as_str()))?; + self.write_row(f, self.header.iter().map(|_| "-"))?; + for row in &self.rows { + self.write_row(f, row.iter().map(|s| s.as_str()))? + } + Ok(()) + } +} + #[cfg(test)] mod test { use super::*; @@ -152,4 +193,15 @@ mod test { fn duration_hours_mins() { assert_eq!(duration(130), "2 hours and 10 minutes") } + + #[test] + fn table() { + let mut table = Table::new(["a".into(), "b".into()]); + table.add_row(["a1".into(), "b1".into()]); + table.add_row(["a2".into(), "b2".into()]); + assert_eq!( + format!("{}", table), + "| a | b |\n| - | - |\n| a1 | b1 |\n| a2 | b2 |\n" + ); + } } diff --git a/mdbook-course/src/replacements.rs b/mdbook-course/src/replacements.rs index ebc5e522..dda51384 100644 --- a/mdbook-course/src/replacements.rs +++ b/mdbook-course/src/replacements.rs @@ -40,19 +40,19 @@ pub fn replace( let directive: Vec<_> = directive_str.split_whitespace().collect(); match directive.as_slice() { ["session", "outline"] if session.is_some() => { - session.unwrap().outline(source_path) + session.unwrap().outline() } ["segment", "outline"] if segment.is_some() => { - segment.unwrap().outline(source_path) + segment.unwrap().outline() } ["course", "outline"] if course.is_some() => { - course.unwrap().schedule(source_path) + course.unwrap().schedule() } ["course", "outline", course_name] => { let Some(course) = courses.find_course(course_name) else { return captures[0].to_string(); }; - course.schedule(source_path) + course.schedule() } _ => directive_str.to_owned(), }