use std::borrow::Cow;
use rustc_hash::FxHashMap;
use serde::{
Serialize,
ser::{SerializeMap, Serializer},
};
pub const MAX_TOC_LEVELS: u8 = 5;
pub const MAX_SECTION_LEVELS: u8 = 5;
#[must_use]
pub fn strip_quotes(s: &str) -> &str {
s.trim_start_matches(['"', '\''])
.trim_end_matches(['"', '\''])
}
#[derive(Debug, PartialEq, Clone)]
struct AttributeMap<'a> {
all: FxHashMap<AttributeName<'a>, AttributeValue<'a>>,
explicit: FxHashMap<AttributeName<'a>, AttributeValue<'a>>,
}
impl Default for AttributeMap<'_> {
fn default() -> Self {
use std::sync::LazyLock;
static DEFAULTS: LazyLock<FxHashMap<AttributeName<'static>, AttributeValue<'static>>> =
LazyLock::new(|| {
crate::constants::DEFAULT_ATTRIBUTE_ENTRIES
.iter()
.cloned()
.collect()
});
AttributeMap {
all: DEFAULTS.clone(),
explicit: FxHashMap::default(), }
}
}
impl<'a> AttributeMap<'a> {
fn empty() -> Self {
AttributeMap {
all: FxHashMap::default(),
explicit: FxHashMap::default(),
}
}
fn iter(&self) -> impl Iterator<Item = (&AttributeName<'a>, &AttributeValue<'a>)> {
self.all.iter()
}
fn is_empty(&self) -> bool {
self.explicit.is_empty()
}
fn insert(&mut self, name: AttributeName<'a>, value: AttributeValue<'a>) {
if !self.contains_key(&name) {
self.all.insert(name.clone(), value.clone());
self.explicit.insert(name, value); }
}
fn set(&mut self, name: AttributeName<'a>, value: AttributeValue<'a>) {
self.all.insert(name.clone(), value.clone());
self.explicit.insert(name, value); }
fn get(&self, name: &str) -> Option<&AttributeValue<'a>> {
self.all.get(name)
}
fn contains_key(&self, name: &str) -> bool {
self.all.contains_key(name)
}
fn remove(&mut self, name: &str) -> Option<AttributeValue<'a>> {
self.explicit.remove(name);
self.all.remove(name)
}
fn merge(&mut self, other: AttributeMap<'a>) {
for (key, value) in other.all {
self.insert(key, value);
}
}
}
impl Serialize for AttributeMap<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut sorted_keys: Vec<_> = self.explicit.keys().collect();
sorted_keys.sort();
let mut state = serializer.serialize_map(Some(self.explicit.len()))?;
for key in sorted_keys {
if let Some(value) = &self.explicit.get(key) {
match value {
AttributeValue::Bool(true) => {
if key == "toc" {
state.serialize_entry(key, "")?;
} else {
state.serialize_entry(key, &true)?;
}
}
value @ (AttributeValue::Bool(false)
| AttributeValue::String(_)
| AttributeValue::None) => {
state.serialize_entry(key, value)?;
}
}
}
}
state.end()
}
}
fn validate_bounded_attribute(key: &str, value: &AttributeValue<'_>) {
let AttributeValue::String(s) = value else {
return;
};
match key {
"sectnumlevels" => {
if let Ok(level) = s.parse::<u8>()
&& level > MAX_SECTION_LEVELS
{
tracing::warn!(
attribute = "sectnumlevels",
value = level,
"sectnumlevels must be between 0 and {MAX_SECTION_LEVELS}, got {level}. \
Values above {MAX_SECTION_LEVELS} will be treated as {MAX_SECTION_LEVELS}."
);
}
}
"toclevels" => {
if let Ok(level) = s.parse::<u8>()
&& level > MAX_TOC_LEVELS
{
tracing::warn!(
attribute = "toclevels",
value = level,
"toclevels must be between 0 and {MAX_TOC_LEVELS}, got {level}. \
Values above {MAX_TOC_LEVELS} will be treated as {MAX_TOC_LEVELS}."
);
}
}
_ => {}
}
}
#[derive(Debug, PartialEq, Clone, Default)]
pub struct DocumentAttributes<'a>(AttributeMap<'a>);
impl<'a> DocumentAttributes<'a> {
pub(crate) fn empty() -> Self {
Self(AttributeMap::empty())
}
pub fn iter(&self) -> impl Iterator<Item = (&AttributeName<'a>, &AttributeValue<'a>)> {
self.0.iter()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn insert(&mut self, name: AttributeName<'a>, value: AttributeValue<'a>) {
validate_bounded_attribute(&name, &value);
self.0.insert(name, value);
}
pub fn set(&mut self, name: AttributeName<'a>, value: AttributeValue<'a>) {
validate_bounded_attribute(&name, &value);
self.0.set(name, value);
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&AttributeValue<'a>> {
self.0.get(name)
}
#[must_use]
pub fn contains_key(&self, name: &str) -> bool {
self.0.contains_key(name)
}
pub fn remove(&mut self, name: &str) -> Option<AttributeValue<'a>> {
self.0.remove(name)
}
pub fn merge(&mut self, other: Self) {
self.0.merge(other.0);
}
#[must_use]
pub fn get_string(&self, name: &str) -> Option<Cow<'a, str>> {
self.get(name).and_then(|v| match v {
AttributeValue::String(s) => Some(match s {
Cow::Borrowed(b) => Cow::Borrowed(strip_quotes(b)),
Cow::Owned(o) => Cow::Owned(strip_quotes(o).to_string()),
}),
AttributeValue::None | AttributeValue::Bool(_) => None,
})
}
#[must_use]
pub fn to_static(&self) -> DocumentAttributes<'static> {
self.clone().into_static()
}
#[must_use]
pub fn into_static(self) -> DocumentAttributes<'static> {
let convert_map = |map: FxHashMap<AttributeName<'a>, AttributeValue<'a>>| -> FxHashMap<AttributeName<'static>, AttributeValue<'static>> {
map.into_iter()
.map(|(k, v)| {
let key: AttributeName<'static> = Cow::Owned(k.into_owned());
let val = match v {
AttributeValue::String(s) => AttributeValue::String(Cow::Owned(s.into_owned())),
AttributeValue::Bool(b) => AttributeValue::Bool(b),
AttributeValue::None => AttributeValue::None,
};
(key, val)
})
.collect()
};
DocumentAttributes(AttributeMap {
all: convert_map(self.0.all),
explicit: convert_map(self.0.explicit),
})
}
}
impl Serialize for DocumentAttributes<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.serialize(serializer)
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct ElementAttributes<'a>(AttributeMap<'a>);
impl Default for ElementAttributes<'_> {
fn default() -> Self {
ElementAttributes(AttributeMap::empty())
}
}
impl<'a> ElementAttributes<'a> {
pub fn iter(&self) -> impl Iterator<Item = (&AttributeName<'a>, &AttributeValue<'a>)> {
self.0.iter()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn insert(&mut self, name: AttributeName<'a>, value: AttributeValue<'a>) {
self.0.insert(name, value);
}
pub fn set(&mut self, name: AttributeName<'a>, value: AttributeValue<'a>) {
self.0.set(name, value);
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&AttributeValue<'a>> {
self.0.get(name)
}
#[must_use]
pub fn contains_key(&self, name: &str) -> bool {
self.0.contains_key(name)
}
pub fn remove(&mut self, name: &str) -> Option<AttributeValue<'a>> {
self.0.remove(name)
}
pub fn merge(&mut self, other: Self) {
self.0.merge(other.0);
}
#[must_use]
pub fn into_static(self) -> ElementAttributes<'static> {
let convert_map = |map: FxHashMap<AttributeName<'a>, AttributeValue<'a>>| -> FxHashMap<AttributeName<'static>, AttributeValue<'static>> {
map.into_iter()
.map(|(k, v)| {
let key: AttributeName<'static> = Cow::Owned(k.into_owned());
let val = match v {
AttributeValue::String(s) => AttributeValue::String(Cow::Owned(s.into_owned())),
AttributeValue::Bool(b) => AttributeValue::Bool(b),
AttributeValue::None => AttributeValue::None,
};
(key, val)
})
.collect()
};
ElementAttributes(AttributeMap {
all: convert_map(self.0.all),
explicit: convert_map(self.0.explicit),
})
}
#[must_use]
pub fn get_string(&self, name: &str) -> Option<Cow<'a, str>> {
self.get(name).and_then(|v| match v {
AttributeValue::String(s) => Some(match s {
Cow::Borrowed(b) => Cow::Borrowed(strip_quotes(b)),
Cow::Owned(o) => Cow::Owned(strip_quotes(o).to_string()),
}),
AttributeValue::None | AttributeValue::Bool(_) => None,
})
}
}
impl Serialize for ElementAttributes<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.serialize(serializer)
}
}
pub type AttributeName<'a> = Cow<'a, str>;
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum AttributeValue<'a> {
String(Cow<'a, str>),
Bool(bool),
None,
}
impl std::fmt::Display for AttributeValue<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AttributeValue::String(value) => write!(f, "{value}"),
AttributeValue::Bool(value) => write!(f, "{value}"),
AttributeValue::None => write!(f, "null"),
}
}
}
impl<'a> From<&'a str> for AttributeValue<'a> {
fn from(value: &'a str) -> Self {
AttributeValue::String(Cow::Borrowed(value))
}
}
impl From<String> for AttributeValue<'_> {
fn from(value: String) -> Self {
AttributeValue::String(Cow::Owned(value))
}
}
impl From<bool> for AttributeValue<'_> {
fn from(value: bool) -> Self {
AttributeValue::Bool(value)
}
}
impl From<()> for AttributeValue<'_> {
fn from((): ()) -> Self {
AttributeValue::None
}
}