ziyy-core 0.1.4

Terminal Styling using HTML-like syntax.
Documentation
use std::borrow::Cow;
use std::mem::take;

use fragment::Fragment;
use fragment::FragmentType::{self, *};

use crate::common::Span;
use crate::{Error, ErrorType, Result};

pub mod fragment;

pub struct Splitter<'a> {
    source: &'a Cow<'a, str>,
    fragments: Vec<Fragment<'a>>,
    start: usize,
    current: usize,
    span: Span,
}

impl<'a> Default for Splitter<'a> {
    fn default() -> Self {
        Self::new()
    }
}

enum Quote {
    Single,
    Double,
    None,
}

static SOURCE: Cow<'_, str> = Cow::Borrowed("");

impl<'a> Splitter<'a> {
    pub fn new() -> Self {
        Self {
            source: &SOURCE,
            fragments: vec![],
            start: 0,
            current: 0,
            span: Span::default(),
        }
    }

    pub fn split(&mut self, source: &'a Cow<'a, str>) -> Result<Vec<Fragment<'a>>> {
        self.source = source;

        macro_rules! consume_word {
            ($c:ident) => {
                loop {
                    if self.is_at_end() {
                        break;
                    }

                    if is_whitespace(self.peek()) {
                        break;
                    }

                    if matches!(self.peek(), b'<') {
                        break;
                    }

                    if matches!($c, b'\\') {
                        self.advance();
                    }

                    self.advance();
                }
            };
        }

        while !self.is_at_end() {
            self.start = self.current;

            let mut c = self.advance();

            match c {
                b' ' | b'\r' | b'\t' | b'\n' => self.whitespace(),
                b'\\' => {
                    c = self.advance();
                    consume_word!(c);
                    self.add_fragment(Word);
                }
                b'<' => self.tag()?,
                _ => {
                    consume_word!(c);
                    self.add_fragment(Word);
                }
            }
        }

        Ok(take(&mut self.fragments))
    }

    fn tag(&mut self) -> Result<()> {
        if self.peek() == b'>' {
            self.advance();
            self.add_fragment(Tag);
            return Ok(());
        }
        let mut quote = Quote::None;

        loop {
            let c = self.advance();
            if self.is_at_end() {
                match quote {
                    Quote::Single | Quote::Double => {
                        return Err(Error::new(
                            ErrorType::UnterminatedString,
                            "Untermitated string literal".into(),
                            self.span,
                        ));
                    }
                    Quote::None => {
                        return Err(Error::new(
                            ErrorType::UnexpectedEof,
                            "Untermitated string literal".into(),
                            self.span,
                        ));
                    }
                }
            }

            let close = matches!(self.peek(), b'>');
            let single = matches!(self.peek(), b'\'');
            let double = matches!(self.peek(), b'"');
            let esc = matches!(c, b'\\');
            match quote {
                Quote::Single => {
                    if single && !esc {
                        quote = Quote::None;
                    }
                }
                Quote::Double => {
                    if double && !esc {
                        quote = Quote::None;
                    }
                }
                Quote::None => {
                    if close {
                        break;
                    } else if single {
                        quote = Quote::Single;
                    } else if double {
                        quote = Quote::Double;
                    }
                }
            }
        }

        self.advance();
        self.add_fragment(Tag);
        Ok(())
    }

    fn whitespace(&mut self) {
        while is_whitespace(self.peek()) {
            self.advance();
        }
        self.add_fragment(Whitespace);
    }

    fn peek(&self) -> u8 {
        if self.is_at_end() {
            b'\0'
        } else {
            self.source.as_bytes()[self.current]
        }
    }

    fn is_at_end(&self) -> bool {
        self.current >= self.source.len()
    }

    fn advance(&mut self) -> u8 {
        self.current += 1;
        self.span += (0, 1);
        let ch = self.source.as_bytes()[self.current - 1];
        if ch == b'\n' {
            self.span += (1, 0);
        }
        ch
    }

    fn add_fragment(&mut self, r#type: FragmentType) {
        let text = &self.source[self.start..self.current];
        self.fragments
            .push(Fragment::new(r#type, Cow::Borrowed(text), self.span));
        self.span.tie_end();
    }
}

pub fn is_whitespace(c: u8) -> bool {
    matches!(c, b' ' | b'\t' | b'\n' | b'\x0c' | b'\x0d')
}