256 lines
6.2 KiB
Rust
256 lines
6.2 KiB
Rust
use std::{
|
|
collections::{HashMap, hash_map::Entry},
|
|
};
|
|
|
|
use crate::{
|
|
prelude::*,
|
|
types::Config,
|
|
syntax::content::{Parseable, Lexeme},
|
|
};
|
|
|
|
use std::fmt::Display;
|
|
|
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
pub struct Header {
|
|
open: Option<bool>,
|
|
level: Level,
|
|
pub dom_id: Option<String>,
|
|
}
|
|
|
|
impl Header {
|
|
pub fn new(level: Level, open: bool, dom_id: Option<&str>) -> Header {
|
|
Header {
|
|
open: Some(open),
|
|
level,
|
|
dom_id: dom_id.map(std::borrow::ToOwned::to_owned),
|
|
}
|
|
}
|
|
|
|
pub fn make_id(
|
|
config: &Config,
|
|
next_lexeme: &Lexeme,
|
|
ids: &mut HashMap<String, Vec<String>>,
|
|
) -> String {
|
|
let base_id = if !config.ascii_dom_ids || next_lexeme.next().is_ascii()
|
|
{
|
|
next_lexeme.next().clone()
|
|
} else {
|
|
String::from("h")
|
|
};
|
|
|
|
match ids.entry(base_id.clone()) {
|
|
Entry::Occupied(mut occupied) => {
|
|
let ids_vec = occupied.get_mut();
|
|
let suffix = ids_vec.len();
|
|
let id_with_suffix = format!("{base_id}-{suffix}");
|
|
ids_vec.push(id_with_suffix.clone());
|
|
id_with_suffix
|
|
},
|
|
Entry::Vacant(vacant) => {
|
|
vacant.insert(vec![base_id.clone()]);
|
|
base_id
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn from_u8(level: u8, open: bool, dom_id: Option<&str>) -> Header {
|
|
Header {
|
|
level: Level::from_u8(level),
|
|
open: Some(open),
|
|
dom_id: dom_id.map(std::borrow::ToOwned::to_owned),
|
|
}
|
|
}
|
|
|
|
pub fn level(&self) -> u8 {
|
|
match self.level {
|
|
Level::One => 1,
|
|
Level::Two => 2,
|
|
Level::Three => 3,
|
|
Level::Four => 4,
|
|
Level::Five => 5,
|
|
Level::Six => 6,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Parseable for Header {
|
|
fn probe(lexeme: &Lexeme) -> bool {
|
|
if lexeme
|
|
.split_chars()
|
|
.into_iter()
|
|
.filter(|e| *e != '#')
|
|
.count()
|
|
== 0
|
|
{
|
|
let level = lexeme.text().len();
|
|
lexeme.clone().split_words().len() == 1 && level > 0 && level <= 6
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn lex(lexeme: &Lexeme) -> Header {
|
|
Header::new(
|
|
lexeme.text().len().into(),
|
|
true,
|
|
Some(&lexeme.next().to_ascii_lowercase()),
|
|
)
|
|
}
|
|
|
|
fn render(&self) -> String {
|
|
if let Some(open) = self.open {
|
|
if open && let Some(ref id) = self.dom_id {
|
|
format!(r#"<h{} id="{}">"#, self.level, id)
|
|
} else if open {
|
|
format!("<h{}>", self.level)
|
|
} else {
|
|
format!("</h{}>", self.level)
|
|
}
|
|
} else {
|
|
panic!("Attempt to render a header tag while open state is unknown")
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
pub enum Level {
|
|
One,
|
|
Two,
|
|
Three,
|
|
Four,
|
|
Five,
|
|
Six,
|
|
}
|
|
|
|
impl Level {
|
|
fn from_u8(u: u8) -> Level {
|
|
if u <= 1 {
|
|
Level::One
|
|
} else if u == 2 {
|
|
Level::Two
|
|
} else if u == 3 {
|
|
Level::Three
|
|
} else if u == 4 {
|
|
Level::Four
|
|
} else if u == 5 {
|
|
Level::Five
|
|
} else {
|
|
Level::Six
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<usize> for Level {
|
|
fn from(z: usize) -> Level {
|
|
let u8 = match u8::try_from(z) {
|
|
Ok(u) => u,
|
|
Err(e) => {
|
|
log!("Truncating header level {z} to 6: {e:?}");
|
|
6
|
|
},
|
|
};
|
|
Level::from_u8(u8)
|
|
}
|
|
}
|
|
|
|
impl Display for Level {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
match *self {
|
|
Level::One => write!(f, "1"),
|
|
Level::Two => write!(f, "2"),
|
|
Level::Three => write!(f, "3"),
|
|
Level::Four => write!(f, "4"),
|
|
Level::Five => write!(f, "5"),
|
|
Level::Six => write!(f, "6"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn make_id() {
|
|
let mut map: HashMap<String, Vec<String>> = HashMap::new();
|
|
let id = Header::make_id(
|
|
&Config::default(),
|
|
&Lexeme::new("##", "Title"),
|
|
&mut map,
|
|
);
|
|
assert_eq!(id, "Title");
|
|
}
|
|
|
|
#[test]
|
|
fn ascii_ids_set() {
|
|
let mut config = Config::new();
|
|
config.ascii_dom_ids = true;
|
|
|
|
let id = Header::make_id(
|
|
&config,
|
|
&Lexeme::new("##", "駄目!"),
|
|
&mut HashMap::new(),
|
|
);
|
|
assert_eq!(id, "h");
|
|
}
|
|
|
|
#[test]
|
|
fn ascii_ids_unset() {
|
|
let mut config = Config::new();
|
|
config.ascii_dom_ids = false;
|
|
|
|
let id = Header::make_id(
|
|
&config,
|
|
&Lexeme::new("##", "駄目!"),
|
|
&mut HashMap::new(),
|
|
);
|
|
assert_eq!(id, "駄目!");
|
|
}
|
|
|
|
#[test]
|
|
fn id_deduplication() {
|
|
let mut map: HashMap<String, Vec<String>> = HashMap::new();
|
|
let config = Config::default();
|
|
let id =
|
|
Header::make_id(&config, &Lexeme::new("##", "UVrcCUjoQ"), &mut map);
|
|
assert_eq!(id, "UVrcCUjoQ");
|
|
|
|
let double =
|
|
Header::make_id(&config, &Lexeme::new("##", "UVrcCUjoQ"), &mut map);
|
|
assert_eq!(double, "UVrcCUjoQ-1");
|
|
|
|
let double2 =
|
|
Header::make_id(&config, &Lexeme::new("##", "UVrcCUjoQ"), &mut map);
|
|
assert_eq!(double2, "UVrcCUjoQ-2");
|
|
}
|
|
|
|
#[test]
|
|
fn get_level() {
|
|
for l in 1..=6 {
|
|
let header = Header::from_u8(l, true, None);
|
|
assert_eq!(header.level(), l);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn no_id_render() {
|
|
let open_header = Header::from_u8(2, true, None);
|
|
let closed_header = Header::from_u8(2, false, None);
|
|
assert_eq!(open_header.render(), "<h2>");
|
|
assert_eq!(closed_header.render(), "</h2>");
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(
|
|
expected = "Attempt to render a header tag while open state is unknown"
|
|
)]
|
|
fn unknown_open_state_render() {
|
|
let header = Header {
|
|
level: Level::Two,
|
|
open: None,
|
|
dom_id: None,
|
|
};
|
|
|
|
header.render();
|
|
}
|
|
}
|