use crate::{Parser, document::InterpretedValue};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum TocMode {
Disabled,
Auto,
Left,
Right,
Preamble,
Macro,
}
impl TocMode {
pub(crate) fn from_parser(parser: &Parser) -> Self {
let value = parser.attribute_value("toc");
if value == InterpretedValue::Unset {
return Self::Disabled;
}
match value.as_maybe_str().map(str::trim) {
Some("macro") => Self::Macro,
Some("left") => Self::Left,
Some("right") => Self::Right,
Some("preamble") => Self::Preamble,
_ => Self::Auto,
}
}
pub fn is_enabled(self) -> bool {
self != Self::Disabled
}
}
pub(crate) const DEFAULT_TOCLEVELS: usize = 2;
pub(crate) const DEFAULT_TOC_TITLE: &str = "Table of Contents";
pub(crate) const DEFAULT_TOC_CLASS: &str = "toc";
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct TocConfig {
pub(crate) mode: TocMode,
pub(crate) levels: usize,
pub(crate) title: String,
pub(crate) class: String,
}
impl TocConfig {
pub(crate) fn from_parser(parser: &Parser) -> Self {
Self {
mode: TocMode::from_parser(parser),
levels: resolve_levels(parser),
title: resolve_title(parser),
class: resolve_class(parser),
}
}
#[cfg(test)]
pub(crate) fn disabled() -> Self {
Self {
mode: TocMode::Disabled,
levels: DEFAULT_TOCLEVELS,
title: DEFAULT_TOC_TITLE.to_string(),
class: DEFAULT_TOC_CLASS.to_string(),
}
}
}
fn resolve_levels(parser: &Parser) -> usize {
parser
.attribute_value("toclevels")
.as_maybe_str()
.and_then(|s| s.trim().parse::<usize>().ok())
.map(|n| n.clamp(1, 5))
.unwrap_or(DEFAULT_TOCLEVELS)
}
fn resolve_title(parser: &Parser) -> String {
match parser.attribute_value("toc-title") {
InterpretedValue::Value(v) => v,
InterpretedValue::Set => String::new(),
InterpretedValue::Unset => DEFAULT_TOC_TITLE.to_string(),
}
}
fn resolve_class(parser: &Parser) -> String {
match parser.attribute_value("toc-class") {
InterpretedValue::Value(v) if !v.trim().is_empty() => v,
_ => DEFAULT_TOC_CLASS.to_string(),
}
}
#[cfg(test)]
mod tests {
use crate::{Parser, document::TocMode};
fn doc_with(header: &str) -> crate::Document<'static> {
let src = format!("= Title\n{header}\n\n== Section\n\ncontent");
Parser::default().parse(&src)
}
#[test]
fn mode_is_disabled_when_unset() {
assert_eq!(doc_with("").toc_mode(), TocMode::Disabled);
assert!(!TocMode::Disabled.is_enabled());
}
#[test]
fn mode_resolves_each_placement() {
assert_eq!(doc_with(":toc:").toc_mode(), TocMode::Auto);
assert_eq!(doc_with(":toc: auto").toc_mode(), TocMode::Auto);
assert_eq!(doc_with(":toc: left").toc_mode(), TocMode::Left);
assert_eq!(doc_with(":toc: right").toc_mode(), TocMode::Right);
assert_eq!(doc_with(":toc: preamble").toc_mode(), TocMode::Preamble);
assert_eq!(doc_with(":toc: macro").toc_mode(), TocMode::Macro);
for mode in [
TocMode::Auto,
TocMode::Left,
TocMode::Right,
TocMode::Preamble,
TocMode::Macro,
] {
assert!(mode.is_enabled());
}
}
#[test]
fn unrecognized_mode_is_treated_as_auto() {
assert_eq!(doc_with(":toc: bogus").toc_mode(), TocMode::Auto);
}
#[test]
fn levels_default_and_overrides() {
assert_eq!(doc_with(":toc:").toc_levels(), 2);
assert_eq!(doc_with(":toc:\n:toclevels: 5").toc_levels(), 5);
assert_eq!(doc_with(":toc:\n:toclevels: 0").toc_levels(), 1);
assert_eq!(doc_with(":toc:\n:toclevels: 6").toc_levels(), 5);
assert_eq!(doc_with(":toc:\n:toclevels: nope").toc_levels(), 2);
}
#[test]
fn title_default_value_and_empty() {
assert_eq!(doc_with(":toc:").toc_title(), "Table of Contents");
assert_eq!(doc_with(":toc:\n:toc-title: My TOC").toc_title(), "My TOC");
assert_eq!(doc_with(":toc:\n:toc-title:").toc_title(), "");
}
#[test]
fn class_default_value_and_empty() {
assert_eq!(doc_with(":toc:").toc_class(), "toc");
assert_eq!(doc_with(":toc:\n:toc-class: floaty").toc_class(), "floaty");
assert_eq!(doc_with(":toc:\n:toc-class:").toc_class(), "toc");
}
}