use unicode_normalization::UnicodeNormalization;
use quillmark_content::delta::diff_import;
use quillmark_content::import::ImportError;
use quillmark_content::{ApplyError, Delta, LineOp, MarkOp, Content};
use crate::document::meta::{validate_composable_kind, CardKindError};
use crate::document::{Card, Document, Payload};
use crate::quill::{CoercionError, FieldSchema, Leniency, QuillConfig};
use crate::value::QuillValue;
use crate::version::QuillReference;
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_alphabetic() && first != '_' {
return false;
}
for ch in chars {
if !ch.is_ascii_alphanumeric() && ch != '_' {
return false;
}
}
true
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum EditError {
#[error("invalid field name '{0}': must match [A-Za-z_][A-Za-z0-9_]*")]
InvalidFieldName(String),
#[error("field '{0}' is not declared in the schema")]
UnknownField(String),
#[error("invalid card kind '{0}': must match [a-z_][a-z0-9_]*")]
InvalidKindName(String),
#[error("card kind 'main' is reserved for the document root")]
ReservedKind,
#[error("index {index} is out of range (len = {len})")]
IndexOutOfRange { index: usize, len: usize },
#[error("value nests deeper than the maximum of {max} levels")]
ValueTooDeep { max: usize },
#[error("markdown import failed: {0}")]
Import(ImportError),
#[error("richtext field '{field}' decode failed: {message}")]
FieldRichtextDecode { field: String, message: String },
#[error("richtext field '{0}' is not inline: richtext(inline) requires a single paragraph line with no list/quote container and no islands")]
FieldRichtextNotInline(String),
#[error("field '{field}' does not conform to its schema type: {message}")]
FieldConform { field: String, message: String },
#[error("content apply failed: {0:?}")]
ContentApply(ApplyError),
}
impl EditError {
pub fn variant_name(&self) -> &'static str {
match self {
EditError::InvalidFieldName(_) => "InvalidFieldName",
EditError::UnknownField(_) => "UnknownField",
EditError::InvalidKindName(_) => "InvalidKindName",
EditError::ReservedKind => "ReservedKind",
EditError::IndexOutOfRange { .. } => "IndexOutOfRange",
EditError::ValueTooDeep { .. } => "ValueTooDeep",
EditError::Import(_) => "Import",
EditError::FieldRichtextDecode { .. } => "FieldRichtextDecode",
EditError::FieldRichtextNotInline(_) => "FieldRichtextNotInline",
EditError::FieldConform { .. } => "FieldConform",
EditError::ContentApply(_) => "ContentApply",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldViolation {
InvalidName,
TooDeep,
}
pub(crate) fn edit_error_from_violation(name: &str, v: FieldViolation) -> EditError {
match v {
FieldViolation::InvalidName => EditError::InvalidFieldName(name.to_string()),
FieldViolation::TooDeep => EditError::ValueTooDeep {
max: crate::document::limits::MAX_YAML_DEPTH,
},
}
}
fn check_field(name: &str, value: &serde_json::Value) -> Result<(), EditError> {
validate_field(name, value).map_err(|v| edit_error_from_violation(name, v))
}
fn check_meta_depth(map: &serde_json::Map<String, serde_json::Value>) -> Result<(), EditError> {
crate::value::depth_check_meta_map(map.clone(), |max| EditError::ValueTooDeep { max })?;
Ok(())
}
pub fn validate_field(key: &str, value: &serde_json::Value) -> Result<(), FieldViolation> {
if !is_valid_field_name(key) {
return Err(FieldViolation::InvalidName);
}
if crate::value::json_depth_exceeds(value, crate::document::limits::MAX_YAML_DEPTH) {
return Err(FieldViolation::TooDeep);
}
Ok(())
}
fn conform_error_to_edit(name: &str, err: CoercionError) -> EditError {
let CoercionError::Uncoercible { target, reason, .. } = err;
match target.as_str() {
"richtext(inline)" => EditError::FieldRichtextNotInline(name.to_string()),
"richtext" => EditError::FieldRichtextDecode {
field: name.to_string(),
message: reason,
},
_ => EditError::FieldConform {
field: name.to_string(),
message: reason,
},
}
}
pub(crate) fn resolve_field_write(
name: &str,
value: QuillValue,
schema: &FieldSchema,
) -> Result<QuillValue, EditError> {
if !is_valid_field_name(name) {
return Err(EditError::InvalidFieldName(name.to_string()));
}
let stored = QuillConfig::conform_value(&value, schema, name, Leniency::Write)
.map_err(|e| conform_error_to_edit(name, e))?;
check_field(name, stored.as_json())?;
Ok(stored)
}
impl Document {
pub fn set_quill_ref(&mut self, reference: QuillReference) {
self.main_mut().payload_mut().set_quill(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) -> Result<(), EditError> {
Self::check_composable_kind(&card)?;
self.cards_vec_mut().push(card);
Ok(())
}
pub fn insert_card(&mut self, index: usize, card: Card) -> Result<(), EditError> {
let len = self.cards().len();
if index > len {
return Err(EditError::IndexOutOfRange { index, len });
}
Self::check_composable_kind(&card)?;
self.cards_vec_mut().insert(index, card);
Ok(())
}
fn check_composable_kind(card: &Card) -> Result<(), EditError> {
let kind = card.kind().unwrap_or("");
validate_composable_kind(kind).map_err(|e| match e {
CardKindError::InvalidName => EditError::InvalidKindName(kind.to_string()),
CardKindError::Reserved => EditError::ReservedKind,
})
}
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_kind(
&mut self,
index: usize,
new_kind: impl Into<String>,
) -> Result<(), EditError> {
let new_kind = new_kind.into();
validate_composable_kind(&new_kind).map_err(|e| match e {
CardKindError::InvalidName => EditError::InvalidKindName(new_kind.clone()),
CardKindError::Reserved => EditError::ReservedKind,
})?;
let len = self.cards().len();
let card = self
.card_mut(index)
.ok_or(EditError::IndexOutOfRange { index, len })?;
card.payload_mut().set_kind(new_kind);
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(kind: impl Into<String>) -> Result<Self, EditError> {
let kind = kind.into();
validate_composable_kind(&kind).map_err(|e| match e {
CardKindError::InvalidName => EditError::InvalidKindName(kind.clone()),
CardKindError::Reserved => EditError::ReservedKind,
})?;
let mut payload = Payload::new();
payload.set_kind(kind);
Ok(Card::from_parts(
payload,
quillmark_content::Content::empty(),
))
}
pub fn store_field(&mut self, name: &str, value: impl Into<QuillValue>) -> Result<(), EditError> {
self.payload_mut()
.insert(name.to_string(), value.into())
.map_err(|v| edit_error_from_violation(name, v))?;
Ok(())
}
pub fn store_fill(&mut self, name: &str, value: impl Into<QuillValue>) -> Result<(), EditError> {
self.payload_mut()
.insert_fill(name.to_string(), value.into())
.map_err(|v| edit_error_from_violation(name, v))?;
Ok(())
}
pub fn store_fields<K, V, I>(&mut self, fields: I) -> Result<(), Vec<(String, EditError)>>
where
K: Into<String>,
V: Into<QuillValue>,
I: IntoIterator<Item = (K, V)>,
{
let fields: Vec<(String, QuillValue)> = fields
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect();
let errors: Vec<(String, EditError)> = fields
.iter()
.filter_map(|(name, value)| {
check_field(name, value.as_json())
.err()
.map(|e| (name.clone(), e))
})
.collect();
if !errors.is_empty() {
return Err(errors);
}
for (name, value) in fields {
self.payload_mut().insert_unchecked(name, value);
}
Ok(())
}
pub fn remove_field(&mut self, name: &str) -> Result<Option<QuillValue>, EditError> {
if !is_valid_field_name(name) {
return Err(EditError::InvalidFieldName(name.to_string()));
}
Ok(self.payload_mut().remove(name))
}
pub fn store_ext(
&mut self,
value: serde_json::Map<String, serde_json::Value>,
) -> Result<(), EditError> {
check_meta_depth(&value)?;
self.payload_mut().set_ext(value);
Ok(())
}
pub fn remove_ext(&mut self) -> Option<serde_json::Map<String, serde_json::Value>> {
self.payload_mut().take_ext()
}
pub fn store_ext_namespace(
&mut self,
namespace: impl Into<String>,
value: serde_json::Value,
) -> Result<(), EditError> {
let mut map = self.payload_mut().ext().cloned().unwrap_or_default();
map.insert(namespace.into(), value);
check_meta_depth(&map)?;
self.payload_mut().take_ext();
self.payload_mut().set_ext(map);
Ok(())
}
pub fn remove_ext_namespace(&mut self, namespace: &str) -> Option<serde_json::Value> {
let mut map = self.payload_mut().take_ext()?;
let removed = map.remove(namespace);
if !map.is_empty() {
self.payload_mut().set_ext(map);
}
removed
}
pub fn seed(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
self.payload().seed()
}
pub fn store_seed_namespace(
&mut self,
card_kind: impl Into<String>,
value: serde_json::Value,
) -> Result<(), EditError> {
let card_kind = card_kind.into();
validate_composable_kind(&card_kind).map_err(|e| match e {
CardKindError::InvalidName => EditError::InvalidKindName(card_kind.clone()),
CardKindError::Reserved => EditError::ReservedKind,
})?;
let mut map = self.payload_mut().seed().cloned().unwrap_or_default();
map.insert(card_kind, value);
check_meta_depth(&map)?;
self.payload_mut().take_seed();
self.payload_mut().set_seed(map);
Ok(())
}
pub fn remove_seed_namespace(&mut self, card_kind: &str) -> Option<serde_json::Value> {
let mut map = self.payload_mut().take_seed()?;
let removed = map.remove(card_kind);
if !map.is_empty() {
self.payload_mut().set_seed(map);
}
removed
}
pub fn install_body(&mut self, content: Content) {
self.overwrite_body(content);
}
pub fn install_field(&mut self, name: &str, content: Content) -> Result<(), EditError> {
if !is_valid_field_name(name) {
return Err(EditError::InvalidFieldName(name.to_string()));
}
self.store_field_content(name, &content);
Ok(())
}
fn store_field_content(&mut self, name: &str, content: &Content) {
let canonical = quillmark_content::serial::to_canonical_value(content);
self.payload_mut()
.insert_unchecked(name.to_string(), QuillValue::from_json(canonical));
}
pub fn commit_field(
&mut self,
name: &str,
value: impl Into<QuillValue>,
schema: &FieldSchema,
) -> Result<(), EditError> {
let stored = resolve_field_write(name, value.into(), schema)?;
self.payload_mut().insert_unchecked(name.to_string(), stored);
Ok(())
}
pub fn revise_body(&mut self, body: impl Into<String>) -> Result<Delta, EditError> {
let (content, delta) =
diff_import(self.body(), &body.into()).map_err(EditError::Import)?;
self.overwrite_body(content);
Ok(delta)
}
fn diff_field(
&self,
name: &str,
body: impl Into<String>,
) -> Result<(Content, Delta), EditError> {
if !is_valid_field_name(name) {
return Err(EditError::InvalidFieldName(name.to_string()));
}
let base = match self.field_richtext(name) {
Some(Ok(rt)) => rt,
Some(Err(e)) => {
return Err(EditError::FieldRichtextDecode {
field: name.to_string(),
message: e.into_message(),
})
}
None => Content::empty(),
};
diff_import(&base, &body.into()).map_err(EditError::Import)
}
pub fn revise_field(&mut self, name: &str, body: impl Into<String>) -> Result<Delta, EditError> {
let (content, delta) = self.diff_field(name, body)?;
self.store_field_content(name, &content);
Ok(delta)
}
pub fn revise_field_checked(
&mut self,
name: &str,
body: impl Into<String>,
schema: &FieldSchema,
) -> Result<Delta, EditError> {
let (content, delta) = self.diff_field(name, body)?;
let canonical = quillmark_content::serial::to_canonical_value(&content);
let stored = resolve_field_write(name, QuillValue::from_json(canonical), schema)?;
self.payload_mut().insert_unchecked(name.to_string(), stored);
Ok(delta)
}
pub fn apply_body_change(
&mut self,
text_delta: &Delta,
line_ops: &[LineOp],
mark_ops: &[MarkOp],
) -> Result<(), EditError> {
self.body_mut()
.apply_field_change(text_delta, line_ops, mark_ops)
.map_err(EditError::ContentApply)
}
pub fn apply_field_richtext_change(
&mut self,
name: &str,
text_delta: &Delta,
line_ops: &[LineOp],
mark_ops: &[MarkOp],
) -> Result<(), EditError> {
let mut content = match self.field_richtext(name) {
Some(Ok(rt)) => rt,
Some(Err(e)) => {
return Err(EditError::FieldRichtextDecode {
field: name.to_string(),
message: e.into_message(),
})
}
None => {
return Err(EditError::FieldRichtextDecode {
field: name.to_string(),
message: "field is absent".to_string(),
})
}
};
content
.apply_field_change(text_delta, line_ops, mark_ops)
.map_err(EditError::ContentApply)?;
self.store_field_content(name, &content);
Ok(())
}
}