use serde::Deserialize;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodeColumns {
Single,
Auto,
TwoUp,
}
#[derive(Debug, Default, Deserialize, Clone)]
pub struct FrontMatter {
pub title: Option<String>,
pub subtitle: Option<String>,
pub author: Option<String>,
pub date: Option<String>,
pub theme: Option<String>,
pub aspect: Option<String>,
pub font: Option<String>,
pub layout: Option<String>,
pub math: Option<String>,
pub math_macros: Option<BTreeMap<String, String>>,
#[serde(alias = "math-scale")]
pub math_scale: Option<f32>,
#[serde(alias = "math-block-align")]
pub math_block_align: Option<String>,
#[serde(alias = "math-max-height")]
pub math_max_height: Option<f32>,
pub doc_style: Option<String>,
pub break_mode: Option<String>,
pub break_fill: Option<f32>,
pub table_fit: Option<String>,
#[serde(alias = "code-theme")]
pub code_theme: Option<String>,
#[serde(alias = "code-columns")]
pub code_columns: Option<String>,
#[serde(default)]
pub toc: bool,
pub logo: Option<String>,
pub transition: Option<String>,
pub transition_duration: Option<f32>,
pub direction: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct Run {
pub text: String,
pub bold: bool,
pub italic: bool,
pub code: bool,
pub strike: bool,
pub link: Option<String>,
}
impl Run {
pub fn plain(s: impl Into<String>) -> Self {
Run {
text: s.into(),
..Default::default()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColumnAlign {
Left,
Center,
Right,
}
#[derive(Debug, Clone)]
pub struct ListItem {
pub runs: Vec<Run>,
pub level: u8,
pub ordered: bool,
}
impl ListItem {
pub fn is_task(&self) -> bool {
self.runs
.first()
.map(|r| r.text.starts_with('☐') || r.text.starts_with('☑'))
.unwrap_or(false)
}
}
#[derive(Debug, Clone)]
pub enum Block {
Paragraph(Vec<Run>),
Heading { level: u8, runs: Vec<Run> },
List(Vec<ListItem>),
CodeBlock {
lang: Option<String>,
title: Option<String>,
lines: Vec<String>,
line_numbers: bool,
start_line: usize,
columns: Option<CodeColumns>,
include_error: Option<String>,
},
Quote(Vec<Vec<Run>>),
Table {
headers: Vec<Vec<Run>>,
rows: Vec<Vec<Vec<Run>>>,
aligns: Vec<ColumnAlign>,
},
ColumnBreak,
Columns { left: Vec<Block>, right: Vec<Block> },
Image {
src: String,
alt: String,
width_pct: Option<u8>,
},
Footnotes(Vec<ListItem>),
}
#[derive(Debug, Clone)]
pub enum SlideKind {
Title {
subtitle: Option<String>,
author: Option<String>,
date: Option<String>,
},
Section,
Content,
}
#[derive(Debug, Clone)]
pub struct Slide {
pub kind: SlideKind,
pub title: String,
pub blocks: Vec<Block>,
pub notes: Option<String>,
pub bg_image: Option<String>,
pub layout_hint: Option<String>,
}
impl Slide {
pub fn full_page_image(&self) -> Option<(&str, &str, Option<u8>)> {
if self.layout_hint.as_deref() != Some("image-full") {
return None;
}
let mut image = None;
for block in &self.blocks {
match block {
Block::Image {
src,
alt,
width_pct,
} => {
if image.is_some() {
return None;
}
image = Some((src.as_str(), alt.as_str(), *width_pct));
}
Block::Footnotes(_) => {}
_ => return None,
}
}
image
}
pub fn full_page_code(&self) -> Option<(&[String], Option<&str>)> {
if self.layout_hint.as_deref() != Some("text-full") {
return None;
}
let mut code = None;
for block in &self.blocks {
match block {
Block::CodeBlock { lang, lines, .. } => {
if code.is_some() {
return None;
}
code = Some((lines.as_slice(), lang.as_deref()));
}
Block::Footnotes(_) => {}
_ => return None,
}
}
code
}
}
pub fn runs_text(runs: &[Run]) -> String {
let mut s = String::new();
for r in runs {
s.push_str(&r.text);
}
s
}