use unicode_normalization::UnicodeNormalization;
use crate::document::sentinel::is_valid_tag_name;
use crate::document::{Card, Document, Frontmatter, Sentinel};
use crate::value::QuillValue;
use crate::version::QuillReference;
pub const RESERVED_NAMES: &[&str] = &["BODY", "CARDS", "QUILL", "CARD"];
#[inline]
pub fn is_reserved_name(name: &str) -> bool {
RESERVED_NAMES.contains(&name)
}
pub fn is_valid_field_name(name: &str) -> bool {
let normalized: String = name.nfc().collect();
if normalized.is_empty() {
return false;
}
let mut chars = normalized.chars();
let first = chars.next().unwrap();
if !first.is_ascii_lowercase() && first != '_' {
return false;
}
for ch in chars {
if !ch.is_ascii_lowercase() && !ch.is_ascii_digit() && ch != '_' {
return false;
}
}
true
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum EditError {
#[error("reserved name '{0}' cannot be used as a field name")]
ReservedName(String),
#[error("invalid field name '{0}': must match [a-z_][a-z0-9_]*")]
InvalidFieldName(String),
#[error("invalid tag name '{0}': must match [a-z_][a-z0-9_]*")]
InvalidTagName(String),
#[error("index {index} is out of range (len = {len})")]
IndexOutOfRange { index: usize, len: usize },
}
impl Document {
pub fn set_quill_ref(&mut self, reference: QuillReference) {
self.main_mut().replace_sentinel(Sentinel::Main(reference));
}
pub fn card_mut(&mut self, index: usize) -> Option<&mut Card> {
self.cards_mut().get_mut(index)
}
pub fn push_card(&mut self, card: Card) {
debug_assert!(
!card.sentinel().is_main(),
"cannot push a Main-sentinel card as a composable card"
);
self.cards_vec_mut().push(card);
}
pub fn insert_card(&mut self, index: usize, card: Card) -> Result<(), EditError> {
debug_assert!(
!card.sentinel().is_main(),
"cannot insert a Main-sentinel card as a composable card"
);
let len = self.cards().len();
if index > len {
return Err(EditError::IndexOutOfRange { index, len });
}
self.cards_vec_mut().insert(index, card);
Ok(())
}
pub fn remove_card(&mut self, index: usize) -> Option<Card> {
if index >= self.cards().len() {
return None;
}
Some(self.cards_vec_mut().remove(index))
}
pub fn set_card_tag(
&mut self,
index: usize,
new_tag: impl Into<String>,
) -> Result<(), EditError> {
let new_tag = new_tag.into();
if !is_valid_tag_name(&new_tag) {
return Err(EditError::InvalidTagName(new_tag));
}
let len = self.cards().len();
let card = self
.card_mut(index)
.ok_or(EditError::IndexOutOfRange { index, len })?;
card.replace_sentinel(Sentinel::Card(new_tag));
Ok(())
}
pub fn move_card(&mut self, from: usize, to: usize) -> Result<(), EditError> {
let len = self.cards().len();
if from >= len {
return Err(EditError::IndexOutOfRange { index: from, len });
}
if to >= len {
return Err(EditError::IndexOutOfRange { index: to, len });
}
if from == to {
return Ok(());
}
let card = self.cards_vec_mut().remove(from);
self.cards_vec_mut().insert(to, card);
Ok(())
}
}
impl Card {
pub fn new(tag: impl Into<String>) -> Result<Self, EditError> {
let tag = tag.into();
if !is_valid_tag_name(&tag) {
return Err(EditError::InvalidTagName(tag));
}
Ok(Card::new_with_sentinel(
Sentinel::Card(tag),
Frontmatter::new(),
String::new(),
))
}
pub fn set_field(&mut self, name: &str, value: QuillValue) -> Result<(), EditError> {
if is_reserved_name(name) {
return Err(EditError::ReservedName(name.to_string()));
}
if !is_valid_field_name(name) {
return Err(EditError::InvalidFieldName(name.to_string()));
}
self.frontmatter_mut().insert(name.to_string(), value);
Ok(())
}
pub fn set_fill(&mut self, name: &str, value: QuillValue) -> Result<(), EditError> {
if is_reserved_name(name) {
return Err(EditError::ReservedName(name.to_string()));
}
if !is_valid_field_name(name) {
return Err(EditError::InvalidFieldName(name.to_string()));
}
self.frontmatter_mut().insert_fill(name.to_string(), value);
Ok(())
}
pub fn remove_field(&mut self, name: &str) -> Result<Option<QuillValue>, EditError> {
if is_reserved_name(name) {
return Err(EditError::ReservedName(name.to_string()));
}
if !is_valid_field_name(name) {
return Err(EditError::InvalidFieldName(name.to_string()));
}
Ok(self.frontmatter_mut().remove(name))
}
pub fn replace_body(&mut self, body: impl Into<String>) {
self.overwrite_body(body.into());
}
}