use std::borrow::Cow;
use std::result::Result;
use itertools::Itertools;
use ropey::{Rope, RopeSlice};
use crate::{
Arena, Headline, HeadlineBuilder, HeadlineError, HeadlinePod, Planning, RopeExt, Section,
StructureError,
};
lazy_static! {
static ref TAG_VALIDATION_RE: regex::Regex =
regex::Regex::new("[\\w@#%:]*").expect("failed to assemble headline regex");
static ref CONTAINS_HEADLINE_RE: regex::Regex =
regex::Regex::new("(^|.*\n)\\*\\** .*").expect("failed to assemble headline regex");
static ref DEFAULT_CONTEXT: Context<'static> = Context::default();
}
#[derive(Debug, Clone)]
pub struct Context<'a> {
pub(crate) keywords: Cow<'a, str>,
}
impl Default for Context<'static> {
fn default() -> Context<'static> {
Context::new("TODO:DONE".into())
}
}
impl Context<'_> {
pub fn default() -> Context<'static> {
Context::new("TODO:DONE".into())
}
pub fn new<'a>(keywords: Cow<'a, str>) -> Context<'a> {
Context { keywords }
}
pub fn from_keywords(keywords: &[&str]) -> Context<'static> {
Context {
keywords: Cow::Owned(keywords.iter().join(":")),
}
}
}
pub(crate) fn context_or<'a, 'b>(context: Option<&'b Context<'a>>) -> &'b Context<'a> {
match context {
Some(context) => context,
None => &DEFAULT_CONTEXT,
}
}
impl Section {
pub fn headline(self, arena: &Arena, context: Option<&Context>) -> Option<Headline> {
if self.level(&arena) > 0 {
Some(parse_valid_single_headline(
self.text(&arena).slice(..),
context_or(context),
))
} else {
None
}
}
pub fn set_headline(self, arena: &mut Arena, headline: &Headline) -> Result<(), HeadlineError> {
self.set_raw(arena, headline.to_rope())
}
pub fn set_level(self, arena: &mut Arena, level: u16) -> Result<(), StructureError> {
if self.level_change_ok(arena, level) {
arena.set_level(self, level);
Ok(())
} else {
Err(StructureError::LevelError)
}
}
pub fn set_raw(self, arena: &mut Arena, raw: Rope) -> Result<(), HeadlineError> {
match arena.new_section(raw) {
None => Err(HeadlineError::InvalidBodyError),
Some(section) if section.children(arena).count() > 0 => {
Err(HeadlineError::InvalidBodyError)
}
Some(section) => {
if self.level_change_ok(arena, section.level(&arena)) {
*arena.arena[self.id].get_mut() =
std::mem::take(arena.arena[section.id].get_mut());
section.id.remove(&mut arena.arena);
Ok(())
} else {
Err(HeadlineError::InvalidLevelError)
}
}
}
}
fn level_change_ok(self, arena: &mut Arena, level: u16) -> bool {
let old_level = self.level(arena);
if old_level == level {
return true;
}
if old_level == 0 || level == 0 {
return false;
}
if let Some(parent) = self.parent(arena) {
if parent.level(arena) >= level {
return false;
}
}
for child in self.children(arena) {
if child.level(arena) <= level {
return false;
}
}
true
}
}
impl HeadlineBuilder {
pub fn validate_partially(&self, context: Option<&Context>) -> Result<(), HeadlineError> {
let context = context_or(context);
if self.0.level == 0 {
return Err(HeadlineError::InvalidLevelError);
};
if let Some(c) = self.0.priority {
if !c.is_ascii_uppercase() {
return Err(HeadlineError::InvalidPriorityError);
}
}
if !self.0.raw_tags_string.is_empty()
&& !TAG_VALIDATION_RE.is_match(&self.0.raw_tags_string)
{
return Err(HeadlineError::InvalidTagsError);
}
if let Some(keyword) = &self.0.keyword {
if !context.keywords.split(':').any(|k| k == keyword) {
return Err(HeadlineError::InvalidKeywordError);
}
}
if CONTAINS_HEADLINE_RE.is_match(&*self.0.body.to_contiguous()) {
return Err(HeadlineError::InvalidBodyError);
}
Ok(())
}
pub fn headline(&self, context: Option<&Context>) -> Result<Headline, HeadlineError> {
let headline = self.to_rope(context)?;
let headline = parse_valid_single_headline(headline.slice(..), context_or(context));
if headline.to_builder() != *self {
return Err(HeadlineError::NonEquivalentReparseError);
}
Ok(Headline(HeadlinePod {
level: self.0.level,
priority: self.0.priority,
raw_tags_string: self.0.raw_tags_string.clone(),
raw_tags_rope: self.0.raw_tags_rope.clone(),
keyword: self.0.keyword.clone(),
title: self.0.title.clone(),
commented: self.0.commented,
planning: self.0.planning.clone(),
body: self.0.body.clone(),
}))
}
pub fn to_rope(&self, context: Option<&Context>) -> Result<Rope, HeadlineError> {
self.validate_partially(context)?;
Ok(self.0.to_rope())
}
}
impl HeadlinePod {
pub(crate) fn to_rope(&self) -> Rope {
let mut capacity = 0;
if self.level > 0 {
capacity += self.level as usize + 1;
}
if let Some(k) = &self.keyword {
capacity += k.len_bytes() + 1;
}
if self.priority.is_some() {
capacity += 5;
}
if self.commented {
if self.title.is_empty() {
capacity += 7;
} else {
capacity += 8;
}
}
let mut prefix = String::with_capacity(capacity);
for _ in 0..self.level {
prefix.push('*');
}
prefix.push(' ');
if let Some(k) = &self.keyword {
for chunk in k.chunks() {
prefix.push_str(chunk);
}
prefix.push(' ');
}
if let Some(p) = self.priority {
prefix.push('[');
prefix.push('#');
prefix.push(p);
prefix.push(']');
prefix.push(' ');
}
if self.commented {
if self.title.is_empty() {
prefix.push_str("COMMENT");
} else {
prefix.push_str("COMMENT ");
}
}
let mut headline = Rope::from(prefix);
headline.append(self.title.clone());
if !self.raw_tags_string.is_empty() {
headline.push_str(" :");
headline.append(self.raw_tags_rope.clone());
headline.push(':');
}
if self.planning != Planning::default() {
headline.push('\n');
for _ in 0..self.level {
headline.push(' ');
}
if let Some(scheduled) = self.planning.scheduled.as_ref() {
headline.push_str(" SCHEDULED: ");
headline.push_string(scheduled.to_string());
}
if let Some(deadline) = self.planning.deadline.as_ref() {
headline.push_str(" DEADLINE: ");
headline.push_string(deadline.to_string());
}
if let Some(closed) = self.planning.closed.as_ref() {
headline.push_str(" CLOSED: ");
headline.push_string(closed.to_string());
}
}
if !self.body.is_empty() {
headline.push('\n');
headline.append(self.body.clone());
}
headline
}
}
pub fn parse_valid_single_headline(text: RopeSlice, context: &Context) -> Headline {
crate::parser::headline::parse_headline(text, context).unwrap()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_level_sync() {
let mut arena = Arena::default();
let mut h1 = HeadlineBuilder::default();
h1.title("Bees".into());
h1.level(1);
let h1 = h1.headline( None).unwrap();
let mut h4 = HeadlineBuilder::default();
h4.title("Wasps".into());
h4.level(4);
let h4 = h4.headline( None).unwrap();
let doc = arena.parse_str("Hello");
assert_eq!(doc.root.text(&arena), "Hello");
assert_eq!(doc.root.level(&arena), 0);
doc.root.set_level(&mut arena, 0).unwrap();
assert!(doc.root.set_level(&mut arena, 1).is_err());
assert!(doc.root.set_level(&mut arena, 4).is_err());
assert!(doc.root.set_raw(&mut arena, "World".into()).is_ok());
assert!(doc.root.set_raw(&mut arena, "* World".into()).is_err());
assert!(doc.root.set_headline(&mut arena, &h1).is_err());
assert!(doc.root.set_headline(&mut arena, &h4).is_err());
let doc = arena.parse_str("* Hello\n*** World");
let hello = doc.root.children(&arena).next().unwrap();
let world = hello.children(&arena).next().unwrap();
assert_eq!(world.text(&arena), "*** World");
assert!(world.set_level(&mut arena, 2).is_ok());
assert_eq!(world.text(&arena), "** World");
assert!(world.set_level(&mut arena, 1).is_err());
assert!(world.set_level(&mut arena, 5).is_ok());
assert_eq!(world.text(&arena), "***** World");
assert!(world.set_raw(&mut arena, "Waterworld".into()).is_err());
assert!(world.set_raw(&mut arena, "** Westworld".into()).is_ok());
assert_eq!(world.level(&arena), 2);
assert!(world.set_raw(&mut arena, "* World".into()).is_err());
assert!(world.set_headline(&mut arena, &h1).is_err());
assert!(world.set_headline(&mut arena, &h4).is_ok());
assert_eq!(world.level(&arena), 4);
assert_eq!(world.text(&arena), "**** Wasps");
assert!(hello.set_level(&mut arena, 0).is_err());
assert!(hello.set_level(&mut arena, 1).is_ok());
assert!(hello.set_level(&mut arena, 2).is_ok());
assert!(hello.set_level(&mut arena, 3).is_ok());
assert!(hello.set_level(&mut arena, 4).is_err());
assert_eq!(hello.level(&arena), 3);
assert!(hello.set_headline(&mut arena, &h4).is_err());
assert!(hello.set_headline(&mut arena, &h1).is_ok());
assert_eq!(hello.level(&arena), 1);
assert_eq!(hello.text(&arena), "* Bees");
assert!(hello.set_raw(&mut arena, "Waterworld".into()).is_err());
assert!(hello.set_raw(&mut arena, "* Waterworld".into()).is_ok());
assert_eq!(hello.level(&arena), 1);
assert_eq!(hello.text(&arena), "* Waterworld");
assert!(hello.set_raw(&mut arena, "** Waterworld".into()).is_ok());
assert_eq!(hello.level(&arena), 2);
assert!(hello.set_raw(&mut arena, "*** Waterworld".into()).is_ok());
assert_eq!(hello.level(&arena), 3);
assert!(hello.set_raw(&mut arena, "**** Waterworld".into()).is_err());
}
}