serde_toon_format 0.1.2

Serde-compatible TOON v3.0 encoder/decoder
Documentation
use memchr::memchr_iter;

use crate::error::Location;
use crate::{Error, Result};

#[derive(Clone, Copy, Debug)]
pub struct ScanLine {
    pub raw_start: usize,
    pub indent: usize,
    pub level: usize,
    pub start: usize,
    pub end: usize,
    pub is_blank: bool,
}

#[derive(Debug)]
pub struct ScanResult {
    pub lines: Vec<ScanLine>,
    pub non_blank: usize,
}

pub fn scan_lines(
    input: &str,
    indent_size: usize,
    strict: bool,
    validate: bool,
) -> Result<ScanResult> {
    if indent_size == 0 {
        return Err(Error::decode("indent size must be greater than zero"));
    }
    let bytes = input.as_bytes();
    if validate && bytes.last() == Some(&b'\n') {
        return Err(Error::decode("trailing newline not allowed"));
    }
    let mut lines = Vec::new();
    let mut non_blank = 0;
    let mut start = 0;
    for idx in memchr_iter(b'\n', bytes) {
        let mut end = idx;
        if end > start && bytes[end - 1] == b'\r' {
            end -= 1;
        }
        if validate && end > start {
            let last = bytes[end - 1];
            if last == b' ' || last == b'\t' {
                return Err(Error::decode("trailing whitespace not allowed"));
            }
        }
        let line_idx = lines.len();
        let line = build_line(bytes, start, end, indent_size, strict).map_err(|err| {
            err.with_location(Location {
                offset: start,
                line: line_idx + 1,
                column: 1,
            })
        })?;
        if !line.is_blank {
            non_blank += 1;
        }
        lines.push(line);
        start = idx + 1;
    }

    let mut end = bytes.len();
    if end > start && bytes[end - 1] == b'\r' {
        end -= 1;
    }
    if validate && end > start {
        let last = bytes[end - 1];
        if last == b' ' || last == b'\t' {
            return Err(Error::decode("trailing whitespace not allowed"));
        }
    }
    let line_idx = lines.len();
    let line = build_line(bytes, start, end, indent_size, strict).map_err(|err| {
        err.with_location(Location {
            offset: start,
            line: line_idx + 1,
            column: 1,
        })
    })?;
    if !line.is_blank {
        non_blank += 1;
    }
    lines.push(line);

    Ok(ScanResult { lines, non_blank })
}

fn build_line(
    bytes: &[u8],
    start: usize,
    end: usize,
    indent_size: usize,
    strict: bool,
) -> Result<ScanLine> {
    if start >= end {
        return Ok(ScanLine {
            raw_start: start,
            indent: 0,
            level: 0,
            start,
            end,
            is_blank: true,
        });
    }
    let mut only_whitespace = true;
    for &byte in &bytes[start..end] {
        if !byte.is_ascii_whitespace() {
            only_whitespace = false;
            break;
        }
    }
    if only_whitespace {
        return Ok(ScanLine {
            raw_start: start,
            indent: 0,
            level: 0,
            start,
            end,
            is_blank: true,
        });
    }
    let mut indent_columns: usize = 0;
    let mut indent_chars: usize = 0;
    for &byte in &bytes[start..end] {
        match byte {
            b' ' => {
                indent_columns += 1;
                indent_chars += 1;
            }
            b'\t' => {
                if strict {
                    return Err(Error::decode("tabs not allowed in indentation"));
                }
                indent_columns = indent_columns.saturating_add(indent_size);
                indent_chars += 1;
            }
            _ => break,
        }
    }
    if strict && !indent_columns.is_multiple_of(indent_size) {
        return Err(Error::decode("invalid indentation"));
    }
    let level = indent_columns / indent_size;
    let content_start = start + indent_chars;
    Ok(ScanLine {
        raw_start: start,
        indent: indent_columns,
        level,
        start: content_start,
        end,
        is_blank: false,
    })
}