tamer: xir::flat: Initial XIRF implementation
This introduces XIR Flat (XIRF), which is conceptually between XIR and XIRT. This provides a more appropriate level of abstraction for further lowering operations to parse against, and removes the need for other parsers to perform their own validations (inappropriately) to ensure well-formed XML. There is still some cleanup worth doing, including moving some of the parsing responsibility up a level back into the XIR parser. DEV-10863main
parent
ce48a654b1
commit
899fa79e59
|
@ -27,6 +27,9 @@
|
|||
// See that function for more information.
|
||||
#![feature(const_fn_trait_bound)]
|
||||
#![feature(const_transmute_copy)]
|
||||
// This is used to unwrap const Option results rather than providing
|
||||
// panicing alternatives.
|
||||
#![feature(const_option)]
|
||||
// Trait aliases are convenient for reducing verbosity in situations where
|
||||
// type aliases cannot be used.
|
||||
// To remove this feature if it is not stabalized,
|
||||
|
@ -41,6 +44,13 @@
|
|||
// this can be done more verbosely in the usual way,
|
||||
// or we can write our own version.
|
||||
#![feature(option_get_or_insert_default)]
|
||||
// This allows for e.g. `parse::<N>(foo)`,
|
||||
// where `fn parse<const N: T>(foo: impl Trait)`.
|
||||
// Rust devs wanted more time for public testing as of the time of writing
|
||||
// (March 2022).
|
||||
// We _could_ do without,
|
||||
// but this provides a nicer API.
|
||||
#![feature(explicit_generic_args_with_impl_trait)]
|
||||
// We build docs for private items.
|
||||
#![allow(rustdoc::private_intra_doc_links)]
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ pub use error::Error;
|
|||
mod escape;
|
||||
pub use escape::{DefaultEscaper, Escaper};
|
||||
|
||||
pub mod flat;
|
||||
pub mod iter;
|
||||
pub mod parse;
|
||||
pub mod pred;
|
||||
|
|
|
@ -0,0 +1,397 @@
|
|||
// XIR flat (XIRF)
|
||||
//
|
||||
// Copyright (C) 2014-2021 Ryan Specialty Group, LLC.
|
||||
//
|
||||
// This file is part of TAME.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Lightly-parsed XIR as a flat stream (XIRF).
|
||||
//!
|
||||
//! XIRF lightly parses a raw XIR [`TokenStream`] into a stream of
|
||||
//! [`Object`]s that are,
|
||||
//! like a [`TokenStream`],
|
||||
//! flat in structure.
|
||||
//! It provides the following features over raw XIR:
|
||||
//!
|
||||
//! 1. All closing tags must correspond to a matching opening tag at the
|
||||
//! same depth;
|
||||
//! 2. [`Object`] exposes the [`Depth`] of each opening/closing tag;
|
||||
//! 3. Attribute tokens are parsed into [`Attr`] objects; and
|
||||
//! 4. Parsing will fail if input ends before all elements have been
|
||||
//! closed.
|
||||
//!
|
||||
//! XIRF lowering does not perform any dynamic memory allocation;
|
||||
//! maximum element nesting depth is set statically depending on the needs
|
||||
//! of the caller.
|
||||
|
||||
use super::{
|
||||
parse::{ParseState, ParseStateResult, ParseStatus, ParsedResult},
|
||||
tree::{
|
||||
attr::{AttrParseError, AttrParseState},
|
||||
Attr,
|
||||
},
|
||||
QName, Token, TokenStream, Whitespace,
|
||||
};
|
||||
use crate::{span::Span, sym::SymbolId};
|
||||
use arrayvec::ArrayVec;
|
||||
use std::{error::Error, fmt::Display, mem::replace};
|
||||
|
||||
/// Tag nesting depth
|
||||
/// (`0` represents the root).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Depth(usize);
|
||||
|
||||
impl Display for Depth {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
/// A lightly-parsed XIRF object.
|
||||
///
|
||||
/// Certain XIR [`Token`]s are formed into a single object,
|
||||
/// such as an [`Attr`].
|
||||
/// Other objects retain the same format as their underlying token,
|
||||
/// but are still validated to ensure that they are well-formed and that
|
||||
/// the XML is well-structured.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Object {
|
||||
/// Opening tag of an element.
|
||||
Open(QName, Span, Depth),
|
||||
|
||||
/// Closing tag of an element.
|
||||
///
|
||||
/// If the name is [`None`],
|
||||
/// then the tag is self-closing.
|
||||
/// If the name is [`Some`],
|
||||
/// then the tag is guaranteed to be balanced
|
||||
/// (matching the depth of its opening tag).
|
||||
Close(Option<QName>, Span, Depth),
|
||||
|
||||
/// An attribute and its value.
|
||||
///
|
||||
/// The associated [`Span`]s can be found on the enclosed [`Attr`]
|
||||
/// object.
|
||||
Attr(Attr),
|
||||
|
||||
/// Comment node.
|
||||
Comment(SymbolId, Span),
|
||||
|
||||
/// Character data as part of an element.
|
||||
///
|
||||
/// See also [`CData`](Object::CData) variant.
|
||||
Text(SymbolId, Span),
|
||||
|
||||
/// CData node (`<![CDATA[...]]>`).
|
||||
///
|
||||
/// _Warning: It is up to the caller to ensure that the string `]]>` is
|
||||
/// not present in the text!_
|
||||
/// This is intended for reading existing XML data where CData is
|
||||
/// already present,
|
||||
/// not for producing new CData safely!
|
||||
CData(SymbolId, Span),
|
||||
|
||||
/// Similar to `Text`,
|
||||
/// but intended for use where only whitespace is allowed,
|
||||
/// such as alignment of attributes.
|
||||
Whitespace(Whitespace, Span),
|
||||
}
|
||||
|
||||
/// XIRF-compatible attribute parser.
|
||||
pub trait FlatAttrParseState = ParseState<Object = Attr>
|
||||
where
|
||||
<Self as ParseState>::Error: Into<StateError>;
|
||||
|
||||
/// Stack of element [`QName`] and [`Span`] pairs,
|
||||
/// representing the current level of nesting.
|
||||
///
|
||||
/// This storage is statically allocated,
|
||||
/// allowing XIRF's parser to avoid memory allocation entirely.
|
||||
type ElementStack<const MAX_DEPTH: usize> = ArrayVec<(QName, Span), MAX_DEPTH>;
|
||||
|
||||
/// XIRF parser state.
|
||||
///
|
||||
/// This parser is a pushdown automaton.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum State<const MAX_DEPTH: usize, SA = AttrParseState>
|
||||
where
|
||||
SA: FlatAttrParseState,
|
||||
{
|
||||
// TODO: Ensure that non-comment nodes are not encountered before the
|
||||
// root,
|
||||
// and that we do not encounter any non-comment nodes after the
|
||||
// root.
|
||||
/// Parsing nodes.
|
||||
NodeExpected(ElementStack<MAX_DEPTH>),
|
||||
|
||||
/// Delegating to attribute parser.
|
||||
AttrExpected(ElementStack<MAX_DEPTH>, SA),
|
||||
|
||||
/// Temporary state used to catch missing explicit state transitions in
|
||||
/// `parse_token`.
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl<const MD: usize, SA: FlatAttrParseState> Default for State<MD, SA> {
|
||||
fn default() -> Self {
|
||||
Self::NodeExpected(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// Denotes a state transition.
|
||||
///
|
||||
/// This newtype was created to produce clear, self-documenting code.
|
||||
struct Transition<T>(T);
|
||||
|
||||
impl<const MAX_DEPTH: usize, SA> ParseState for State<MAX_DEPTH, SA>
|
||||
where
|
||||
SA: FlatAttrParseState,
|
||||
{
|
||||
type Object = Object;
|
||||
type Error = StateError;
|
||||
|
||||
fn parse_token(&mut self, tok: Token) -> ParseStateResult<Self> {
|
||||
use ParseStatus::{Dead, Incomplete, Object as Obj};
|
||||
use State::{AttrExpected, Invalid, NodeExpected};
|
||||
|
||||
let result;
|
||||
|
||||
// This awkward-looking take-reassign forces us to be explicit
|
||||
// about state transitions in every case,
|
||||
// ensuring that we always have documented proof of what state
|
||||
// the system winds up in.
|
||||
// The `Invalid` state prevents using `return`.
|
||||
(Transition(*self), result) = match (replace(self, Invalid), tok) {
|
||||
(NodeExpected(stack), tok) => Self::parse_node(stack, tok),
|
||||
|
||||
(AttrExpected(stack, mut sa), tok) => match sa.parse_token(tok) {
|
||||
Ok(Incomplete) => {
|
||||
(Transition(AttrExpected(stack, sa)), Ok(Incomplete))
|
||||
}
|
||||
Ok(Obj(attr)) => (
|
||||
Transition(AttrExpected(stack, sa)),
|
||||
Ok(Obj(Object::Attr(attr))),
|
||||
),
|
||||
Ok(Dead(lookahead)) => Self::parse_node(stack, lookahead),
|
||||
Err(x) => (Transition(AttrExpected(stack, sa)), Err(x.into())),
|
||||
},
|
||||
|
||||
// See comment at the top of this function.
|
||||
(Invalid, _) => {
|
||||
unreachable!("XIRF parser reached invalid state")
|
||||
}
|
||||
};
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Whether all elements have been closed.
|
||||
///
|
||||
/// Parsing will fail if there are any open elements.
|
||||
/// Intuitively,
|
||||
/// this means that the parser must have encountered the closing tag
|
||||
/// for the root element.
|
||||
fn is_accepting(&self) -> bool {
|
||||
// TODO: It'd be nice if we could also return additional context to
|
||||
// aid the user in diagnosing the problem,
|
||||
// e.g. what element(s) still need closing.
|
||||
matches!(self, Self::NodeExpected(stack) if stack.len() == 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const MAX_DEPTH: usize, SA> State<MAX_DEPTH, SA>
|
||||
where
|
||||
SA: FlatAttrParseState,
|
||||
{
|
||||
/// Parse a token while in a state expecting a node.
|
||||
fn parse_node(
|
||||
mut stack: ElementStack<MAX_DEPTH>,
|
||||
tok: Token,
|
||||
) -> (Transition<Self>, ParseStateResult<Self>) {
|
||||
use ParseStatus::Object as Obj;
|
||||
use State::{AttrExpected, NodeExpected};
|
||||
|
||||
match tok {
|
||||
Token::Open(qname, span) if stack.len() == MAX_DEPTH => (
|
||||
Transition(NodeExpected(stack)),
|
||||
Err(StateError::MaxDepthExceeded {
|
||||
open: (qname, span),
|
||||
max: Depth(MAX_DEPTH),
|
||||
}),
|
||||
),
|
||||
|
||||
Token::Open(qname, span) => {
|
||||
let depth = stack.len();
|
||||
stack.push((qname, span));
|
||||
|
||||
// Delegate to the attribute parser until it is complete.
|
||||
(
|
||||
Transition(AttrExpected(stack, SA::default())),
|
||||
Ok(Obj(Object::Open(qname, span, Depth(depth)))),
|
||||
)
|
||||
}
|
||||
|
||||
Token::Close(close_oqname, close_span) => {
|
||||
match (close_oqname, stack.pop()) {
|
||||
(_, None) => (
|
||||
Transition(NodeExpected(stack)),
|
||||
Err(StateError::ExtraClosingTag(
|
||||
close_oqname,
|
||||
close_span,
|
||||
)),
|
||||
),
|
||||
|
||||
(Some(qname), Some((open_qname, open_span)))
|
||||
if qname != open_qname =>
|
||||
{
|
||||
(
|
||||
Transition(NodeExpected(stack)),
|
||||
Err(StateError::UnbalancedTag {
|
||||
open: (open_qname, open_span),
|
||||
close: (qname, close_span),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
(..) => {
|
||||
let depth = stack.len();
|
||||
(
|
||||
Transition(NodeExpected(stack)),
|
||||
Ok(Obj(Object::Close(
|
||||
close_oqname,
|
||||
close_span,
|
||||
Depth(depth),
|
||||
))),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Token::Comment(sym, span) => (
|
||||
Transition(NodeExpected(stack)),
|
||||
Ok(Obj(Object::Comment(sym, span))),
|
||||
),
|
||||
Token::Text(sym, span) => (
|
||||
Transition(NodeExpected(stack)),
|
||||
Ok(Obj(Object::Text(sym, span))),
|
||||
),
|
||||
Token::CData(sym, span) => (
|
||||
Transition(NodeExpected(stack)),
|
||||
Ok(Obj(Object::CData(sym, span))),
|
||||
),
|
||||
Token::Whitespace(ws, span) => (
|
||||
Transition(NodeExpected(stack)),
|
||||
Ok(Obj(Object::Whitespace(ws, span))),
|
||||
),
|
||||
|
||||
// We should transition to `State::Attr` before encountering any
|
||||
// of these tokens.
|
||||
Token::AttrName(..)
|
||||
| Token::AttrValue(..)
|
||||
| Token::AttrValueFragment(..) => {
|
||||
unreachable!("attribute token in NodeExpected state: {tok:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Produce a streaming parser lowering a XIR [`TokenStream`] into a XIRF
|
||||
/// stream.
|
||||
pub fn parse<const MAX_DEPTH: usize>(
|
||||
toks: impl TokenStream,
|
||||
) -> impl Iterator<Item = ParsedResult<State<MAX_DEPTH>>> {
|
||||
State::<MAX_DEPTH>::parse(toks)
|
||||
}
|
||||
|
||||
/// Parsing error from [`State`].
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum StateError {
|
||||
/// Opening tag exceeds the maximum nesting depth for this parser.
|
||||
MaxDepthExceeded { open: (QName, Span), max: Depth },
|
||||
|
||||
/// The closing tag does not match the opening tag at the same level of
|
||||
/// nesting.
|
||||
UnbalancedTag {
|
||||
open: (QName, Span),
|
||||
close: (QName, Span),
|
||||
},
|
||||
|
||||
/// Attempt to close a tag with no corresponding opening tag
|
||||
/// (which would result in a negative depth).
|
||||
ExtraClosingTag(Option<QName>, Span),
|
||||
|
||||
/// Error from the attribute parser.
|
||||
AttrError(AttrParseError),
|
||||
}
|
||||
|
||||
impl Display for StateError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use StateError::*;
|
||||
|
||||
match self {
|
||||
MaxDepthExceeded {
|
||||
open: (name, span),
|
||||
max,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"maximum element nesting depth of {max} exceeded \
|
||||
by `{name}` at {span}"
|
||||
)
|
||||
}
|
||||
|
||||
UnbalancedTag {
|
||||
open: (open_name, open_span),
|
||||
close: (close_name, close_span),
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"expected closing tag `{open_name}`, \
|
||||
but found `{close_name}` at {close_span} \
|
||||
(opening tag at {open_span})",
|
||||
)
|
||||
}
|
||||
|
||||
ExtraClosingTag(Some(name), span) => {
|
||||
write!(f, "closing tag `{name}` at {span} has no opening tag",)
|
||||
}
|
||||
|
||||
// If this occurs, its likely that something generated invalid
|
||||
// XIR;
|
||||
// it should be a parsing error on read and no generator
|
||||
// should ever produce this.
|
||||
ExtraClosingTag(None, span) => {
|
||||
write!(f, "self-closing tag at {span} has no opening tag")
|
||||
}
|
||||
|
||||
AttrError(e) => Display::fmt(e, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for StateError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AttrParseError> for StateError {
|
||||
fn from(e: AttrParseError) -> Self {
|
||||
Self::AttrError(e)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
|
@ -0,0 +1,357 @@
|
|||
// Test XIRF representation
|
||||
//
|
||||
// Copyright (C) 2014-2021 Ryan Specialty Group, LLC.
|
||||
//
|
||||
// This file is part of TAME.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Integration tests for XIRF parser.
|
||||
//!
|
||||
//! These tests take place within the context of the XIR parsing framework,
|
||||
//! so they are one layer of abstraction away from unit tests.
|
||||
|
||||
use super::super::parse::ParseError;
|
||||
use super::*;
|
||||
use crate::convert::ExpectInto;
|
||||
use crate::span::DUMMY_SPAN;
|
||||
use crate::sym::GlobalSymbolIntern;
|
||||
use crate::xir::parse::Parsed;
|
||||
|
||||
const S: Span = DUMMY_SPAN;
|
||||
const S2: Span = S.offset_add(1).unwrap();
|
||||
const S3: Span = S2.offset_add(1).unwrap();
|
||||
const S4: Span = S3.offset_add(1).unwrap();
|
||||
|
||||
#[test]
|
||||
fn empty_element_self_close() {
|
||||
let name = ("ns", "elem").unwrap_into();
|
||||
|
||||
let toks = [Token::Open(name, S), Token::Close(None, S2)].into_iter();
|
||||
|
||||
let sut = parse::<1>(toks);
|
||||
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Parsed::Object(Object::Open(name, S, Depth(0))),
|
||||
Parsed::Object(Object::Close(None, S2, Depth(0))),
|
||||
]),
|
||||
sut.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// Same as above test, but with balanced closing instead of self
|
||||
// closing.
|
||||
#[test]
|
||||
fn empty_element_balanced_close() {
|
||||
let name = ("ns", "openclose").unwrap_into();
|
||||
|
||||
let toks = [Token::Open(name, S), Token::Close(Some(name), S2)].into_iter();
|
||||
|
||||
let sut = parse::<1>(toks);
|
||||
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Parsed::Object(Object::Open(name, S, Depth(0))),
|
||||
Parsed::Object(Object::Close(Some(name), S2, Depth(0))),
|
||||
]),
|
||||
sut.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// More closing tags than opening.
|
||||
#[test]
|
||||
fn extra_closing_tag() {
|
||||
let name = ("ns", "openclose").unwrap_into();
|
||||
let toks = [Token::Close(Some(name), S)].into_iter();
|
||||
|
||||
let mut sut = parse::<1>(toks);
|
||||
|
||||
assert_eq!(
|
||||
sut.next(),
|
||||
Some(Err(ParseError::StateError(StateError::ExtraClosingTag(
|
||||
Some(name),
|
||||
S,
|
||||
))))
|
||||
);
|
||||
}
|
||||
|
||||
// This should never happen, but let's operate in a sane way in case it ever
|
||||
// does, since that's not the user's fault (that is, we shouldn't have
|
||||
// gotten to XIRF).
|
||||
#[test]
|
||||
fn extra_self_closing_tag() {
|
||||
let toks = [Token::Close(None, S)].into_iter();
|
||||
|
||||
let mut sut = parse::<1>(toks);
|
||||
|
||||
assert_eq!(
|
||||
sut.next(),
|
||||
Some(Err(ParseError::StateError(StateError::ExtraClosingTag(
|
||||
None, S,
|
||||
))))
|
||||
);
|
||||
}
|
||||
|
||||
// Unbalanced should result in error. This does not test what happens
|
||||
// _after_ the error.
|
||||
#[test]
|
||||
fn empty_element_unbalanced_close() {
|
||||
let open_name = "open".unwrap_into();
|
||||
let close_name = "unbalanced_name".unwrap_into();
|
||||
|
||||
let toks = [
|
||||
Token::Open(open_name, S),
|
||||
Token::Close(Some(close_name), S2),
|
||||
]
|
||||
.into_iter();
|
||||
|
||||
let mut sut = parse::<1>(toks);
|
||||
|
||||
assert_eq!(
|
||||
sut.next(),
|
||||
Some(Ok(Parsed::Object(Object::Open(open_name, S, Depth(0)))))
|
||||
);
|
||||
assert_eq!(
|
||||
sut.next(),
|
||||
Some(Err(ParseError::StateError(StateError::UnbalancedTag {
|
||||
open: (open_name, S),
|
||||
close: (close_name, S2),
|
||||
})))
|
||||
);
|
||||
}
|
||||
|
||||
// Testing depth value.
|
||||
#[test]
|
||||
fn single_empty_child() {
|
||||
let name = ("ns", "openclose").unwrap_into();
|
||||
let child = "child".unwrap_into();
|
||||
|
||||
let toks = [
|
||||
Token::Open(name, S),
|
||||
Token::Open(child, S2),
|
||||
Token::Close(None, S3),
|
||||
Token::Close(Some(name), S4),
|
||||
]
|
||||
.into_iter();
|
||||
|
||||
let sut = parse::<2>(toks);
|
||||
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Parsed::Object(Object::Open(name, S, Depth(0))),
|
||||
Parsed::Object(Object::Open(child, S2, Depth(1))),
|
||||
Parsed::Object(Object::Close(None, S3, Depth(1))),
|
||||
Parsed::Object(Object::Close(Some(name), S4, Depth(0))),
|
||||
]),
|
||||
sut.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn depth_exceeded() {
|
||||
let name = ("ns", "openclose").unwrap_into();
|
||||
let exceed = "exceed".unwrap_into();
|
||||
|
||||
let toks = [
|
||||
Token::Open(name, S),
|
||||
// This one exceeds the max depth, ...
|
||||
Token::Open(exceed, S2),
|
||||
]
|
||||
.into_iter();
|
||||
|
||||
// ...which is set here: MAX_DEPTH here is 1
|
||||
let mut sut = parse::<1>(toks);
|
||||
|
||||
assert_eq!(
|
||||
Some(Ok(Parsed::Object(Object::Open(name, S, Depth(0))))),
|
||||
sut.next()
|
||||
);
|
||||
assert_eq!(
|
||||
Some(Err(ParseError::StateError(StateError::MaxDepthExceeded {
|
||||
open: (exceed, S2),
|
||||
max: Depth(1),
|
||||
}))),
|
||||
sut.next()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_element_with_attrs() {
|
||||
let name = ("ns", "elem").unwrap_into();
|
||||
let attr1 = "a".unwrap_into();
|
||||
let attr2 = "b".unwrap_into();
|
||||
let val1 = "val1".intern();
|
||||
let val2 = "val2".intern();
|
||||
|
||||
let toks = [
|
||||
Token::Open(name, S),
|
||||
Token::AttrName(attr1, S2),
|
||||
Token::AttrValue(val1, S3),
|
||||
Token::AttrName(attr2, S3),
|
||||
Token::AttrValue(val2, S4),
|
||||
Token::Close(None, S4),
|
||||
]
|
||||
.into_iter();
|
||||
|
||||
let sut = parse::<2>(toks);
|
||||
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Parsed::Object(Object::Open(name, S, Depth(0))),
|
||||
Parsed::Incomplete,
|
||||
Parsed::Object(Object::Attr(Attr::new(attr1, val1, (S2, S3)))),
|
||||
Parsed::Incomplete,
|
||||
Parsed::Object(Object::Attr(Attr::new(attr2, val2, (S3, S4)))),
|
||||
Parsed::Object(Object::Close(None, S4, Depth(0))),
|
||||
]),
|
||||
sut.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_element_after_attrs() {
|
||||
let name = ("ns", "elem").unwrap_into();
|
||||
let child = "child".unwrap_into();
|
||||
let attr = "a".unwrap_into();
|
||||
let val = "val".intern();
|
||||
|
||||
let toks = [
|
||||
Token::Open(name, S),
|
||||
Token::AttrName(attr, S),
|
||||
Token::AttrValue(val, S2),
|
||||
Token::Open(child, S),
|
||||
Token::Close(None, S2),
|
||||
Token::Close(Some(name), S3),
|
||||
]
|
||||
.into_iter();
|
||||
|
||||
let sut = parse::<2>(toks);
|
||||
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Parsed::Object(Object::Open(name, S, Depth(0))),
|
||||
Parsed::Incomplete,
|
||||
Parsed::Object(Object::Attr(Attr::new(attr, val, (S, S2)))),
|
||||
Parsed::Object(Object::Open(child, S, Depth(1))),
|
||||
Parsed::Object(Object::Close(None, S2, Depth(1))),
|
||||
Parsed::Object(Object::Close(Some(name), S3, Depth(0))),
|
||||
]),
|
||||
sut.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn element_with_empty_sibling_children() {
|
||||
let parent = "parent".unwrap_into();
|
||||
let childa = "childa".unwrap_into();
|
||||
let childb = "childb".unwrap_into();
|
||||
|
||||
let toks = [
|
||||
Token::Open(parent, S),
|
||||
Token::Open(childa, S2),
|
||||
Token::Close(None, S3),
|
||||
Token::Open(childb, S2),
|
||||
Token::Close(None, S3),
|
||||
Token::Close(Some(parent), S2),
|
||||
]
|
||||
.into_iter();
|
||||
|
||||
let sut = parse::<2>(toks);
|
||||
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Parsed::Object(Object::Open(parent, S, Depth(0))),
|
||||
Parsed::Object(Object::Open(childa, S2, Depth(1))),
|
||||
Parsed::Object(Object::Close(None, S3, Depth(1))),
|
||||
Parsed::Object(Object::Open(childb, S2, Depth(1))),
|
||||
Parsed::Object(Object::Close(None, S3, Depth(1))),
|
||||
Parsed::Object(Object::Close(Some(parent), S2, Depth(0))),
|
||||
]),
|
||||
sut.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
// Ensures that attributes do not cause the parent context to be lost.
|
||||
#[test]
|
||||
fn element_with_child_with_attributes() {
|
||||
let parent = "parent".unwrap_into();
|
||||
let child = "child".unwrap_into();
|
||||
let attr = "attr".unwrap_into();
|
||||
let value = "attr value".intern();
|
||||
|
||||
let toks = [
|
||||
Token::Open(parent, S),
|
||||
Token::Open(child, S),
|
||||
Token::AttrName(attr, S),
|
||||
Token::AttrValue(value, S2),
|
||||
Token::Close(None, S3),
|
||||
Token::Close(Some(parent), S3),
|
||||
]
|
||||
.into_iter();
|
||||
|
||||
let sut = parse::<2>(toks);
|
||||
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Parsed::Object(Object::Open(parent, S, Depth(0))),
|
||||
Parsed::Object(Object::Open(child, S, Depth(1))),
|
||||
Parsed::Incomplete,
|
||||
Parsed::Object(Object::Attr(Attr::new(attr, value, (S, S2)))),
|
||||
Parsed::Object(Object::Close(None, S3, Depth(1))),
|
||||
Parsed::Object(Object::Close(Some(parent), S3, Depth(0))),
|
||||
]),
|
||||
sut.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn element_with_text() {
|
||||
let parent = "parent".unwrap_into();
|
||||
let text = "inner text".into();
|
||||
|
||||
let toks = [
|
||||
Token::Open(parent, S),
|
||||
Token::Text(text, S2),
|
||||
Token::Close(Some(parent), S3),
|
||||
]
|
||||
.into_iter();
|
||||
|
||||
let sut = parse::<1>(toks);
|
||||
|
||||
assert_eq!(
|
||||
Ok(vec![
|
||||
Parsed::Object(Object::Open(parent, S, Depth(0))),
|
||||
Parsed::Object(Object::Text(text, S2)),
|
||||
Parsed::Object(Object::Close(Some(parent), S3, Depth(0))),
|
||||
]),
|
||||
sut.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_accepting_state_if_element_open() {
|
||||
let name = "unclosed".unwrap_into();
|
||||
let toks = [Token::Open(name, S)].into_iter();
|
||||
|
||||
let mut sut = parse::<1>(toks);
|
||||
|
||||
assert_eq!(
|
||||
Some(Ok(Parsed::Object(Object::Open(name, S, Depth(0))))),
|
||||
sut.next()
|
||||
);
|
||||
|
||||
// Element was not closed.
|
||||
assert_eq!(Some(Err(ParseError::UnexpectedEof(Some(S)))), sut.next());
|
||||
}
|
|
@ -173,7 +173,7 @@
|
|||
//!
|
||||
//! [state machine]: https://en.wikipedia.org/wiki/Finite-state_machine
|
||||
|
||||
mod attr;
|
||||
pub mod attr;
|
||||
|
||||
use self::{
|
||||
super::parse::{
|
||||
|
|
Loading…
Reference in New Issue