blocks 0.1.0

A high-performance Rust library for block-based content editing with JSON, Markdown, and HTML support
Documentation
#[cfg(test)]
mod tests {
    use crate::{Block, BlockType, Document, ListType};
    use proptest::prelude::*;

    // Strategy for generating safe strings for content
    fn safe_string() -> impl Strategy<Value = String> {
        r"[a-zA-Z0-9\s\-_.,!?]{1,100}"
            .prop_filter("Non-empty content", |s: &String| !s.trim().is_empty())
    }

    // Strategy for generating header levels
    fn header_level() -> impl Strategy<Value = u8> {
        1u8..=6u8
    }

    // Strategy for generating URLs
    fn safe_url() -> impl Strategy<Value = String> {
        prop_oneof![
            Just("https://example.com".to_string()),
            Just("http://test.org".to_string()),
            Just("/local/path".to_string()),
            r"https://[a-z]{3,10}\.com".prop_map(|s| s.to_string())
        ]
    }

    // Strategy for generating alt text
    fn alt_text() -> impl Strategy<Value = String> {
        r"[a-zA-Z0-9\s\-_]{1,50}"
            .prop_filter("Non-empty alt text", |s: &String| !s.trim().is_empty())
    }

    proptest! {
        #[test]
        fn test_text_block_property(content in safe_string()) {
            let block = Block::new(BlockType::Text, content.clone());
            prop_assert_eq!(&block.content, &content);
            prop_assert!(block.validate().is_ok());
        }

        #[test]
        fn test_header_block_property(level in header_level(), content in safe_string()) {
            let block = Block::new(BlockType::Header { level }, content.clone());
            prop_assert_eq!(&block.content, &content);
            prop_assert!(block.validate().is_ok());
        }

        #[test]
        fn test_link_block_property(url in safe_url(), content in safe_string()) {
            let block = Block::new(
                BlockType::Link {
                    url: url.clone(),
                    title: None
                },
                content.clone()
            );
            prop_assert_eq!(&block.content, &content);
            prop_assert!(block.validate().is_ok());
        }

        #[test]
        fn test_image_block_property(
            url in safe_url(),
            alt in alt_text()
        ) {
            let block = Block::new(
                BlockType::Image {
                    url: url.clone(),
                    alt: alt.clone(),
                    caption: None
                },
                "".to_string()
            );
            prop_assert!(block.validate().is_ok());
        }

        #[test]
        fn test_document_add_blocks_property(blocks_count in 1usize..=20) {
            let mut doc = Document::with_title("Test Document".to_string());

            for i in 0..blocks_count {
                let content = format!("Block content {i}");
                doc.add_block(Block::new(BlockType::Text, content));
            }

            prop_assert_eq!(doc.len(), blocks_count);
            prop_assert!(!doc.is_empty());
        }

        #[test]
        fn test_list_block_property(content in safe_string()) {
            let ordered_block = Block::new(
                BlockType::List { list_type: ListType::Ordered },
                content.clone()
            );
            prop_assert!(ordered_block.validate().is_ok());

            let unordered_block = Block::new(
                BlockType::List { list_type: ListType::Unordered },
                content.clone()
            );
            prop_assert!(unordered_block.validate().is_ok());
        }

        #[test]
        fn test_code_block_property(content in ".*", language in r"[a-z]{2,10}") {
            let block = Block::new(
                BlockType::Code {
                    language: Some(language.clone())
                },
                content.clone()
            );
            prop_assert_eq!(&block.content, &content);
            prop_assert!(block.validate().is_ok());
        }

        #[test]
        fn test_quote_block_property(content in safe_string()) {
            let block = Block::new(BlockType::Quote, content.clone());
            prop_assert_eq!(&block.content, &content);
            prop_assert!(block.validate().is_ok());
        }

        #[test]
        fn test_document_conversion_property(title in safe_string()) {
            let mut doc = Document::with_title(title.clone());
            doc.add_block(Block::new(BlockType::Header { level: 1 }, "Test Header".to_string()));
            doc.add_block(Block::new(BlockType::Text, "Test content".to_string()));

            let markdown = doc.to_format(crate::converters::ConversionFormat::Markdown);
            let html = doc.to_format(crate::converters::ConversionFormat::Html);

            prop_assert!(markdown.is_ok());
            prop_assert!(html.is_ok());
        }

        #[test]
        fn test_table_consistency_property(
            headers in prop::collection::vec(r"[A-Za-z]{1,10}", 1..=5),
            rows_count in 1usize..=5
        ) {
            let col_count = headers.len();
            let rows: Vec<Vec<String>> = (0..rows_count)
                .map(|i| (0..col_count).map(|j| format!("Cell{i}_{j}")).collect())
                .collect();

            let block = Block::new(
                BlockType::Table {
                    headers,
                    rows,
                    has_header: true
                },
                "".to_string()
            );

            prop_assert!(block.validate().is_ok());
        }

        #[test]
        fn test_roundtrip_markdown_property(_content in safe_string()) {
            let original_doc = Document::with_title("Test".to_string());

            // Test that conversion to markdown and back preserves structure
            let markdown = original_doc.to_format(crate::converters::ConversionFormat::Markdown);
            prop_assert!(markdown.is_ok());
        }

        #[test]
        fn test_block_metadata_property(
            key in r"[a-zA-Z_]{1,20}",
            value in safe_string()
        ) {
            let mut block = Block::new(BlockType::Text, "Test content".to_string());
            block.add_metadata(key.clone(), value.clone());

            prop_assert_eq!(block.get_metadata(&key), Some(&value));
        }

        #[test]
        fn test_document_operations_property(
            title in safe_string(),
            operations_count in 1usize..=10
        ) {
            let mut doc = Document::with_title(title.clone());

            // Add several blocks
            for i in 0..operations_count {
                doc.add_block(Block::new(
                    BlockType::Text,
                    format!("Content {i}")
                ));
            }

            let initial_len = doc.len();
            prop_assert_eq!(initial_len, operations_count);

            // Remove some blocks
            for _ in 0..(operations_count / 2) {
                if !doc.is_empty() {
                    let _ = doc.remove_block_at(0);
                }
            }

            prop_assert!(doc.len() <= initial_len);
        }

        #[test]
        fn test_validation_consistency_property(content in safe_string()) {
            let block = Block::new(BlockType::Text, content);

            // Validation should be consistent across multiple calls
            let first_validation = block.validate();
            let second_validation = block.validate();

            prop_assert_eq!(first_validation.is_ok(), second_validation.is_ok());
        }

        #[test]
        fn test_serialization_roundtrip_property(content in safe_string()) {
            let original_block = Block::new(BlockType::Text, content);

            let serialized = serde_json::to_string(&original_block);
            prop_assert!(serialized.is_ok());

            let deserialized: Result<Block, _> = serde_json::from_str(&serialized.unwrap());
            prop_assert!(deserialized.is_ok());

            let deserialized_block = deserialized.unwrap();
            prop_assert_eq!(original_block.content, deserialized_block.content);
            prop_assert_eq!(original_block.block_type, deserialized_block.block_type);
        }
    }
}