use std::fmt::Display;
use std::str::FromStr;
use serde::Deserialize;
use crate::directives::{Directive, DirectiveVisibility};
fn generate_decoration(ch: char, len: usize) -> String {
let mut decoration = String::with_capacity(len);
for _ in 0..len {
decoration.push(ch);
}
decoration
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Hash, PartialEq, Eq)]
pub(crate) enum Format {
#[serde(rename(deserialize = "md"))]
Md,
#[default]
#[serde(rename(deserialize = "rst"))]
Rst,
}
impl Format {
const MD_VALUES: [&'static str; 3] = ["md", ".md", "markdown"];
const RST_VALUES: [&'static str; 3] = ["rst", ".rst", "restructuredtext"];
pub fn extension(&self) -> &'static str {
match self {
Format::Md => Self::MD_VALUES[0],
Format::Rst => Self::RST_VALUES[0],
}
}
pub(crate) fn make_inline_code<D: Display>(&self, text: D) -> String {
match self {
Format::Md => format!("`{text}`"),
Format::Rst => format!("``{text}``"),
}
}
pub(crate) fn make_title(&self, title: &str) -> Vec<String> {
match self {
Format::Md => {
vec![format!("# {title}"), String::new()]
}
Format::Rst => {
let decoration = generate_decoration('=', title.len());
vec![
decoration.clone(),
title.to_string(),
decoration,
String::new(),
]
}
}
}
pub(crate) fn format_directive<T>(
&self,
directive: T,
max_visibility: &DirectiveVisibility,
) -> Vec<String>
where
T: RstDirective + MdDirective,
{
match self {
Format::Md => {
let fence_size = directive.fence_size();
directive.get_md_text(fence_size, max_visibility)
}
Format::Rst => directive.get_rst_text(0, max_visibility),
}
}
}
impl FromStr for Format {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let value_lower = value.to_lowercase();
if Self::RST_VALUES.contains(&&*value_lower) {
Ok(Format::Rst)
}
else if Self::MD_VALUES.contains(&&*value_lower) {
Ok(Format::Md)
}
else {
Err(format!("Not a valid format value: {value}"))
}
}
}
pub(crate) trait RstDirective {
const INDENT: &'static str = " ";
fn get_rst_text(self, level: usize, max_visibility: &DirectiveVisibility) -> Vec<String>;
fn make_indent(level: usize) -> String {
let mut indent = String::with_capacity(Self::INDENT.len() * level);
for _ in 0..level {
indent += Self::INDENT;
}
indent
}
fn make_content_indent(level: usize) -> String {
Self::make_indent(level + 1)
}
fn make_rst_header<O: RstOption, D: Display, E: Display>(
directive: D,
name: E,
options: &[O],
level: usize,
) -> Vec<String> {
let indent = &Self::make_indent(level);
let option_indent = &Self::make_indent(level + 1);
let mut header = Vec::with_capacity(3 + options.len());
header.push(String::new());
header.push(
format!("{indent}.. rust:{directive}:: {name}")
.trim_end()
.to_string(),
);
options
.iter()
.filter_map(|o| o.get_rst_text(option_indent))
.for_each(|t| header.push(t));
header.push(String::new());
header
}
fn make_rst_toctree<I: Display, T: Iterator<Item = I>>(
indent: &str,
caption: &str,
max_depth: Option<u8>,
tree: T,
) -> Vec<String> {
let tree: Vec<I> = tree.collect();
if tree.is_empty() {
return Vec::new();
}
let mut toc_tree = vec![
String::new(),
format!("{indent}.. rubric:: {caption}"),
format!("{indent}.. toctree::"),
];
if let Some(md) = max_depth {
toc_tree.push(format!("{indent}{}:maxdepth: {md}", Self::INDENT));
}
toc_tree.push(String::new());
for item in tree {
toc_tree.push(format!("{indent}{}{item}", Self::INDENT));
}
toc_tree.push(String::new());
toc_tree
}
fn make_rst_section(
section: &str,
level: usize,
items: Vec<Directive>,
max_visibility: &DirectiveVisibility,
) -> Vec<String> {
let indent = Self::make_content_indent(level);
let mut section = vec![
String::new(),
format!("{indent}.. rubric:: {section}"),
String::new(),
];
for item in items {
section.extend(item.get_rst_text(level + 1, max_visibility))
}
if section.len() > 3 {
section
}
else {
Vec::new()
}
}
fn make_rst_list(indent: &str, title: &str, items: &[(String, Vec<String>)]) -> Vec<String> {
if items.is_empty() {
return vec![];
}
let mut text = vec![format!("{indent}.. rubric:: {title}"), String::new()];
for (item, content) in items {
text.push(format!("{indent}* :rust:any:`{item}`"));
text.extend(content.iter().map(|l| format!("{indent} {l}")));
}
text
}
}
pub(crate) trait MdDirective {
const DEFAULT_FENCE_SIZE: usize = 4;
fn get_md_text(self, fence_size: usize, max_visibility: &DirectiveVisibility) -> Vec<String>;
fn make_fence(fence_size: usize) -> String {
if fence_size < 3 {
panic!("Invalid fence size {fence_size}. Must be >= 3");
}
generate_decoration(':', fence_size)
}
fn calc_fence_size(items: &[Directive]) -> usize {
match items.iter().map(Directive::fence_size).max() {
Some(s) => s + 1,
None => Self::DEFAULT_FENCE_SIZE,
}
}
fn make_md_header<O: MdOption, D: Display, E: Display>(
directive: D,
name: E,
options: &[O],
fence: &str,
) -> Vec<String> {
let mut header = Vec::with_capacity(2 + options.len());
header.push(
format!("{fence}{{rust:{directive}}} {name}")
.trim_end()
.to_string(),
);
options
.iter()
.filter_map(|o| o.get_md_text())
.for_each(|t| header.push(t));
header.push(String::new());
header
}
fn make_md_toctree<I: Display, T: Iterator<Item = I>>(
fence_size: usize,
caption: &str,
max_depth: Option<u8>,
tree: T,
) -> Vec<String> {
let tree: Vec<I> = tree.collect();
if tree.is_empty() {
return Vec::new();
}
let fence = Self::make_fence(fence_size);
let mut toc_tree = vec![
String::new(),
format!("{fence}{{rubric}} {caption}"),
fence.clone(),
format!("{fence}{{toctree}}"),
];
if let Some(md) = max_depth {
toc_tree.push(format!(":maxdepth: {md}"));
}
toc_tree.push(String::new());
for item in tree {
toc_tree.push(item.to_string());
}
toc_tree.push(fence);
toc_tree
}
fn make_md_section(
section: &str,
fence_size: usize,
items: Vec<Directive>,
max_visibility: &DirectiveVisibility,
) -> Vec<String> {
let fence = Self::make_fence(3);
let mut section = vec![
String::new(),
format!("{fence}{{rubric}} {section}"),
fence,
String::new(),
];
for item in items {
section.extend(item.get_md_text(fence_size - 1, max_visibility))
}
if section.len() > 4 {
section
}
else {
Vec::new()
}
}
fn make_md_list(
fence_size: usize,
title: &str,
items: &[(String, Vec<String>)],
) -> Vec<String> {
if items.is_empty() {
return vec![];
}
let fence = Self::make_fence(fence_size);
let mut text = vec![format!("{fence}{{rubric}} {title}"), fence, String::new()];
for (item, content) in items {
text.push(format!(
"* {{rust:any}}`{item}`{}",
if content.is_empty() {
""
}
else {
" "
}
));
text.extend(content.iter().map(|l| format!(" {l}")));
}
text
}
fn fence_size(&self) -> usize {
Self::DEFAULT_FENCE_SIZE
}
}
pub(crate) trait RstOption {
fn get_rst_text(&self, indent: &str) -> Option<String>;
}
pub(crate) trait MdOption {
fn get_md_text(&self) -> Option<String>;
}
pub(crate) trait RstContent {
fn get_rst_text(self, indent: &str) -> Vec<String>;
}
impl<T> RstContent for T
where
T: IntoIterator<Item = String>,
{
fn get_rst_text(self, indent: &str) -> Vec<String> {
self.into_iter().map(|s| format!("{indent}{s}")).collect()
}
}
pub(crate) trait MdContent {
fn get_md_text(self) -> Vec<String>;
}
impl<T> MdContent for T
where
T: IntoIterator<Item = String>,
{
fn get_md_text(self) -> Vec<String> {
let mut text = vec![String::from(" :::")];
text.extend(self.into_iter().map(|s| format!(" {s}")));
text.push(String::from(" :::"));
text
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decoration() {
assert_eq!(generate_decoration('=', 0), "");
assert_eq!(generate_decoration('=', 1), "=");
assert_eq!(generate_decoration('=', 5), "=====");
}
#[test]
fn test_format() {
let rst = Format::Rst;
assert_eq!(rst.extension(), "rst");
assert_eq!(rst.make_title("foo"), vec!["===", "foo", "===", ""]);
assert_eq!(
rst.make_title(&rst.make_inline_code("foo")),
vec!["=======", "``foo``", "=======", ""]
);
assert_eq!(Format::from_str("rst").unwrap(), rst);
let md = Format::Md;
assert_eq!(md.extension(), "md");
assert_eq!(md.make_title("foo"), vec!["# foo", ""]);
assert_eq!(
md.make_title(&md.make_inline_code("foo")),
vec!["# `foo`", ""]
);
assert_eq!(Format::from_str("md").unwrap(), md);
assert!(Format::from_str("foo").is_err());
}
#[test]
fn test_content_traits() {
let text: Vec<String> = ["line 1", "line 2", "line 3"]
.iter()
.map(|&s| s.to_string())
.collect();
let expected = vec![" :::", " line 1", " line 2", " line 3", " :::"];
assert_eq!(text.clone().get_rst_text(" "), &expected[1..4]);
assert_eq!(text.clone().get_md_text(), expected);
}
}