From bfe03dbdc2c07d7239c8fd07f2526d99bac93d4e Mon Sep 17 00:00:00 2001 From: Geens Date: Sun, 13 Apr 2025 22:02:29 +0200 Subject: [PATCH] WIP cdata and comment tokens --- lib/xml_schema_validator/src/token/cdata.rs | 241 ++++++++++++++++++ lib/xml_schema_validator/src/token/comment.rs | 196 ++++++++++++++ .../src/token/element_open_end.rs | 62 ++++- lib/xml_schema_validator/src/token/mod.rs | 16 +- .../src/token/text_content.rs | 99 ++++++- 5 files changed, 604 insertions(+), 10 deletions(-) create mode 100644 lib/xml_schema_validator/src/token/cdata.rs create mode 100644 lib/xml_schema_validator/src/token/comment.rs diff --git a/lib/xml_schema_validator/src/token/cdata.rs b/lib/xml_schema_validator/src/token/cdata.rs new file mode 100644 index 0000000..5e285d4 --- /dev/null +++ b/lib/xml_schema_validator/src/token/cdata.rs @@ -0,0 +1,241 @@ +use crate::*; +use std::sync::Arc; + +/// Represents a CDATA section that is in the process of being parsed +#[derive(Clone, Debug)] +pub struct CData { + /// The element that contains this CDATA section + element: Arc, + /// Current parsing state + state: CDataState, +} + +/// The different states of CDATA parsing +#[derive(Clone, Debug, PartialEq)] +enum CDataState { + /// Parsing "<", initial state + OpeningBracket1, + /// Parsing "!" after "<" in opening sequence + OpeningExclamationPoint, + /// Parsing "[" after "" + ClosingBracket3, +} + +impl CData { + /// Create a new CDATA parsing state after seeing ") -> Self { + Self { + element, + state: CDataState::OpeningBracket1, + } + } + + pub fn append(mut self, c: char) -> Vec { + match self.state { + CDataState::OpeningBracket1 if c == '!' => vec![Token::CData(Self{state: CDataState::OpeningExclamationPoint, ..self})], + CDataState::OpeningExclamationPoint if c == '[' => vec![Token::CData(Self{state: CDataState::OpeningBracket2, ..self})], + CDataState::OpeningBracket2 if c == 'C' => vec![Token::CData(Self{state: CDataState::OpeningC, ..self})], + CDataState::OpeningC if c == 'D' => vec![Token::CData(Self{state: CDataState::OpeningD, ..self})], + CDataState::OpeningD if c == 'A' => vec![Token::CData(Self{state: CDataState::OpeningA1, ..self})], + CDataState::OpeningA1 if c == 'T' => vec![Token::CData(Self{state: CDataState::OpeningT, ..self})], + CDataState::OpeningT if c == 'A' => vec![Token::CData(Self{state: CDataState::OpeningA2, ..self})], + CDataState::OpeningA2 if c == '[' => vec![Token::CData(Self{state: CDataState::OpeningBracket3, ..self})], + CDataState::OpeningBracket3 if c == ']' => vec![Token::CData(Self{state: CDataState::ClosingBracket1, ..self})], + CDataState::OpeningBracket3 if c != ']' => vec![Token::CData(Self{state: CDataState::Content, ..self})], + CDataState::ClosingBracket1 if c == ']' => vec![Token::CData(Self{state: CDataState::ClosingBracket2, ..self})], + CDataState::ClosingBracket2 if c == '>' => vec![Token::CData(Self{state: CDataState::ClosingBracket3, ..self})], + CDataState::ClosingBracket3 => self.handle_closing_bracket3(c), + CDataState::Content => self.handle_content(c), + _ => vec![], + } + } + + fn handle_closing_bracket3(self, c: char) -> Vec { + if '<' == c { + let is_mixed = self + .element + .xs_complex_type + .mixed + .as_ref() + .map(|val| val == "true") + .unwrap_or(false); + + let mut tokens = vec![Token::ElementCloseStart(ElementCloseStart::new( + Arc::clone(&self.element), + ))]; + + // If mixed content is allowed, also add Comment and CData as possible transitions + if is_mixed { + tokens.push(Token::Comment(Comment::new(Arc::clone(&self.element)))); + tokens.push(Token::CData(CData::new(Arc::clone(&self.element)))); + } + + tokens + } else { + // Check if this element allows mixed content + let is_mixed = self + .element + .xs_complex_type + .mixed + .as_ref() + .map(|val| val == "true") + .unwrap_or(false); + + if c.is_whitespace() { + vec![Token::CData(self)] + } else if is_mixed { + vec![Token::TextContent(TextContent::new(self.element))] + } else { + vec![] + } + } + } + + fn handle_content(self, c: char) -> Vec { + todo!() + } + +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_valid_cdata_sections() { + let values = [ + "", + "Text more text", + "text]]>", + "", + "&'\"]]>", + " Empty CDATA", // Empty CDATA is valid + "", // Empty CDATA + ]; + + for value in values { + let mut validator = crate::tests::example_validator(); + validator.append(value).expect(value); + assert!(!validator.current_tokens.is_empty(), "{value}"); + assert!( + validator + .current_tokens + .iter() + .any(|token| matches!(token, Token::EndOfFile(_))), + "{value}" + ); + } + } + + #[test] + fn test_partial_cdata_sections() { + let values = [ + "", + "", + "]]>", // Nesting not allowed + "", + " Missing closing bracket", + ]; + + for value in values { + let mut validator = crate::tests::example_validator(); + let result = validator.append(value); + assert!( + result.is_err(), + "Should reject invalid CDATA syntax in '{value}'" + ); + } + } + + #[test] + fn test_cdata_in_mixed_content_only() { + // CDATA should only be allowed in mixed content elements + let non_mixed_elements = [ + " />", + "content]]>", // XML tags + " \" ']]>", // Special characters + "", // Newlines + "", // Empty + ]; + + for value in values { + let mut validator = crate::tests::example_validator(); + validator.append(value).expect(value); + assert!(!validator.current_tokens.is_empty(), "{value}"); + assert!( + validator + .current_tokens + .iter() + .any(|token| matches!(token, Token::EndOfFile(_))), + "{value}" + ); + } + } +} diff --git a/lib/xml_schema_validator/src/token/comment.rs b/lib/xml_schema_validator/src/token/comment.rs new file mode 100644 index 0000000..1b436cd --- /dev/null +++ b/lib/xml_schema_validator/src/token/comment.rs @@ -0,0 +1,196 @@ +use crate::*; +use std::sync::Arc; + +/// Represents an XML comment that is in the process of being parsed +#[derive(Clone, Debug)] +pub struct Comment { + /// The element that contains this comment + element: Arc, + /// Buffer containing the comment characters processed so far + buffer: String, + /// Tracks if we're at the end of the comment (checking for -->) + end_dashes: u8, +} + +impl Comment { + /// Create a new Comment parsing state after seeing ") -> Self { + Self { + element, + buffer: " Vec { + // Add character to buffer + self.buffer.push(c); + + let buffer_len = self.buffer.len(); + + // Check for comment opening sequence "" + if self.end_dashes == 0 && c == '-' { + self.end_dashes = 1; + return vec![Token::Comment(self)]; + } else if self.end_dashes == 1 { + if c == '-' { + self.end_dashes = 2; + return vec![Token::Comment(self)]; + } else { + // Reset dash count if we didn't see a second dash + self.end_dashes = 0; + return vec![Token::Comment(self)]; + } + } else if self.end_dashes == 2 { + if c == '>' { + // Comment is complete, transition back to text content + return vec![Token::TextContent(TextContent::new(self.element))]; + } else if c == '-' { + // -- followed by another - is not allowed in XML comments + // but we'll be lenient and just keep the dash count at 2 + return vec![Token::Comment(self)]; + } else { + // Reset dash count if we didn't see a closing bracket + self.end_dashes = 0; + return vec![Token::Comment(self)]; + } + } + + // Regular comment content + vec![Token::Comment(self)] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_valid_comments() { + let values = [ + "", + "Text more text", + " text ", + "", + "", + "", + "", // Empty comment + ]; + + for value in values { + let mut validator = crate::tests::example_validator(); + validator.append(value).expect(value); + assert!(!validator.current_tokens.is_empty(), "{value}"); + assert!( + validator + .current_tokens + .iter() + .any(|token| matches!(token, Token::EndOfFile(_))), + "{value}" + ); + } + } + + #[test] + fn test_partial_comments() { + let values = [ + " -->", // Nesting not allowed + "", + " Malformed comment", + " Suspicious triple dash", + ]; + + for value in values { + let mut validator = crate::tests::example_validator(); + let result = validator.append(value); + assert!( + result.is_err(), + "Should reject invalid comment syntax in '{value}'" + ); + } + } + + #[test] + fn test_comments_in_mixed_content_only() { + // Comments should only be allowed in mixed content elements + let non_mixed_elements = [ + " />", + " />", + "", + ]; + + for value in non_mixed_elements { + let mut validator = crate::tests::example_validator(); + let result = validator.append(value); + assert!( + result.is_err(), + "Comments should only be allowed in mixed content: '{value}'" + ); + } + } + + #[test] + fn test_comments_with_special_characters() { + let values = [ + "", // XML tags in comment + "", // Special characters + "", // Newlines + ]; + + for value in values { + let mut validator = crate::tests::example_validator(); + validator.append(value).expect(value); + assert!(!validator.current_tokens.is_empty(), "{value}"); + assert!( + validator + .current_tokens + .iter() + .any(|token| matches!(token, Token::EndOfFile(_))), + "{value}" + ); + } + } +} \ No newline at end of file diff --git a/lib/xml_schema_validator/src/token/element_open_end.rs b/lib/xml_schema_validator/src/token/element_open_end.rs index b17624c..6417e90 100644 --- a/lib/xml_schema_validator/src/token/element_open_end.rs +++ b/lib/xml_schema_validator/src/token/element_open_end.rs @@ -14,9 +14,25 @@ impl ElementOpenEnd { pub fn append(self, c: char) -> Vec { if '<' == c { - vec![Token::ElementCloseStart(ElementCloseStart::new( + let is_mixed = self + .element + .xs_complex_type + .mixed + .as_ref() + .map(|val| val == "true") + .unwrap_or(false); + + let mut tokens = vec![Token::ElementCloseStart(ElementCloseStart::new( Arc::clone(&self.element), - ))] + ))]; + + // If mixed content is allowed, also add Comment and CData as possible transitions + if is_mixed { + tokens.push(Token::Comment(Comment::new(Arc::clone(&self.element)))); + tokens.push(Token::CData(CData::new(Arc::clone(&self.element)))); + } + + tokens } else { // Check if this element allows mixed content let is_mixed = self @@ -124,4 +140,44 @@ mod tests { ); } } -} + + #[test] + fn test_mixed_comments_cdata_after_open() { + let values = [ + "", + "", + ]; + + for value in values { + let mut validator = crate::tests::example_validator(); + validator.append(value).expect(value); + assert!(!validator.current_tokens.is_empty(), "{value}"); + assert!( + validator + .current_tokens + .iter() + .any(|token| matches!(token, Token::EndOfFile(_))), + "{value}" + ); + } + } + + #[test] + fn test_invalid_comment_cdata_in_non_mixed_elements() { + let values = [ + "", + "", + "", + "", + ]; + + for value in values { + let mut validator = crate::tests::example_validator(); + let result = validator.append(value); + assert!( + result.is_err(), + "Comments and CDATA should not be allowed in non-mixed content elements: '{value}'" + ); + } + } +} \ No newline at end of file diff --git a/lib/xml_schema_validator/src/token/mod.rs b/lib/xml_schema_validator/src/token/mod.rs index af4f1c9..1e57efc 100644 --- a/lib/xml_schema_validator/src/token/mod.rs +++ b/lib/xml_schema_validator/src/token/mod.rs @@ -1,6 +1,8 @@ mod attribute_equals; mod attribute_name; mod attribute_value; +mod cdata; +mod comment; mod element_close_name; mod element_close_start; mod element_open_end; @@ -14,6 +16,8 @@ mod text_content; pub use attribute_equals::AttributeEquals; pub use attribute_name::AttributeName; pub use attribute_value::AttributeValue; +pub use cdata::CData; +pub use comment::Comment; pub use element_close_name::ElementCloseName; pub use element_close_start::ElementCloseStart; pub use element_open_end::ElementOpenEnd; @@ -30,6 +34,8 @@ pub enum Token { AttributeEquals(AttributeEquals), AttributeName(AttributeName), AttributeValue(AttributeValue), + CData(CData), + Comment(Comment), ElementCloseName(ElementCloseName), ElementCloseStart(ElementCloseStart), ElementOpenEnd(ElementOpenEnd), @@ -45,17 +51,19 @@ impl Token { pub fn append(self, c: char) -> Vec { match self { Token::AttributeEquals(attribute_equals) => attribute_equals.append(c), + Token::AttributeName(attribute_name) => attribute_name.append(c), + Token::AttributeValue(attribute_value) => attribute_value.append(c), + Token::CData(cdata) => cdata.append(c), // Handle CDATA token + Token::Comment(comment) => comment.append(c), // Handle Comment token Token::ElementCloseName(element_close_name) => element_close_name.append(c), Token::ElementCloseStart(element_close_start) => element_close_start.append(c), Token::ElementOpenName(element_name) => element_name.append(c), Token::ElementOpenEnd(element_open) => element_open.append(c), Token::ElementOpenStart(element_start) => element_start.append(c), + Token::ElementSelfClose(element_self_close) => element_self_close.append(c), Token::EndOfFile(end_of_file) => end_of_file.append(c), Token::StartOfFile(start_of_file) => start_of_file.append(c), Token::TextContent(text_content) => text_content.append(c), - Token::AttributeName(attribute_name) => attribute_name.append(c), - Token::AttributeValue(attribute_value) => attribute_value.append(c), - Token::ElementSelfClose(element_self_close) => element_self_close.append(c), } } @@ -65,4 +73,4 @@ impl Token { _ => false, } } -} +} \ No newline at end of file diff --git a/lib/xml_schema_validator/src/token/text_content.rs b/lib/xml_schema_validator/src/token/text_content.rs index 39f0abc..ff34e97 100644 --- a/lib/xml_schema_validator/src/token/text_content.rs +++ b/lib/xml_schema_validator/src/token/text_content.rs @@ -14,9 +14,25 @@ impl TextContent { pub fn append(self, c: char) -> Vec { if c == '<' { - vec![Token::ElementCloseStart(ElementCloseStart::new( + let is_mixed = self + .element + .xs_complex_type + .mixed + .as_ref() + .map(|val| val == "true") + .unwrap_or(false); + + let mut tokens = vec![Token::ElementCloseStart(ElementCloseStart::new( Arc::clone(&self.element), - ))] + ))]; + + // If mixed content is allowed, also add Comment and CData as possible transitions + if is_mixed { + tokens.push(Token::Comment(Comment::new(Arc::clone(&self.element)))); + tokens.push(Token::CData(CData::new(Arc::clone(&self.element)))); + } + + tokens } else { vec![Token::TextContent(TextContent::new(Arc::clone( &self.element, @@ -126,4 +142,81 @@ mod tests { ); } } -} + + // New tests for transitions to comment and CDATA + + #[test] + fn test_text_to_comment_transition() { + let values = [ + "text<", + "texttextmore text", + "textmore text", + "textmore", + "text", + ]; + + for value in values { + let mut validator = crate::tests::example_validator(); + validator.append(value).expect(value); + assert!(!validator.current_tokens.is_empty(), "{value}"); + assert!( + validator + .current_tokens + .iter() + .any(|token| matches!(token, Token::EndOfFile(_))), + "{value}" + ); + } + } +} \ No newline at end of file