vize_l1 0.483.0

L1 - the lossless Vue-template surface tree (codename Sinopia, Davinci P2-7)
Documentation
//! Vue directive-head syntax, resolved outside the generic markup lexer.

use crate::markup::directive::{
    ArgSyntax, DirectiveName, DirectiveNameError, DirectivePrefix, DirectiveSyntax,
};
use vize_l0::{Allocator, Span, StringBuilder};

mod scan;
#[cfg(test)]
mod tests;

/// Maximum simultaneous distinct delimiter runs in the allocation-free hook.
/// Repeated identical brackets share a counter rather than consuming entries.
pub const MAX_DIRECTIVE_DELIMITER_RUNS: usize = 64;

/// Vue 3 directive-name syntax.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct VueDirectives;

impl DirectiveSyntax for VueDirectives {
    fn decompose(
        &self,
        name: &str,
        offset: u32,
    ) -> Result<Option<DirectiveName>, DirectiveNameError> {
        let length = u32::try_from(name.len()).map_err(|_| DirectiveNameError::OffsetOverflow)?;
        offset
            .checked_add(length)
            .ok_or(DirectiveNameError::OffsetOverflow)?;
        let bytes = name.as_bytes();
        let (prefix, name_start, mut index) = match bytes.first() {
            Some(b'v') if bytes.get(1) == Some(&b'-') => (DirectivePrefix::Full, 2, 2),
            Some(b':') => (DirectivePrefix::Bind, 0, 1),
            Some(b'.') => (DirectivePrefix::Prop, 0, 1),
            Some(b'@') => (DirectivePrefix::On, 0, 1),
            Some(b'#') => (DirectivePrefix::Slot, 0, 1),
            _ => return Ok(None),
        };
        let name_end = if prefix == DirectivePrefix::Full {
            index = full_head_end(bytes);
            index
        } else {
            0
        };
        let mut result = DirectiveName {
            prefix,
            name: absolute(offset, name_start, name_end),
            arg: None,
            modifiers: absolute(offset, name.len(), name.len()),
        };
        if prefix == DirectivePrefix::Full && bytes.get(index) == Some(&b':') {
            index += 1;
        }
        let mut section_start = index;
        while let Some(&byte) = bytes.get(index) {
            if byte == b'.' {
                if section_start < index {
                    result.arg = Some(ArgSyntax::Static(absolute(offset, section_start, index)));
                }
                result.modifiers = absolute(offset, index, name.len());
                return Ok(Some(result));
            }
            if byte == b'[' {
                if section_start < index {
                    result.arg = Some(ArgSyntax::Static(absolute(offset, section_start, index)));
                }
                let (end, closed) = scan::argument(bytes, index + 1)?;
                if closed || index + 1 < end {
                    result.arg = Some(ArgSyntax::Dynamic(absolute(offset, index + 1, end)));
                }
                if !closed {
                    return Ok(Some(result));
                }
                index = end + 1;
                section_start = index;
            } else {
                index += 1;
            }
        }
        if section_start < index {
            result.arg = Some(ArgSyntax::Static(absolute(offset, section_start, index)));
        }
        Ok(Some(result))
    }
}

/// Freeze the complete name after Vue's `v-pre` boundary. Only the full
/// directive head's argument separator disappears; the argument and modifiers
/// are opaque authored bytes, including their quotes and brackets.
pub fn frozen_attribute_name<'a>(allocator: &'a Allocator, authored: &'a str) -> &'a str {
    if !authored.starts_with("v-") {
        return authored;
    }
    let end = full_head_end(authored.as_bytes());
    if authored.as_bytes().get(end) != Some(&b':') {
        return authored;
    }
    let Some((head, tail)) = authored.split_at_checked(end) else {
        return authored;
    };
    let Some(tail) = tail.strip_prefix(':') else {
        return authored;
    };
    let mut out = StringBuilder::with_capacity_in(authored.len() - 1, allocator);
    out.push_str(head);
    out.push_str(tail);
    out.into_str()
}

/// The existing full directive head boundary, shared without scanning its
/// argument. Every admitted name has the ASCII `v-` prefix.
fn full_head_end(bytes: &[u8]) -> usize {
    let mut index = 2;
    while bytes
        .get(index)
        .is_some_and(|byte| !matches!(byte, b':' | b'.'))
    {
        index += 1;
    }
    index
}

/// The complete input range was admitted before any component is constructed.
fn absolute(offset: u32, start: usize, end: usize) -> Span {
    Span::new(offset + start as u32, offset + end as u32)
}