diff --git a/src/attr.rs b/src/attr.rs index c826ea6b..92f1bd9c 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -1,14 +1,7 @@ use crate::CowStr; use std::fmt; -/// Parse attributes, assumed to be valid. -pub(crate) fn parse(src: &str) -> Attributes { - let mut a = Attributes::new(); - a.parse(src); - a -} - -pub fn valid(src: &str) -> usize { +pub(crate) fn valid(src: &str) -> usize { use State::*; let mut n = 0; @@ -31,12 +24,20 @@ pub fn valid(src: &str) -> usize { /// Stores an attribute value that supports backslash escapes of ASCII punctuation upon displaying, /// without allocating. -#[derive(Clone, Debug, Eq, PartialEq)] +/// +/// Each value is paired together with an [`AttributeKind`] in order to form an element. +#[derive(Clone, Debug, Eq, PartialEq, Default)] pub struct AttributeValue<'s> { raw: CowStr<'s>, } impl<'s> AttributeValue<'s> { + /// Create an empty attribute value. + #[must_use] + pub fn new() -> Self { + Self::default() + } + /// Processes the attribute value escapes and returns an iterator of the parts of the value /// that should be displayed. pub fn parts(&'s self) -> AttributeValueParts<'s> { @@ -45,6 +46,9 @@ impl<'s> AttributeValue<'s> { // lifetime is 's to avoid allocation if empty value is concatenated with single value fn extend(&mut self, s: &'s str) { + if s.is_empty() { + return; + } match &mut self.raw { CowStr::Borrowed(prev) => { if prev.is_empty() { @@ -54,8 +58,12 @@ impl<'s> AttributeValue<'s> { } } CowStr::Owned(ref mut prev) => { - prev.push(' '); - prev.push_str(s); + if prev.is_empty() { + self.raw = s.into(); + } else { + prev.push(' '); + prev.push_str(s); + } } } } @@ -114,12 +122,155 @@ impl<'s> Iterator for AttributeValueParts<'s> { } } -/// A collection of attributes, i.e. a key-value map. -// Attributes are relatively rare, we choose to pay 8 bytes always and sometimes an extra -// indirection instead of always 24 bytes. -#[allow(clippy::box_collection)] +/// The kind of an element within an attribute set. +/// +/// Each kind is paired together with an [`AttributeValue`] to form an element. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AttributeKind<'s> { + /// A class element, e.g. `.a`. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let mut a = Attributes::try_from("{.a}").unwrap().into_iter(); + /// assert_eq!(a.next(), Some((AttributeKind::Class, "a".into()))); + /// assert_eq!(a.next(), None); + /// ``` + Class, + /// An id element, e.g. `#a`. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let mut a = Attributes::try_from("{#a}").unwrap().into_iter(); + /// assert_eq!(a.next(), Some((AttributeKind::Id, "a".into()))); + /// assert_eq!(a.next(), None); + /// ``` + Id, + /// A key-value pair element, e.g. `key=value`. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let mut a = Attributes::try_from(r#"{key=value id="a"}"#) + /// .unwrap() + /// .into_iter(); + /// assert_eq!( + /// a.next(), + /// Some((AttributeKind::Pair { key: "key" }, "value".into())), + /// ); + /// assert_eq!( + /// a.next(), + /// Some((AttributeKind::Pair { key: "id" }, "a".into())), + /// ); + /// assert_eq!(a.next(), None); + /// ``` + Pair { key: &'s str }, + /// A comment element, e.g. `%cmt%`. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let mut a = Attributes::try_from("{%cmt0% %cmt1}").unwrap().into_iter(); + /// assert_eq!(a.next(), Some((AttributeKind::Comment, "cmt0".into()))); + /// assert_eq!(a.next(), Some((AttributeKind::Comment, "cmt1".into()))); + /// assert_eq!(a.next(), None); + /// ``` + Comment, +} + +impl<'s> AttributeKind<'s> { + /// Returns the element's key, if applicable. + #[must_use] + pub fn key(&self) -> Option<&'s str> { + match self { + AttributeKind::Class => Some("class"), + AttributeKind::Id => Some("id"), + AttributeKind::Pair { key } => Some(key), + AttributeKind::Comment => None, + } + } +} + +/// A set of attributes, with order, duplicates and comments preserved. +/// +/// `Attributes` is a wrapper object around a [`Vec`] containing the elements of the set, each a +/// pair of an [`AttributeKind`] and an [`AttributeValue`]. It implements [`std::ops::Deref`] and +/// [`std::ops::DerefMut`] so methods of the inner [`Vec`] and [`slice`] can be used directly on +/// the `Attributes` to access or modify the elements. The wrapper also implements [`From`] and +/// [`Into`] for [`Vec`] so one can easily add or remove the wrapper. +/// +/// `Attributes` are typically created by a [`crate::Parser`] and placed in the [`crate::Event`]s +/// that it emits. `Attributes` can also be created from a djot string representation, see +/// [`Attributes::try_from`]. +/// +/// The attribute elements can be accessed using e.g. [`slice::iter`] or [`slice::iter_mut`], but +/// if e.g. duplicate keys or comments are not desired, refer to [`Attributes::get_value`] and +/// [`Attributes::unique_pairs`]. +/// +/// # Examples +/// +/// Access the inner [`Vec`]: +/// +/// ``` +/// # use jotdown::*; +/// let a: Attributes = r#"{#a .b id=c class=d key="val" %comment%}"# +/// .try_into() +/// .unwrap(); +/// assert_eq!( +/// Vec::from(a), +/// vec![ +/// (AttributeKind::Id, "a".into()), +/// (AttributeKind::Class, "b".into()), +/// (AttributeKind::Pair { key: "id" }, "c".into()), +/// (AttributeKind::Pair { key: "class" }, "d".into()), +/// (AttributeKind::Pair { key: "key" }, "val".into()), +/// (AttributeKind::Comment, "comment".into()), +/// ], +/// ); +/// ``` +/// +/// Replace a value: +/// +/// ``` +/// # use jotdown::*; +/// let mut attrs = Attributes::try_from("{key1=val1 key2=val2}").unwrap(); +/// +/// for (attr, value) in &mut attrs { +/// if attr.key() == Some("key2") { +/// *value = "new_val".into(); +/// } +/// } +/// +/// assert_eq!( +/// attrs.as_slice(), +/// &[ +/// (AttributeKind::Pair { key: "key1" }, "val1".into()), +/// (AttributeKind::Pair { key: "key2" }, "new_val".into()), +/// ] +/// ); +/// ``` +/// +/// Filter out keys with a specific prefix: +/// +/// ``` +/// # use jotdown::*; +/// let a: Attributes = Attributes::try_from("{ign:x=a ign:y=b z=c}") +/// .unwrap() +/// .into_iter() +/// .filter(|(k, _)| !matches!(k.key(), Some(key) if key.starts_with("ign:"))) +/// .collect(); +/// let b = Attributes::try_from("{z=c}").unwrap(); +/// assert_eq!(a, b); +/// ``` #[derive(Clone, PartialEq, Eq, Default)] -pub struct Attributes<'s>(Option)>>>); +pub struct Attributes<'s>(Vec>); + +type AttributeElem<'s> = (AttributeKind<'s>, AttributeValue<'s>); impl<'s> Attributes<'s> { /// Create an empty collection. @@ -130,203 +281,415 @@ impl<'s> Attributes<'s> { #[must_use] pub(crate) fn take(&mut self) -> Self { - Self(self.0.take()) + std::mem::take(self) } - /// Parse and append attributes, assumed to be valid. - pub(crate) fn parse(&mut self, input: &'s str) { + /// Parse and append attributes. + pub(crate) fn parse(&mut self, input: &'s str) -> Result<(), usize> { let mut parser = Parser::new(self.take()); - parser.parse(input); + parser.parse(input)?; *self = parser.finish(); + Ok(()) } - /// Combine all attributes from both objects, prioritizing self on conflicts. - pub(crate) fn union(&mut self, other: Self) { - if let Some(attrs0) = &mut self.0 { - if let Some(mut attrs1) = other.0 { - for (key, val) in attrs1.drain(..) { - if key == "class" || !attrs0.iter().any(|(k, _)| *k == key) { - attrs0.push((key, val)); - } - } - } - } else { - self.0 = other.0; - } - } - - /// Insert an attribute. If the attribute already exists, the previous value will be - /// overwritten, unless it is a "class" attribute. In that case the provided value will be - /// appended to the existing value. - pub fn insert(&mut self, key: &'s str, val: AttributeValue<'s>) { - self.insert_pos(key, val); + /// Returns whether the specified key exists in the set. + /// + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let a = Attributes::try_from("{x=y .a}").unwrap(); + /// assert!(a.contains_key("x")); + /// assert!(!a.contains_key("y")); + /// assert!(a.contains_key("class")); + /// ``` + #[must_use] + pub fn contains_key(&self, key: &str) -> bool { + self.0 + .iter() + .any(|(k, _)| matches!(k.key(), Some(k) if k == key)) } - // duplicate of insert but returns position of inserted value - fn insert_pos(&mut self, key: &'s str, val: AttributeValue<'s>) -> usize { - if self.0.is_none() { - self.0 = Some(Vec::new().into()); - }; - - let attrs = self.0.as_mut().unwrap(); - if let Some(i) = attrs.iter().position(|(k, _)| *k == key) { - let prev = &mut attrs[i].1; - if key == "class" { - match val.raw { - CowStr::Borrowed(s) => prev.extend(s), - CowStr::Owned(s) => { - *prev = format!("{} {}", prev, s).into(); - } + /// Returns the value corresponding to the provided attribute key. + /// + /// Note: A copy of the value is returned rather than a reference, due to class values + /// differing from its internal representation. + /// + /// # Examples + /// + /// For the "class" key, concatenate all class values: + /// + /// ``` + /// # use jotdown::*; + /// assert_eq!( + /// Attributes::try_from("{.a class=b}").unwrap().get_value("class"), + /// Some("a b".into()), + /// ); + /// ``` + /// + /// For other keys, return the last set value: + /// + /// ``` + /// # use jotdown::*; + /// assert_eq!( + /// Attributes::try_from("{x=a x=b}").unwrap().get_value("x"), + /// Some("b".into()), + /// ); + /// ``` + #[must_use] + pub fn get_value(&self, key: &str) -> Option { + if key == "class" + && self + .0 + .iter() + .filter(|(k, _)| k.key() == Some("class")) + .count() + > 1 + { + let mut value = AttributeValue::new(); + for (k, v) in &self.0 { + if k.key() == Some("class") { + value.extend(&v.raw); } - } else { - *prev = val; } - i + Some(value) } else { - let i = attrs.len(); - attrs.push((key, val)); - i + self.0 + .iter() + .rfind(|(k, _)| k.key() == Some(key)) + .map(|(_, v)| v.clone()) } } - /// Returns true if the collection contains no attributes. + /// Returns an iterator that only emits a single key-value pair per unique key, i.e. like they + /// appear in the rendered output. + /// + /// # Examples + /// + /// For "class" elements, values are concatenated: + /// + /// ``` + /// # use jotdown::*; + /// let a: Attributes = "{class=a .b}".try_into().unwrap(); + /// let mut pairs = a.unique_pairs(); + /// assert_eq!(pairs.next(), Some(("class", "a b".into()))); + /// assert_eq!(pairs.next(), None); + /// ``` + /// + /// For other keys, the last set value is used: + /// + /// ``` + /// # use jotdown::*; + /// let a: Attributes = "{id=a key=b #c key=d}".try_into().unwrap(); + /// let mut pairs = a.unique_pairs(); + /// assert_eq!(pairs.next(), Some(("id", "c".into()))); + /// assert_eq!(pairs.next(), Some(("key", "d".into()))); + /// assert_eq!(pairs.next(), None); + /// ``` + /// + /// Comments are ignored: + /// + /// ``` + /// # use jotdown::*; + /// let a: Attributes = "{%cmt% #a}".try_into().unwrap(); + /// let mut pairs = a.unique_pairs(); + /// assert_eq!(pairs.next(), Some(("id", "a".into()))); + /// ``` #[must_use] - pub fn is_empty(&self) -> bool { - self.0.as_ref().map_or(true, |v| v.is_empty()) + pub fn unique_pairs<'a>(&'a self) -> AttributePairsIter<'a, 's> { + AttributePairsIter { + attrs: &self.0, + pos: 0, + } } +} - #[must_use] - pub fn len(&self) -> usize { - self.0.as_ref().map_or(0, |v| v.len()) +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ParseAttributesError { + /// Location in input string where attributes became invalid. + pub pos: usize, +} + +impl<'s> TryFrom<&'s str> for Attributes<'s> { + type Error = ParseAttributesError; + + /// Parse attributes represented in the djot syntax. + /// + /// Note: The [`Attributes`] borrows from the provided [`&str`], it is therefore not compatible + /// with the existing [`std::str::FromStr`] trait. + /// + /// # Examples + /// + /// A single set of attributes can be parsed: + /// + /// ``` + /// # use jotdown::*; + /// let mut a = Attributes::try_from("{.a}").unwrap().into_iter(); + /// assert_eq!(a.next(), Some((AttributeKind::Class, "a".into()))); + /// assert_eq!(a.next(), None); + /// ``` + /// + /// Multiple sets can be parsed if they immediately follow the each other: + /// + /// ``` + /// # use jotdown::*; + /// let mut a = Attributes::try_from("{.a}{.b}").unwrap().into_iter(); + /// assert_eq!(a.next(), Some((AttributeKind::Class, "a".into()))); + /// assert_eq!(a.next(), Some((AttributeKind::Class, "b".into()))); + /// assert_eq!(a.next(), None); + /// ``` + /// + /// When the attributes are invalid, the position where the parsing failed is returned: + /// + /// ``` + /// # use jotdown::*; + /// assert_eq!(Attributes::try_from("{.a $}"), Err(ParseAttributesError { pos: 4 })); + /// ``` + fn try_from(s: &'s str) -> Result { + let mut a = Attributes::new(); + match a.parse(s) { + Ok(()) => Ok(a), + Err(pos) => Err(ParseAttributesError { pos }), + } } +} - /// Returns a reference to the value corresponding to the attribute key. - #[must_use] - pub fn get(&self, key: &str) -> Option<&AttributeValue> { - self.iter().find(|(k, _)| *k == key).map(|(_, v)| v) +impl<'s> From>> for Attributes<'s> { + fn from(v: Vec>) -> Self { + Self(v) } +} - /// Returns a mutable reference to the value corresponding to the attribute key. - pub fn get_mut(&'s mut self, key: &str) -> Option<&mut AttributeValue> { - self.iter_mut().find(|(k, _)| *k == key).map(|(_, v)| v) +impl<'s> From> for Vec> { + fn from(a: Attributes<'s>) -> Self { + a.0 } +} - /// Returns an iterator over references to the attribute keys and values in undefined order. - pub fn iter(&self) -> AttributesIter { - self.into_iter() +impl<'s> std::ops::Deref for Attributes<'s> { + type Target = Vec>; + + fn deref(&self) -> &Self::Target { + &self.0 } +} - /// Returns an iterator over mutable references to the attribute keys and values in undefined order. - pub fn iter_mut<'i>(&'i mut self) -> AttributesIterMut<'i, 's> { - self.into_iter() +impl<'s> std::ops::DerefMut for Attributes<'s> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } } #[cfg(test)] -impl<'s> FromIterator<(&'s str, &'s str)> for Attributes<'s> { - fn from_iter>(iter: I) -> Self { +impl<'s> FromIterator<(AttributeKind<'s>, &'s str)> for Attributes<'s> { + fn from_iter, &'s str)>>(iter: I) -> Self { let attrs = iter .into_iter() .map(|(a, v)| (a, v.into())) .collect::>(); - if attrs.is_empty() { - Attributes::new() - } else { - Attributes(Some(attrs.into())) - } + Attributes(attrs) + } +} + +impl<'s> FromIterator> for Attributes<'s> { + /// Create `Attributes` from an iterator of elements. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let e0 = (AttributeKind::Class, AttributeValue::from("a")); + /// let e1 = (AttributeKind::Id, AttributeValue::from("b")); + /// let a: Attributes = [e0.clone(), e1.clone()].into_iter().collect(); + /// assert_eq!(format!("{:?}", a), "{.a #b}"); + /// let mut elems = a.into_iter(); + /// assert_eq!(elems.next(), Some(e0)); + /// assert_eq!(elems.next(), Some(e1)); + /// ``` + fn from_iter>>(iter: I) -> Self { + Attributes(iter.into_iter().collect()) } } impl<'s> std::fmt::Debug for Attributes<'s> { + /// Formats the attributes using the given formatter. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let a = r#"{#a .b id=c class=d key="val" %comment%}"#; + /// let b = r#"{#a .b id="c" class="d" key="val" %comment%}"#; + /// assert_eq!(format!("{:?}", Attributes::try_from(a).unwrap()), b); + /// ``` fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{{")?; let mut first = true; for (k, v) in self { if !first { - write!(f, ", ")?; + write!(f, " ")?; } first = false; - write!(f, "{}=\"{}\"", k, v.raw)?; + match k { + AttributeKind::Class => write!(f, ".{}", v.raw)?, + AttributeKind::Id => write!(f, "#{}", v.raw)?, + AttributeKind::Pair { key } => write!(f, "{}=\"{}\"", key, v.raw)?, + AttributeKind::Comment => write!(f, "%{}%", v.raw)?, + } } write!(f, "}}") } } -/// Iterator over [Attributes] key-value pairs, in arbitrary order. -pub struct AttributesIntoIter<'s>(std::vec::IntoIter<(&'s str, AttributeValue<'s>)>); - -impl<'s> Iterator for AttributesIntoIter<'s> { - type Item = (&'s str, AttributeValue<'s>); - - fn next(&mut self) -> Option { - self.0.next() - } - - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() - } -} - impl<'s> IntoIterator for Attributes<'s> { - type Item = (&'s str, AttributeValue<'s>); - - type IntoIter = AttributesIntoIter<'s>; - + type Item = AttributeElem<'s>; + + type IntoIter = std::vec::IntoIter>; + + /// Turn into an iterator of attribute elements. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let a = Attributes::try_from("{key1=val1 key2=val2}").unwrap(); + /// let mut elems = a.into_iter(); + /// assert_eq!( + /// elems.next(), + /// Some(( + /// AttributeKind::Pair { key: "key1" }, + /// AttributeValue::from("val1"), + /// )), + /// ); + /// assert_eq!( + /// elems.next(), + /// Some(( + /// AttributeKind::Pair { key: "key2" }, + /// AttributeValue::from("val2"), + /// )), + /// ); + /// assert_eq!(elems.next(), None); + /// ``` fn into_iter(self) -> Self::IntoIter { - AttributesIntoIter(self.0.map_or(vec![].into_iter(), |b| (*b).into_iter())) + self.0.into_iter() } } -/// Iterator over references to [Attributes] key-value pairs, in arbitrary order. -pub struct AttributesIter<'i, 's>(std::slice::Iter<'i, (&'s str, AttributeValue<'s>)>); - -impl<'i, 's> Iterator for AttributesIter<'i, 's> { - type Item = (&'s str, &'i AttributeValue<'s>); - - fn next(&mut self) -> Option { - self.0.next().map(move |(k, v)| (*k, v)) - } - - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() +impl<'i, 's> IntoIterator for &'i Attributes<'s> { + type Item = &'i AttributeElem<'s>; + + type IntoIter = std::slice::Iter<'i, AttributeElem<'s>>; + + /// Create an iterator of borrowed attribute elements. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let a = Attributes::try_from("{key1=val1 key2=val2}").unwrap(); + /// let mut elems = a.iter(); + /// assert_eq!( + /// elems.next(), + /// Some(&( + /// AttributeKind::Pair { key: "key1" }, + /// AttributeValue::from("val1"), + /// )), + /// ); + /// assert_eq!( + /// elems.next(), + /// Some(&( + /// AttributeKind::Pair { key: "key2" }, + /// AttributeValue::from("val2"), + /// )), + /// ); + /// assert_eq!(elems.next(), None); + /// ``` + fn into_iter(self) -> Self::IntoIter { + self.0.iter() } } -impl<'i, 's> IntoIterator for &'i Attributes<'s> { - type Item = (&'s str, &'i AttributeValue<'s>); - - type IntoIter = AttributesIter<'i, 's>; - +impl<'i, 's> IntoIterator for &'i mut Attributes<'s> { + type Item = &'i mut AttributeElem<'s>; + + type IntoIter = std::slice::IterMut<'i, AttributeElem<'s>>; + + /// Create an iterator of mutably borrowed attribute elements. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let mut a = Attributes::try_from("{key1=val1 key2=val2}").unwrap(); + /// let mut elems = a.iter_mut(); + /// assert_eq!( + /// elems.next(), + /// Some(&mut ( + /// AttributeKind::Pair { key: "key1" }, + /// AttributeValue::from("val1"), + /// )), + /// ); + /// assert_eq!( + /// elems.next(), + /// Some(&mut ( + /// AttributeKind::Pair { key: "key2" }, + /// AttributeValue::from("val2"), + /// )), + /// ); + /// assert_eq!(elems.next(), None); + /// ``` fn into_iter(self) -> Self::IntoIter { - let sl = self.0.as_ref().map_or(&[][..], |a| a.as_slice()); - AttributesIter(sl.iter()) + self.0.iter_mut() } } -/// Iterator over mutable references to [Attributes] key-value pairs, in arbitrary order. -pub struct AttributesIterMut<'i, 's>(std::slice::IterMut<'i, (&'s str, AttributeValue<'s>)>); - -impl<'i, 's> Iterator for AttributesIterMut<'i, 's> { - type Item = (&'s str, &'i mut AttributeValue<'s>); +/// Iterator of unique attribute pairs. +/// +/// See [`Attributes::unique_pairs`] for more information. +pub struct AttributePairsIter<'a, 's> { + attrs: &'a [AttributeElem<'s>], + pos: usize, +} +impl<'a: 's, 's> Iterator for AttributePairsIter<'a, 's> { + type Item = (&'s str, AttributeValue<'s>); fn next(&mut self) -> Option { - // this map splits &(&k, v) into (&&k, &v) - self.0.next().map(|(k, v)| (*k, v)) - } + while let Some((key, value)) = self.attrs[self.pos..].first() { + self.pos += 1; + let key = if let Some(k) = key.key() { + k + } else { + continue; // ignore comments + }; - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() - } -} + if self.attrs[..self.pos - 1] + .iter() + .any(|(k, _)| k.key() == Some(key)) + { + continue; // already emitted when this key first encountered + } -impl<'i, 's> IntoIterator for &'i mut Attributes<'s> { - type Item = (&'s str, &'i mut AttributeValue<'s>); + if key == "class" { + let mut value = value.clone(); + for (k, v) in &self.attrs[self.pos..] { + if k.key() == Some("class") { + value.extend(&v.raw); + } + } + return Some((key, value)); + } - type IntoIter = AttributesIterMut<'i, 's>; + if let Some((_, v)) = self.attrs[self.pos..] + .iter() + .rfind(|(k, _)| k.key() == Some(key)) + { + return Some((key, v.clone())); // emit last value when key first encountered + } - fn into_iter(self) -> Self::IntoIter { - let sl = self.0.as_mut().map_or(&mut [][..], |a| a.as_mut()); - AttributesIterMut(sl.iter_mut()) + return Some((key, value.clone())); + } + None } } @@ -364,12 +727,8 @@ impl Validator { /// Attributes parser, take input of one or more consecutive attributes and create an `Attributes` /// object. -/// -/// Input is assumed to contain a valid series of attribute sets, the attributes are added as they -/// are encountered. pub struct Parser<'s> { attrs: Attributes<'s>, - i_prev: usize, state: State, } @@ -377,19 +736,23 @@ impl<'s> Parser<'s> { pub fn new(attrs: Attributes<'s>) -> Self { Self { attrs, - i_prev: usize::MAX, state: State::Start, } } /// Return value indicates the number of bytes parsed if finished. If None, more input is /// required to finish the attributes. - pub fn parse(&mut self, input: &'s str) { + pub fn parse(&mut self, input: &'s str) -> Result<(), usize> { use State::*; let mut pos_prev = 0; for (pos, c) in input.bytes().enumerate() { let state_next = self.state.step(c); + + if matches!(state_next, Invalid) { + return Err(pos); + } + let st = std::mem::replace(&mut self.state, state_next); if st != self.state && !matches!((st, self.state), (ValueEscape, _) | (_, ValueEscape)) @@ -397,14 +760,18 @@ impl<'s> Parser<'s> { let content = &input[pos_prev..pos]; pos_prev = pos; match st { - Class => self.attrs.insert("class", content.into()), - Identifier => self.attrs.insert("id", content.into()), - Key => self.i_prev = self.attrs.insert_pos(content, "".into()), - Value | ValueQuoted | ValueContinued => { - self.attrs.0.as_mut().unwrap()[self.i_prev] + Class => self.attrs.push((AttributeKind::Class, content.into())), + Identifier => self.attrs.push((AttributeKind::Id, content.into())), + Key => self + .attrs + .push((AttributeKind::Pair { key: content }, "".into())), + Value | ValueQuoted | ValueContinued | Comment => { + let last = self.attrs.len() - 1; + self.attrs.0[last] .1 .extend(&content[usize::from(matches!(st, ValueQuoted))..]); } + CommentFirst => self.attrs.push((AttributeKind::Comment, "".into())), _ => {} } }; @@ -415,10 +782,12 @@ impl<'s> Parser<'s> { if input[pos + 1..].starts_with('{') { self.state = Start; } else { - return; + return Ok(()); } } } + + Ok(()) } pub fn finish(self) -> Attributes<'s> { @@ -430,6 +799,7 @@ impl<'s> Parser<'s> { enum State { Start, Whitespace, + CommentFirst, Comment, ClassFirst, Class, @@ -457,14 +827,14 @@ impl State { b'}' => Done, b'.' => ClassFirst, b'#' => IdentifierFirst, - b'%' => Comment, + b'%' => CommentFirst, c if is_name(c) => Key, c if c.is_ascii_whitespace() => Whitespace, _ => Invalid, }, - Comment if c == b'%' => Whitespace, - Comment if c == b'}' => Done, - Comment => Comment, + CommentFirst | Comment if c == b'%' => Whitespace, + CommentFirst | Comment if c == b'}' => Done, + CommentFirst | Comment => Comment, ClassFirst if is_name(c) => Class, ClassFirst => Invalid, IdentifierFirst if is_name(c) => Identifier, @@ -495,44 +865,49 @@ pub fn is_name(c: u8) -> bool { #[cfg(test)] mod test { + use super::AttributeKind::*; use super::*; macro_rules! test_attr { - ($src:expr $(,$($av:expr),* $(,)?)?) => { + ($src:expr, [$($exp:expr),* $(,)?], [$($exp_uniq:expr),* $(,)?] $(,)?) => { #[allow(unused)] - let mut attr = Attributes::new(); - attr.parse($src); - let actual = attr.iter().collect::>(); - let expected = &[$($($av),*,)?]; - for i in 0..actual.len() { - let actual_val = format!("{}", actual[i].1); - assert_eq!((actual[i].0, actual_val.as_str()), expected[i], "\n\n{}\n\n", $src); - } + let mut attr = Attributes::try_from($src).unwrap(); + + let actual = attr.iter().map(|(k, v)| (k.clone(), v.to_string())).collect::>(); + let expected = &[$($exp),*].map(|(k, v): (_, &str)| (k, v.to_string())); + assert_eq!(actual, expected, "\n\n{}\n\n", $src); + + let actual = attr.unique_pairs().map(|(k, v)| (k, v.to_string())).collect::>(); + let expected = &[$($exp_uniq),*].map(|(k, v): (_, &str)| (k, v.to_string())); + assert_eq!(actual, expected, "\n\n{}\n\n", $src); }; } #[test] fn empty() { - test_attr!("{}"); + test_attr!("{}", [], []); } #[test] fn class_id() { test_attr!( "{.some_class #some_id}", - ("class", "some_class"), - ("id", "some_id"), + [(Class, "some_class"), (Id, "some_id")], + [("class", "some_class"), ("id", "some_id")], ); - test_attr!("{.a .b}", ("class", "a b")); - test_attr!("{#a #b}", ("id", "b")); + test_attr!("{.a .b}", [(Class, "a"), (Class, "b")], [("class", "a b")]); + test_attr!("{#a #b}", [(Id, "a"), (Id, "b")], [("id", "b")]); } #[test] fn value_unquoted() { test_attr!( "{attr0=val0 attr1=val1}", - ("attr0", "val0"), - ("attr1", "val1"), + [ + (Pair { key: "attr0" }, "val0"), + (Pair { key: "attr1" }, "val1"), + ], + [("attr0", "val0"), ("attr1", "val1")], ); } @@ -540,32 +915,46 @@ mod test { fn value_quoted() { test_attr!( r#"{attr0="val0" attr1="val1"}"#, - ("attr0", "val0"), - ("attr1", "val1"), + [ + (Pair { key: "attr0" }, "val0"), + (Pair { key: "attr1" }, "val1"), + ], + [("attr0", "val0"), ("attr1", "val1")], ); test_attr!( r#"{#id .class style="color:red"}"#, - ("id", "id"), - ("class", "class"), - ("style", "color:red") + [ + (Id, "id"), + (Class, "class"), + (Pair { key: "style" }, "color:red"), + ], + [("id", "id"), ("class", "class"), ("style", "color:red")] ); } #[test] fn value_newline() { - test_attr!("{attr0=\"abc\ndef\"}", ("attr0", "abc def")); + test_attr!( + "{attr0=\"abc\ndef\"}", + [(Pair { key: "attr0" }, "abc def")], + [("attr0", "abc def")] + ); } #[test] fn comment() { - test_attr!("{%}"); - test_attr!("{%%}"); - test_attr!("{ % abc % }"); - test_attr!("{ .some_class % #some_id }", ("class", "some_class")); + test_attr!("{%}", [(Comment, "")], []); + test_attr!("{%%}", [(Comment, "")], []); + test_attr!("{ % abc % }", [(Comment, " abc ")], []); + test_attr!( + "{ .some_class % #some_id }", + [(Class, "some_class"), (Comment, " #some_id ")], + [("class", "some_class")] + ); test_attr!( "{ .some_class % abc % #some_id}", - ("class", "some_class"), - ("id", "some_id"), + [(Class, "some_class"), (Comment, " abc "), (Id, "some_id")], + [("class", "some_class"), ("id", "some_id")], ); } @@ -573,33 +962,46 @@ mod test { fn escape() { test_attr!( r#"{attr="with escaped \~ char"}"#, - ("attr", "with escaped ~ char") + [(Pair { key: "attr" }, "with escaped ~ char")], + [("attr", "with escaped ~ char")] ); test_attr!( r#"{key="quotes \" should be escaped"}"#, - ("key", r#"quotes " should be escaped"#) + [(Pair { key: "key" }, r#"quotes " should be escaped"#)], + [("key", r#"quotes " should be escaped"#)] ); } #[test] fn escape_backslash() { - test_attr!(r#"{attr="with\\backslash"}"#, ("attr", r"with\backslash")); + test_attr!( + r#"{attr="with\\backslash"}"#, + [(Pair { key: "attr" }, r"with\backslash")], + [("attr", r"with\backslash")] + ); test_attr!( r#"{attr="with many backslashes\\\\"}"#, - ("attr", r"with many backslashes\\") + [(Pair { key: "attr" }, r"with many backslashes\\")], + [("attr", r"with many backslashes\\")] ); test_attr!( r#"{attr="\\escaped backslash at start"}"#, - ("attr", r"\escaped backslash at start") + [(Pair { key: "attr" }, r"\escaped backslash at start")], + [("attr", r"\escaped backslash at start")] ); } #[test] fn only_escape_punctuation() { - test_attr!(r#"{attr="do not \escape"}"#, ("attr", r"do not \escape")); + test_attr!( + r#"{attr="do not \escape"}"#, + [(Pair { key: "attr" }, r"do not \escape")], + [("attr", r"do not \escape")] + ); test_attr!( r#"{attr="\backslash at the beginning"}"#, - ("attr", r"\backslash at the beginning") + [(Pair { key: "attr" }, r"\backslash at the beginning")], + [("attr", r"\backslash at the beginning")] ); } @@ -648,52 +1050,68 @@ mod test { assert_eq!(super::valid("{.abc.}"), 0); } - fn make_attrs<'a>(v: Vec<(&'a str, &'a str)>) -> Attributes<'a> { - v.into_iter().collect() - } - #[test] - fn can_iter() { - let attrs = make_attrs(vec![("key1", "val1"), ("key2", "val2")]); - let as_vec = attrs.iter().collect::>(); + fn get_value_named() { + assert_eq!( + Attributes::try_from("{x=a}").unwrap().get_value("x"), + Some("a".into()), + ); assert_eq!( - as_vec, - vec![ - ("key1", &AttributeValue::from("val1")), - ("key2", &AttributeValue::from("val2")), - ] + Attributes::try_from("{x=a x=b}").unwrap().get_value("x"), + Some("b".into()), ); } #[test] - fn can_iter_mut() { - let mut attrs = make_attrs(vec![("key1", "val1"), ("key2", "val2")]); - let as_vec = attrs.iter_mut().collect::>(); + fn get_value_id() { assert_eq!( - as_vec, - vec![ - ("key1", &mut AttributeValue::from("val1")), - ("key2", &mut AttributeValue::from("val2")), - ] + Attributes::try_from("{#a}").unwrap().get_value("id"), + Some("a".into()), + ); + assert_eq!( + Attributes::try_from("{#a #b}").unwrap().get_value("id"), + Some("b".into()), + ); + assert_eq!( + Attributes::try_from("{#a id=b}").unwrap().get_value("id"), + Some("b".into()), + ); + assert_eq!( + Attributes::try_from("{id=a #b}").unwrap().get_value("id"), + Some("b".into()), ); } #[test] - fn iter_after_iter_mut() { - let mut attrs: Attributes = make_attrs(vec![("key1", "val1"), ("key2", "val2")]); - - for (attr, value) in &mut attrs { - if attr == "key2" { - *value = "new_val".into(); - } - } - + fn get_value_class() { assert_eq!( - attrs.iter().collect::>(), - vec![ - ("key1", &AttributeValue::from("val1")), - ("key2", &AttributeValue::from("new_val")), - ] + Attributes::try_from("{.a #a .b #b .c}") + .unwrap() + .get_value("class"), + Some("a b c".into()), ); + assert_eq!( + Attributes::try_from("{#a}").unwrap().get_value("class"), + None, + ); + assert_eq!( + Attributes::try_from("{.a}").unwrap().get_value("class"), + Some("a".into()), + ); + assert_eq!( + Attributes::try_from("{.a #a class=b #b .c}") + .unwrap() + .get_value("class"), + Some("a b c".into()), + ); + } + + #[test] + fn from_to_vec() { + let v0: Vec<(AttributeKind, AttributeValue)> = vec![(Class, "a".into()), (Id, "b".into())]; + let a: Attributes = v0.clone().into(); + assert_eq!(format!("{:?}", a), "{.a #b}"); + let v1: Vec<(AttributeKind, AttributeValue)> = a.into(); + assert_eq!(v0, v1); } } diff --git a/src/html.rs b/src/html.rs index 83b96e42..d2e454df 100644 --- a/src/html.rs +++ b/src/html.rs @@ -208,9 +208,19 @@ impl<'s> Writer<'s> { Container::LinkDefinition { .. } => return Ok(()), } - for (a, v) in attrs.into_iter().filter(|(a, _)| *a != "class") { + let mut id_written = false; + let mut class_written = false; + for (a, v) in attrs.unique_pairs() { write!(out, r#" {}=""#, a)?; v.parts().try_for_each(|part| write_attr(part, &mut out))?; + match a { + "class" => { + class_written = true; + write_class(c, true, &mut out)?; + } + "id" => id_written = true, + _ => {} + } out.write_char('"')?; } @@ -221,59 +231,25 @@ impl<'s> Writer<'s> { } | Container::Section { id } = &c { - if !attrs.into_iter().any(|(a, _)| a == "id") { + if !id_written { out.write_str(r#" id=""#)?; write_attr(id, &mut out)?; out.write_char('"')?; } - } - - if attrs.into_iter().any(|(a, _)| a == "class") + } else if (matches!(c, Container::Div { class } if !class.is_empty()) || matches!( c, - Container::Div { class } if !class.is_empty()) - || matches!(c, |Container::Math { .. }| Container::List { - kind: ListKind::Task, - .. - } | Container::TaskListItem { .. }) + Container::Math { .. } + | Container::List { + kind: ListKind::Task, + .. + } + | Container::TaskListItem { .. } + )) + && !class_written { out.write_str(r#" class=""#)?; - let mut first_written = false; - if let Some(cls) = match c { - Container::List { - kind: ListKind::Task, - .. - } => Some("task-list"), - Container::TaskListItem { checked: false } => Some("unchecked"), - Container::TaskListItem { checked: true } => Some("checked"), - Container::Math { display: false } => Some("math inline"), - Container::Math { display: true } => Some("math display"), - _ => None, - } { - first_written = true; - out.write_str(cls)?; - } - for cls in attrs - .into_iter() - .filter(|(a, _)| a == &"class") - .map(|(_, cls)| cls) - { - if first_written { - out.write_char(' ')?; - } - first_written = true; - cls.parts() - .try_for_each(|part| write_attr(part, &mut out))?; - } - // div class goes after classes from attrs - if let Container::Div { class } = c { - if !class.is_empty() { - if first_written { - out.write_char(' ')?; - } - out.write_str(class)?; - } - } + write_class(c, false, &mut out)?; out.write_char('"')?; } @@ -409,13 +385,13 @@ impl<'s> Writer<'s> { Event::NonBreakingSpace => out.write_str(" ")?, Event::Hardbreak => out.write_str("
\n")?, Event::Softbreak => out.write_char('\n')?, - Event::Escape | Event::Blankline => {} + Event::Escape | Event::Blankline | Event::Attributes(..) => {} Event::ThematicBreak(attrs) => { if self.not_first_line { out.write_char('\n')?; } out.write_str(" Writer<'s> { } } +fn write_class(c: &Container, mut first_written: bool, out: &mut W) -> std::fmt::Result +where + W: std::fmt::Write, +{ + if let Some(cls) = match c { + Container::List { + kind: ListKind::Task, + .. + } => Some("task-list"), + Container::TaskListItem { checked: false } => Some("unchecked"), + Container::TaskListItem { checked: true } => Some("checked"), + Container::Math { display: false } => Some("math inline"), + Container::Math { display: true } => Some("math display"), + _ => None, + } { + first_written = true; + out.write_str(cls)?; + } + if let Container::Div { class } = c { + if !class.is_empty() { + if first_written { + out.write_char(' ')?; + } + out.write_str(class)?; + } + } + Ok(()) +} + fn write_text(s: &str, out: W) -> std::fmt::Result where W: std::fmt::Write, diff --git a/src/inline.rs b/src/inline.rs index 03270702..58a3f52a 100644 --- a/src/inline.rs +++ b/src/inline.rs @@ -61,6 +61,7 @@ pub enum EventKind<'s> { Exit(Container<'s>), Atom(Atom<'s>), Str, + Empty, // dummy to hold attributes Attributes { container: bool, attrs: AttributesIndex, @@ -516,7 +517,9 @@ impl<'s> Parser<'s> { .chain(self.input.ahead.iter().take(state.valid_lines).cloned()) { let line = line.start..usize::min(state.end_attr, line.end); - parser.parse(&self.input.src[line]); + parser + .parse(&self.input.src[line]) + .expect("should be valid"); } parser.finish() }; @@ -555,6 +558,8 @@ impl<'s> Parser<'s> { } AttributesElementType::Word => { self.events.push_back(attr_event); + // push for now, pop later if attrs attached to word + self.push(EventKind::Empty); } } } @@ -817,13 +822,16 @@ impl<'s> Parser<'s> { if self.input.peek().map_or(false, |t| { matches!(t.kind, lex::Kind::Open(Delimiter::Brace)) }) { - self.ahead_attributes( + let elem_ty = if matches!(opener, Opener::DoubleQuoted | Opener::SingleQuoted) { + // quote delimiters will turn into atoms instead of containers, so cannot + // place attributes on the container start + AttributesElementType::Word + } else { AttributesElementType::Container { e_placeholder: e_attr, - }, - false, - ) - .or(Some(Continue)) + } + }; + self.ahead_attributes(elem_ty, false).or(Some(Continue)) } else { closed } @@ -983,18 +991,22 @@ impl<'s> Parser<'s> { } } else { let attr = self.events.pop_front().unwrap(); - self.events.push_front(Event { - kind: EventKind::Exit(Span), - span: attr.span.clone(), - }); - self.events.push_front(Event { - kind: EventKind::Str, - span: span_str.clone(), - }); - self.events.push_front(Event { - kind: EventKind::Enter(Span), - span: span_str.start..span_str.start, - }); + if !span_str.is_empty() { + let empty = self.events.pop_front(); + debug_assert_eq!(empty.unwrap().kind, EventKind::Empty); + self.events.push_front(Event { + kind: EventKind::Exit(Span), + span: attr.span.clone(), + }); + self.events.push_front(Event { + kind: EventKind::Str, + span: span_str.clone(), + }); + self.events.push_front(Event { + kind: EventKind::Enter(Span), + span: span_str.start..span_str.start, + }); + } attr } } @@ -1182,12 +1194,20 @@ impl<'s> Iterator for Parser<'s> { } self.events.pop_front().and_then(|e| match e.kind { - EventKind::Str if e.span.is_empty() => self.next(), + EventKind::Str + if e.span.is_empty() + && !matches!( + self.events.front().map(|ev| &ev.kind), + Some(EventKind::Attributes { + container: false, + .. + }) + ) => + { + self.next() + } EventKind::Str => Some(self.merge_str_events(e.span)), - EventKind::Placeholder - | EventKind::Attributes { - container: false, .. - } => self.next(), + EventKind::Placeholder => self.next(), _ => Some(e), }) } @@ -1569,7 +1589,19 @@ mod test { (Str, "abc"), (Exit(Span), "]{.def}"), ); - test_parse!("not a [span] {#id}.", (Str, "not a [span] "), (Str, ".")); + test_parse!( + "not a [span] {#id}.", + (Str, "not a [span] "), + ( + Attributes { + container: false, + attrs: 0, + }, + "{#id}", + ), + (Empty, "{#id}"), + (Str, "."), + ); } #[test] @@ -1731,6 +1763,13 @@ mod test { ); test_parse!( "_abc def_{ % comment % } ghi", + ( + Attributes { + container: true, + attrs: 0 + }, + "{ % comment % }" + ), (Enter(Emphasis), "_"), (Str, "abc def"), (Exit(Emphasis), "_{ % comment % }"), @@ -1753,6 +1792,14 @@ mod test { (Str, "abc def"), (Exit(Emphasis), "_{.a}{.b}{.c}"), (Str, " "), + ( + Attributes { + container: false, + attrs: 1, + }, + "{.d}", + ), + (Empty, "{.d}"), ); } @@ -1807,16 +1854,90 @@ mod test { #[test] fn attr_whitespace() { - test_parse!("word {%comment%}", (Str, "word ")); - test_parse!("word {%comment%} word", (Str, "word "), (Str, " word")); - test_parse!("word {a=b}", (Str, "word ")); - test_parse!("word {.d}", (Str, "word ")); + test_parse!( + "word {%comment%}", + (Str, "word "), + ( + Attributes { + container: false, + attrs: 0, + }, + "{%comment%}", + ), + (Empty, "{%comment%}"), + ); + test_parse!( + "word {%comment%} word", + (Str, "word "), + ( + Attributes { + container: false, + attrs: 0 + }, + "{%comment%}", + ), + (Empty, "{%comment%}"), + (Str, " word"), + ); + test_parse!( + "word {a=b}", + (Str, "word "), + ( + Attributes { + container: false, + attrs: 0, + }, + "{a=b}", + ), + (Empty, "{a=b}"), + ); + test_parse!( + " {a=b}", + (Str, " "), + ( + Attributes { + container: false, + attrs: 0, + }, + "{a=b}", + ), + (Empty, "{a=b}"), + ); + } + + #[test] + fn attr_start() { + test_parse!( + "{a=b} word", + ( + Attributes { + container: false, + attrs: 0, + }, + "{a=b}", + ), + (Empty, "{a=b}"), + (Str, " word"), + ); } #[test] fn attr_empty() { test_parse!("word{}", (Str, "word")); - test_parse!("word{ % comment % } trail", (Str, "word"), (Str, " trail")); + test_parse!( + "word{ % comment % } trail", + ( + Attributes { + container: false, + attrs: 0 + }, + "{ % comment % }" + ), + (Enter(Span), ""), + (Str, "word"), + (Exit(Span), "{ % comment % }"), + (Str, " trail") + ); } #[test] @@ -1860,4 +1981,34 @@ mod test { (Str, " "), ); } + + #[test] + fn quote_attr() { + test_parse!( + "'a'{.b}", + ( + Atom(Quote { + ty: QuoteType::Single, + left: true + }), + "'" + ), + (Str, "a"), + ( + Atom(Quote { + ty: QuoteType::Single, + left: false + }), + "'" + ), + ( + Attributes { + container: false, + attrs: 0, + }, + "{.b}", + ), + (Empty, "{.b}"), + ); + } } diff --git a/src/lib.rs b/src/lib.rs index 0abbb24a..227ef49a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,8 +57,7 @@ mod inline; mod lex; pub use attr::{ - AttributeValue, AttributeValueParts, Attributes, AttributesIntoIter, AttributesIter, - AttributesIterMut, + AttributeKind, AttributeValue, AttributeValueParts, Attributes, ParseAttributesError, }; type CowStr<'s> = std::borrow::Cow<'s, str>; @@ -206,41 +205,476 @@ impl<'s> AsRef> for &Event<'s> { #[derive(Debug, Clone, PartialEq, Eq)] pub enum Event<'s> { /// Start of a container. + /// + /// Always paired with a matching [`Event::End`]. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "{#a}\n", + /// "[word]{#b}\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::Paragraph, + /// [(AttributeKind::Id, "a".into())].into_iter().collect(), + /// ), + /// Event::Start( + /// Container::Span, + /// [(AttributeKind::Id, "b".into())].into_iter().collect(), + /// ), + /// Event::Str("word".into()), + /// Event::End(Container::Span), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

word

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Start(Container<'s>, Attributes<'s>), /// End of a container. + /// + /// Always paired with a matching [`Event::Start`]. End(Container<'s>), /// A string object, text only. + /// + /// The strings from the parser will always be borrowed, but users may replace them with owned + /// variants before rendering. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "str"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("str".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

str

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Str(CowStr<'s>), /// A footnote reference. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "txt[^nb]."; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("txt".into()), + /// Event::FootnoteReference("nb"), + /// Event::Str(".".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = concat!( + /// "

txt1.

\n", + /// "
\n", + /// "
\n", + /// "
    \n", + /// "
  1. \n", + /// "

    ↩\u{fe0e}

    \n", + /// "
  2. \n", + /// "
\n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` FootnoteReference(&'s str), /// A symbol, by default rendered literally but may be treated specially. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "a :sym:"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("a ".into()), + /// Event::Symbol("sym".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

a :sym:

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Symbol(CowStr<'s>), /// Left single quotation mark. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = r#"'quote'"#; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::LeftSingleQuote, + /// Event::Str("quote".into()), + /// Event::RightSingleQuote, + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

‘quote’

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` LeftSingleQuote, - /// Right double quotation mark. + /// Right single quotation mark. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = r#"'}Tis Socrates'"#; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::RightSingleQuote, + /// Event::Str("Tis Socrates".into()), + /// Event::RightSingleQuote, + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

’Tis Socrates’

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` RightSingleQuote, /// Left single quotation mark. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = r#""Hello," he said"#; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::LeftDoubleQuote, + /// Event::Str("Hello,".into()), + /// Event::RightDoubleQuote, + /// Event::Str(" he said".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

“Hello,” he said

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` LeftDoubleQuote, /// Right double quotation mark. RightDoubleQuote, /// A horizontal ellipsis, i.e. a set of three periods. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "yes..."; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("yes".into()), + /// Event::Ellipsis, + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

yes…

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Ellipsis, /// An en dash. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "57--33"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("57".into()), + /// Event::EnDash, + /// Event::Str("33".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

57–33

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` EnDash, /// An em dash. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "oxen---and"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("oxen".into()), + /// Event::EmDash, + /// Event::Str("and".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

oxen—and

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` EmDash, /// A space that must not break a line. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "no\\ break"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("no".into()), + /// Event::Escape, + /// Event::NonBreakingSpace, + /// Event::Str("break".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

no break

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` NonBreakingSpace, /// A newline that may or may not break a line in the output. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "soft\n", + /// "break\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("soft".into()), + /// Event::Softbreak, + /// Event::Str("break".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = concat!( + /// "

soft\n", + /// "break

\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Softbreak, /// A newline that must break a line in the output. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "hard\\\n", + /// "break\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("hard".into()), + /// Event::Escape, + /// Event::Hardbreak, + /// Event::Str("break".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = concat!( + /// "

hard
\n", + /// "break

\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Hardbreak, /// An escape character, not visible in output. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "\\*a\\*"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Escape, + /// Event::Str("*a".into()), + /// Event::Escape, + /// Event::Str("*".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

*a*

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Escape, /// A blank line, not visible in output. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "para0\n", + /// "\n", + /// "para1\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("para0".into()), + /// Event::End(Container::Paragraph), + /// Event::Blankline, + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("para1".into()), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = concat!( + /// "

para0

\n", + /// "

para1

\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Blankline, /// A thematic break, typically a horizontal rule. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "para0\n", + /// "\n", + /// " * * * *\n", + /// "para1\n", + /// "\n", + /// "{.c}\n", + /// "----\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("para0".into()), + /// Event::End(Container::Paragraph), + /// Event::Blankline, + /// Event::ThematicBreak(Attributes::new()), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("para1".into()), + /// Event::End(Container::Paragraph), + /// Event::Blankline, + /// Event::ThematicBreak( + /// [(AttributeKind::Class, "c".into())] + /// .into_iter() + /// .collect(), + /// ), + /// ], + /// ); + /// let html = concat!( + /// "

para0

\n", + /// "
\n", + /// "

para1

\n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` ThematicBreak(Attributes<'s>), + /// Dangling attributes not attached to anything. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "{#a}\n", + /// "\n", + /// "inline {#b}\n", + /// "\n", + /// "{#c}\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Attributes( + /// [(AttributeKind::Id, "a".into())] + /// .into_iter() + /// .collect(), + /// ), + /// Event::Blankline, + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("inline ".into()), + /// Event::Attributes( + /// [(AttributeKind::Id, "b".into())] + /// .into_iter() + /// .collect(), + /// ), + /// Event::End(Container::Paragraph), + /// Event::Blankline, + /// Event::Attributes( + /// [(AttributeKind::Id, "c".into())] + /// .into_iter() + /// .collect(), + /// ), + /// ], + /// ); + /// let html = concat!( + /// "\n", + /// "

inline

\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` + Attributes(Attributes<'s>), } /// A container that may contain other elements. @@ -253,30 +687,500 @@ pub enum Event<'s> { #[derive(Debug, Clone, PartialEq, Eq)] pub enum Container<'s> { /// A blockquote element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "> a\n", + /// "> b\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Blockquote, Attributes::new()), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("a".into()), + /// Event::Softbreak, + /// Event::Str("b".into()), + /// Event::End(Container::Paragraph), + /// Event::End(Container::Blockquote), + /// ], + /// ); + /// let html = concat!( + /// "
\n", + /// "

a\n", + /// "b

\n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Blockquote, /// A list. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "- a\n", + /// "\n", + /// "- b\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::List { + /// kind: ListKind::Unordered, + /// tight: false, + /// }, + /// Attributes::new(), + /// ), + /// Event::Start(Container::ListItem, Attributes::new()), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("a".into()), + /// Event::End(Container::Paragraph), + /// Event::Blankline, + /// Event::End(Container::ListItem), + /// Event::Start(Container::ListItem, Attributes::new()), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("b".into()), + /// Event::End(Container::Paragraph), + /// Event::End(Container::ListItem), + /// Event::End(Container::List { + /// kind: ListKind::Unordered, + /// tight: false + /// }), + /// ], + /// ); + /// let html = concat!( + /// "
    \n", + /// "
  • \n", + /// "

    a

    \n", + /// "
  • \n", + /// "
  • \n", + /// "

    b

    \n", + /// "
  • \n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` List { kind: ListKind, tight: bool }, /// An item of a list + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "- a"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::List { kind: ListKind::Unordered, tight: true }, + /// Attributes::new(), + /// ), + /// Event::Start(Container::ListItem, Attributes::new()), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("a".into()), + /// Event::End(Container::Paragraph), + /// Event::End(Container::ListItem), + /// Event::End(Container::List { + /// kind: ListKind::Unordered, + /// tight: true, + /// }), + /// ], + /// ); + /// let html = concat!( + /// "
    \n", + /// "
  • \n", + /// "a\n", + /// "
  • \n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` ListItem, /// An item of a task list, either checked or unchecked. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "- [x] a"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::List { kind: ListKind::Task, tight: true }, + /// Attributes::new(), + /// ), + /// Event::Start( + /// Container::TaskListItem { checked: true }, + /// Attributes::new(), + /// ), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("a".into()), + /// Event::End(Container::Paragraph), + /// Event::End(Container::TaskListItem { checked: true }), + /// Event::End(Container::List { + /// kind: ListKind::Task, + /// tight: true, + /// }), + /// ], + /// ); + /// let html = concat!( + /// "
    \n", + /// "
  • \n", + /// "a\n", + /// "
  • \n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` TaskListItem { checked: bool }, - /// A description list element. + /// A description list. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// ": orange\n", + /// "\n", + /// " citrus fruit\n", + /// ": apple\n", + /// "\n", + /// " malus fruit\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::DescriptionList, Attributes::new()), + /// Event::Start(Container::DescriptionTerm, Attributes::new()), + /// Event::Str("orange".into()), + /// Event::End(Container::DescriptionTerm), + /// Event::Blankline, + /// Event::Start(Container::DescriptionDetails, Attributes::new()), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("citrus fruit".into()), + /// Event::End(Container::Paragraph), + /// Event::End(Container::DescriptionDetails), + /// Event::Start(Container::DescriptionTerm, Attributes::new()), + /// Event::Str("apple".into()), + /// Event::End(Container::DescriptionTerm), + /// Event::Blankline, + /// Event::Start(Container::DescriptionDetails, Attributes::new()), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("malus fruit".into()), + /// Event::End(Container::Paragraph), + /// Event::End(Container::DescriptionDetails), + /// Event::End(Container::DescriptionList), + /// ], + /// ); + /// let html = concat!( + /// "
\n", + /// "
orange
\n", + /// "
\n", + /// "

citrus fruit

\n", + /// "
\n", + /// "
apple
\n", + /// "
\n", + /// "

malus fruit

\n", + /// "
\n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` DescriptionList, /// Details describing a term within a description list. DescriptionDetails, /// A footnote definition. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "txt[^nb]\n", + /// "\n", + /// "[^nb]: actually..\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("txt".into()), + /// Event::FootnoteReference("nb".into()), + /// Event::End(Container::Paragraph), + /// Event::Blankline, + /// Event::Start( + /// Container::Footnote { label: "nb" }, + /// Attributes::new(), + /// ), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("actually..".into()), + /// Event::End(Container::Paragraph), + /// Event::End(Container::Footnote { label: "nb" }), + /// ], + /// ); + /// let html = concat!( + /// "

txt1

\n", + /// "
\n", + /// "
\n", + /// "
    \n", + /// "
  1. \n", + /// "

    actually..↩\u{fe0e}

    \n", + /// "
  2. \n", + /// "
\n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Footnote { label: &'s str }, /// A table element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "| a | b |\n", + /// "|---|--:|\n", + /// "| 1 | 2 |\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Table, Attributes::new()), + /// Event::Start( + /// Container::TableRow { head: true }, + /// Attributes::new(), + /// ), + /// Event::Start( + /// Container::TableCell { + /// alignment: Alignment::Unspecified, + /// head: true + /// }, + /// Attributes::new(), + /// ), + /// Event::Str("a".into()), + /// Event::End(Container::TableCell { + /// alignment: Alignment::Unspecified, + /// head: true, + /// }), + /// Event::Start( + /// Container::TableCell { + /// alignment: Alignment::Right, + /// head: true, + /// }, + /// Attributes::new(), + /// ), + /// Event::Str("b".into()), + /// Event::End(Container::TableCell { + /// alignment: Alignment::Right, + /// head: true, + /// }), + /// Event::End(Container::TableRow { head: true } ), + /// Event::Start( + /// Container::TableRow { head: false }, + /// Attributes::new(), + /// ), + /// Event::Start( + /// Container::TableCell { + /// alignment: Alignment::Unspecified, + /// head: false + /// }, + /// Attributes::new(), + /// ), + /// Event::Str("1".into()), + /// Event::End(Container::TableCell { + /// alignment: Alignment::Unspecified, + /// head: false, + /// }), + /// Event::Start( + /// Container::TableCell { + /// alignment: Alignment::Right, + /// head: false, + /// }, + /// Attributes::new(), + /// ), + /// Event::Str("2".into()), + /// Event::End(Container::TableCell { + /// alignment: Alignment::Right, + /// head: false, + /// }), + /// Event::End(Container::TableRow { head: false } ), + /// Event::End(Container::Table), + /// ], + /// ); + /// let html = concat!( + /// "\n", + /// "\n", + /// "\n", + /// "\n", + /// "\n", + /// "\n", + /// "\n", + /// "\n", + /// "\n", + /// "
ab
12
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Table, /// A row element of a table. TableRow { head: bool }, /// A section belonging to a top level heading. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "# outer\n", + /// "\n", + /// "## inner\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::Section { id: "outer".into() }, + /// Attributes::new(), + /// ), + /// Event::Start( + /// Container::Heading { + /// level: 1, + /// has_section: true, + /// id: "outer".into(), + /// }, + /// Attributes::new(), + /// ), + /// Event::Str("outer".into()), + /// Event::End(Container::Heading { + /// level: 1, + /// has_section: true, + /// id: "outer".into(), + /// }), + /// Event::Blankline, + /// Event::Start( + /// Container::Section { id: "inner".into() }, + /// Attributes::new(), + /// ), + /// Event::Start( + /// Container::Heading { + /// level: 2, + /// has_section: true, + /// id: "inner".into(), + /// }, + /// Attributes::new(), + /// ), + /// Event::Str("inner".into()), + /// Event::End(Container::Heading { + /// level: 2, + /// has_section: true, + /// id: "inner".into(), + /// }), + /// Event::End(Container::Section { id: "inner".into() }), + /// Event::End(Container::Section { id: "outer".into() }), + /// ], + /// ); + /// let html = concat!( + /// "
\n", + /// "

outer

\n", + /// "
\n", + /// "

inner

\n", + /// "
\n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Section { id: CowStr<'s> }, /// A block-level divider element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "::: note\n", + /// "this is a note\n", + /// ":::\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::Div { class: "note" }, + /// Attributes::new(), + /// ), + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("this is a note".into()), + /// Event::End(Container::Paragraph), + /// Event::End(Container::Div { class: "note" }), + /// ], + /// ); + /// let html = concat!( + /// "
\n", + /// "

this is a note

\n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Div { class: &'s str }, /// A paragraph. Paragraph, /// A heading. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "# heading"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::Section { id: "heading".into() }, + /// Attributes::new(), + /// ), + /// Event::Start( + /// Container::Heading { + /// level: 1, + /// has_section: true, + /// id: "heading".into(), + /// }, + /// Attributes::new(), + /// ), + /// Event::Str("heading".into()), + /// Event::End(Container::Heading { + /// level: 1, + /// has_section: true, + /// id: "heading".into(), + /// }), + /// Event::End(Container::Section { id: "heading".into() }), + /// ], + /// ); + /// let html = concat!( + /// "
\n", + /// "

heading

\n", + /// "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Heading { level: u16, has_section: bool, @@ -285,41 +1189,585 @@ pub enum Container<'s> { /// A cell element of row within a table. TableCell { alignment: Alignment, head: bool }, /// A caption within a table. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "|a|\n", + /// "^ caption\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Table, Attributes::new()), + /// Event::Start(Container::Caption, Attributes::new()), + /// Event::Str("caption".into()), + /// Event::End(Container::Caption), + /// Event::Start( + /// Container::TableRow { head: false }, + /// Attributes::new(), + /// ), + /// Event::Start( + /// Container::TableCell { + /// alignment: Alignment::Unspecified, + /// head: false + /// }, + /// Attributes::new(), + /// ), + /// Event::Str("a".into()), + /// Event::End(Container::TableCell { + /// alignment: Alignment::Unspecified, + /// head: false, + /// }), + /// Event::End(Container::TableRow { head: false } ), + /// Event::End(Container::Table), + /// ], + /// ); + /// let html = concat!( + /// "\n", + /// "\n", + /// "\n", + /// "\n", + /// "\n", + /// "
caption
a
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Caption, /// A term within a description list. DescriptionTerm, /// A link definition. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "[label]: url"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::LinkDefinition { label: "label" }, + /// Attributes::new(), + /// ), + /// Event::Str("url".into()), + /// Event::End(Container::LinkDefinition { label: "label" }), + /// ], + /// ); + /// let html = "\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` LinkDefinition { label: &'s str }, /// A block with raw markup for a specific output format. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "```=html\n", + /// "x\n", + /// "```\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::RawBlock { format: "html" }, + /// Attributes::new(), + /// ), + /// Event::Str("x".into()), + /// Event::End(Container::RawBlock { format: "html" }), + /// ], + /// ); + /// let html = "x\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` RawBlock { format: &'s str }, /// A block with code in a specific language. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "```html\n", + /// "x\n", + /// "```\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start( + /// Container::CodeBlock { language: "html" }, + /// Attributes::new(), + /// ), + /// Event::Str("x\n".into()), + /// Event::End(Container::CodeBlock { language: "html" }), + /// ], + /// ); + /// let html = concat!( + /// "
<tag>x</tag>\n",
+    ///     "
\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` CodeBlock { language: &'s str }, /// An inline divider element. + /// + /// # Examples + /// + /// Can be used to add attributes: + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "word{#a}\n", + /// "[two words]{#b}\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start( + /// Container::Span, + /// [(AttributeKind::Id, "a".into())].into_iter().collect(), + /// ), + /// Event::Str("word".into()), + /// Event::End(Container::Span), + /// Event::Softbreak, + /// Event::Start( + /// Container::Span, + /// [(AttributeKind::Id, "b".into())].into_iter().collect(), + /// ), + /// Event::Str("two words".into()), + /// Event::End(Container::Span), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = concat!( + /// "

word\n", + /// "two words

\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Span, /// An inline link, the first field is either a destination URL or an unresolved tag. + /// + /// # Examples + /// + /// URLs or email addresses can be enclosed with angled brackets to create a hyperlink: + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "\n", + /// "\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start( + /// Container::Link( + /// "https://example.com".into(), + /// LinkType::AutoLink, + /// ), + /// Attributes::new(), + /// ), + /// Event::Str("https://example.com".into()), + /// Event::End(Container::Link( + /// "https://example.com".into(), + /// LinkType::AutoLink, + /// )), + /// Event::Softbreak, + /// Event::Start( + /// Container::Link( + /// "me@example.com".into(), + /// LinkType::Email, + /// ), + /// Attributes::new(), + /// ), + /// Event::Str("me@example.com".into()), + /// Event::End(Container::Link( + /// "me@example.com".into(), + /// LinkType::Email, + /// )), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = concat!( + /// "

https://example.com\n", + /// "me@example.com

\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` + /// + /// Anchor text and the URL can be specified inline: + /// + /// ``` + /// # use jotdown::*; + /// let src = "[anchor](url)\n"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start( + /// Container::Link( + /// "url".into(), + /// LinkType::Span(SpanLinkType::Inline), + /// ), + /// Attributes::new(), + /// ), + /// Event::Str("anchor".into()), + /// Event::End( + /// Container::Link("url".into(), + /// LinkType::Span(SpanLinkType::Inline)), + /// ), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

anchor

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` + /// + /// Alternatively, the URL can be retrieved from a link definition using hard brackets, if it + /// exists: + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "[a][label]\n", + /// "[b][non-existent]\n", + /// "\n", + /// "[label]: url\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start( + /// Container::Link( + /// "url".into(), + /// LinkType::Span(SpanLinkType::Reference), + /// ), + /// Attributes::new(), + /// ), + /// Event::Str("a".into()), + /// Event::End( + /// Container::Link("url".into(), + /// LinkType::Span(SpanLinkType::Reference)), + /// ), + /// Event::Softbreak, + /// Event::Start( + /// Container::Link( + /// "non-existent".into(), + /// LinkType::Span(SpanLinkType::Unresolved), + /// ), + /// Attributes::new(), + /// ), + /// Event::Str("b".into()), + /// Event::End( + /// Container::Link("non-existent".into(), + /// LinkType::Span(SpanLinkType::Unresolved)), + /// ), + /// Event::End(Container::Paragraph), + /// Event::Blankline, + /// Event::Start( + /// Container::LinkDefinition { label: "label" }, + /// Attributes::new(), + /// ), + /// Event::Str("url".into()), + /// Event::End(Container::LinkDefinition { label: "label" }), + /// ], + /// ); + /// let html = concat!( + /// "

a\n", + /// "b

\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Link(CowStr<'s>, LinkType), - /// An inline image, the first field is either a destination URL or an unresolved tag. Inner - /// Str objects compose the alternative text. + /// An inline image, the first field is either a destination URL or an unresolved tag. + /// + /// # Examples + /// + /// Inner Str objects compose the alternative text: + /// + /// ``` + /// # use jotdown::*; + /// let src = "![alt text](img.png)"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start( + /// Container::Image("img.png".into(), SpanLinkType::Inline), + /// Attributes::new(), + /// ), + /// Event::Str("alt text".into()), + /// Event::End( + /// Container::Image("img.png".into(), SpanLinkType::Inline), + /// ), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

\"alt

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Image(CowStr<'s>, SpanLinkType), /// An inline verbatim string. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "inline `verbatim`"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("inline ".into()), + /// Event::Start(Container::Verbatim, Attributes::new()), + /// Event::Str("verbatim".into()), + /// Event::End(Container::Verbatim), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

inline verbatim

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Verbatim, /// An inline or display math element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = concat!( + /// "inline $`a\\cdot{}b` or\n", + /// "display $$`\\frac{a}{b}`\n", + /// ); + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Str("inline ".into()), + /// Event::Start( + /// Container::Math { display: false }, + /// Attributes::new(), + /// ), + /// Event::Str(r"a\cdot{}b".into()), + /// Event::End(Container::Math { display: false }), + /// Event::Str(" or".into()), + /// Event::Softbreak, + /// Event::Str("display ".into()), + /// Event::Start( + /// Container::Math { display: true }, + /// Attributes::new(), + /// ), + /// Event::Str(r"\frac{a}{b}".into()), + /// Event::End(Container::Math { display: true }), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = concat!( + /// "

inline \\(a\\cdot{}b\\) or\n", + /// "display \\[\\frac{a}{b}\\]

\n", + /// ); + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Math { display: bool }, /// Inline raw markup for a specific output format. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "`a`{=html}"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start( + /// Container::RawInline { format: "html" }, Attributes::new(), + /// ), + /// Event::Str("a".into()), + /// Event::End(Container::RawInline { format: "html" }), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

a

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` RawInline { format: &'s str }, /// A subscripted element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "~SUB~"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start(Container::Subscript, Attributes::new()), + /// Event::Str("SUB".into()), + /// Event::End(Container::Subscript), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

SUB

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Subscript, /// A superscripted element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "^SUP^"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start(Container::Superscript, Attributes::new()), + /// Event::Str("SUP".into()), + /// Event::End(Container::Superscript), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

SUP

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Superscript, /// An inserted inline element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "{+INS+}"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start(Container::Insert, Attributes::new()), + /// Event::Str("INS".into()), + /// Event::End(Container::Insert), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

INS

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Insert, /// A deleted inline element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "{-DEL-}"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start(Container::Delete, Attributes::new()), + /// Event::Str("DEL".into()), + /// Event::End(Container::Delete), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

DEL

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Delete, /// An inline element emphasized with a bold typeface. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "*STRONG*"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start(Container::Strong, Attributes::new()), + /// Event::Str("STRONG".into()), + /// Event::End(Container::Strong), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

STRONG

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Strong, /// An emphasized inline element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "_EM_"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start(Container::Emphasis, Attributes::new()), + /// Event::Str("EM".into()), + /// Event::End(Container::Emphasis), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

EM

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Emphasis, /// A highlighted inline element. + /// + /// # Examples + /// + /// ``` + /// # use jotdown::*; + /// let src = "{=MARK=}"; + /// let events: Vec<_> = Parser::new(src).collect(); + /// assert_eq!( + /// &events, + /// &[ + /// Event::Start(Container::Paragraph, Attributes::new()), + /// Event::Start(Container::Mark, Attributes::new()), + /// Event::Str("MARK".into()), + /// Event::End(Container::Mark), + /// Event::End(Container::Paragraph), + /// ], + /// ); + /// let html = "

MARK

\n"; + /// assert_eq!(&html::render_to_string(events.into_iter()), html); + /// ``` Mark, } @@ -563,9 +2011,8 @@ pub struct Parser<'s> { /// Contents obtained by the prepass. pre_pass: PrePass<'s>, - /// Last parsed block attributes, and its starting offset. - block_attributes: Attributes<'s>, - block_attributes_pos: Option, + /// Last parsed block attributes, and its span. + block_attributes: Option<(Attributes<'s>, Range)>, /// Current table row is a head row. table_head_row: bool, @@ -619,9 +2066,9 @@ impl<'s> PrePass<'s> { })) => { // All link definition tags have to be obtained initially, as references can // appear before the definition. - let attrs = attr_prev - .as_ref() - .map_or_else(Attributes::new, |sp| attr::parse(&src[sp.clone()])); + let attrs = attr_prev.as_ref().map_or_else(Attributes::new, |sp| { + src[sp.clone()].try_into().expect("should be valid") + }); let url = if let Some(block::Event { kind: block::EventKind::Inline, span, @@ -663,11 +2110,13 @@ impl<'s> PrePass<'s> { // as formatting must be removed. // // We choose to parse all headers twice instead of caching them. - let attrs = attr_prev.as_ref().map(|sp| attr::parse(&src[sp.clone()])); + let attrs = attr_prev + .as_ref() + .map(|sp| Attributes::try_from(&src[sp.clone()]).expect("should be valid")); let id_override = attrs .as_ref() - .and_then(|attrs| attrs.get("id")) - .map(ToString::to_string); + .and_then(|attrs| attrs.get_value("id")) + .map(|s| s.to_string()); let mut id_auto = String::new(); let mut text = String::new(); @@ -799,8 +2248,7 @@ impl<'s> Parser<'s> { src, blocks: blocks.into_iter().peekable(), pre_pass, - block_attributes: Attributes::new(), - block_attributes_pos: None, + block_attributes: None, table_head_row: false, verbatim: false, inline_parser, @@ -813,10 +2261,8 @@ impl<'s> Parser<'s> { /// Generally, the range of each event does not overlap with any other event and the ranges are /// in same order as the events are emitted, i.e. the start offset of an event must be greater /// or equal to the (exclusive) end offset of all events that were emitted before that event. - /// However, there are some exceptions to this rule: + /// However, there is an exception to this rule: /// - /// - Blank lines inbetween block attributes and the block causes the blankline events to - /// overlap with the block start event. /// - Caption events are emitted before the table rows while the input for the caption content /// is located after the table rows, causing the ranges to be out of order. /// @@ -931,7 +2377,7 @@ impl<'s> Parser<'s> { inline => (Some(inline), Attributes::new()), }; - inline.map(|inline| { + let event = inline.map(|inline| { let enter = matches!(inline.kind, inline::EventKind::Enter(_)); let event = match inline.kind { inline::EventKind::Enter(c) | inline::EventKind::Exit(c) => { @@ -965,8 +2411,11 @@ impl<'s> Parser<'s> { .get::(tag.as_ref()) .cloned(); - let (url_or_tag, ty) = if let Some((url, attrs_def)) = link_def { - attributes.union(attrs_def); + let (url_or_tag, ty) = if let Some((url, mut attrs_def)) = link_def { + if enter { + attrs_def.append(&mut attributes); + attributes = attrs_def; + } (url, SpanLinkType::Reference) } else { self.pre_pass.heading_id_by_tag(tag.as_ref()).map_or_else( @@ -991,7 +2440,7 @@ impl<'s> Parser<'s> { } }; if enter { - Event::Start(t, attributes) + Event::Start(t, attributes.take()) } else { Event::End(t) } @@ -1013,31 +2462,59 @@ impl<'s> Parser<'s> { inline::Atom::Hardbreak => Event::Hardbreak, inline::Atom::Escape => Event::Escape, }, + inline::EventKind::Empty => { + debug_assert!(!attributes.is_empty()); + Event::Attributes(attributes.take()) + } inline::EventKind::Str => Event::Str(self.src[inline.span.clone()].into()), inline::EventKind::Attributes { .. } | inline::EventKind::Placeholder => { panic!("{:?}", inline) } }; (event, inline.span) - }) + }); + + debug_assert!( + attributes.is_empty(), + "unhandled attributes: {:?}", + attributes + ); + + event } fn block(&mut self) -> Option<(Event<'s>, Range)> { while let Some(mut ev) = self.blocks.next() { let event = match ev.kind { block::EventKind::Atom(a) => match a { - block::Atom::Blankline => Event::Blankline, + block::Atom::Blankline => { + debug_assert_eq!(self.block_attributes, None); + Event::Blankline + } block::Atom::ThematicBreak => { - if let Some(pos) = self.block_attributes_pos.take() { - ev.span.start = pos; - } - Event::ThematicBreak(self.block_attributes.take()) + let attrs = if let Some((attrs, span)) = self.block_attributes.take() { + ev.span.start = span.start; + attrs + } else { + Attributes::new() + }; + Event::ThematicBreak(attrs) } block::Atom::Attributes => { - if self.block_attributes_pos.is_none() { - self.block_attributes_pos = Some(ev.span.start); + let (mut attrs, span) = self + .block_attributes + .take() + .unwrap_or_else(|| (Attributes::new(), ev.span.clone())); + attrs + .parse(&self.src[ev.span.clone()]) + .expect("should be valid"); + if matches!( + self.blocks.peek().map(|e| &e.kind), + Some(block::EventKind::Atom(block::Atom::Blankline)) + ) { + return Some((Event::Attributes(attrs), span)); } - self.block_attributes.parse(&self.src[ev.span.clone()]); + self.block_attributes = Some((attrs, span)); continue; } }, @@ -1131,13 +2608,15 @@ impl<'s> Parser<'s> { }, }; if enter { - if let Some(pos) = self.block_attributes_pos.take() { - ev.span.start = pos; - } - Event::Start(cont, self.block_attributes.take()) + let attrs = if let Some((attrs, span)) = self.block_attributes.take() { + ev.span.start = span.start; + attrs + } else { + Attributes::new() + }; + Event::Start(cont, attrs) } else { - self.block_attributes = Attributes::new(); - self.block_attributes_pos = None; + self.block_attributes = None; Event::End(cont) } } @@ -1163,7 +2642,11 @@ impl<'s> Parser<'s> { } fn next_span(&mut self) -> Option<(Event<'s>, Range)> { - self.inline().or_else(|| self.block()) + self.inline().or_else(|| self.block()).or_else(|| { + self.block_attributes + .take() + .map(|(attrs, span)| (Event::Attributes(attrs), span)) + }) } } @@ -1193,6 +2676,7 @@ impl<'s> Iterator for OffsetIter<'s> { #[cfg(test)] mod test { + use super::AttributeKind; use super::Attributes; use super::Container::*; use super::Event::*; @@ -1205,7 +2689,10 @@ mod test { macro_rules! test_parse { ($src:expr $(,$($token:expr),* $(,)?)?) => { #[allow(unused)] - let actual = super::Parser::new($src).collect::>(); + let actual = super::Parser::new($src) + .into_offset_iter() + .map(|(e, r)| (e, &$src[r])) + .collect::>(); let expected = &[$($($token),*,)?]; assert_eq!( actual, @@ -1213,12 +2700,17 @@ mod test { concat!( "\n", "\x1b[0;1m====================== INPUT =========================\x1b[0m\n", - "\x1b[2m{}", + "\x1b[2m{}{}", "\x1b[0;1m================ ACTUAL vs EXPECTED ==================\x1b[0m\n", "{}", "\x1b[0;1m======================================================\x1b[0m\n", ), $src, + if $src.ends_with('\n') { + "" + } else { + "\n" + }, { let a = actual.iter().map(|n| format!("{:?}", n)).collect::>(); let b = expected.iter().map(|n| format!("{:?}", n)).collect::>(); @@ -1255,49 +2747,67 @@ mod test { fn heading() { test_parse!( "#\n", - Start(Section { id: "s-1".into() }, Attributes::new()), - Start( - Heading { + (Start(Section { id: "s-1".into() }, Attributes::new()), ""), + ( + Start( + Heading { + level: 1, + has_section: true, + id: "s-1".into(), + }, + Attributes::new(), + ), + "#", + ), + ( + End(Heading { level: 1, has_section: true, - id: "s-1".into() - }, - Attributes::new() - ), - End(Heading { - level: 1, - has_section: true, - id: "s-1".into() - }), - End(Section { id: "s-1".into() }), + id: "s-1".into(), + }), + "", + ), + (End(Section { id: "s-1".into() }), ""), ); test_parse!( "# abc\ndef\n", - Start( - Section { - id: "abc-def".into() - }, - Attributes::new() + ( + Start( + Section { + id: "abc-def".into(), + }, + Attributes::new(), + ), + "", ), - Start( - Heading { + ( + Start( + Heading { + level: 1, + has_section: true, + id: "abc-def".into(), + }, + Attributes::new(), + ), + "#", + ), + (Str("abc".into()), "abc"), + (Softbreak, "\n"), + (Str("def".into()), "def"), + ( + End(Heading { level: 1, has_section: true, - id: "abc-def".into() - }, - Attributes::new() - ), - Str("abc".into()), - Softbreak, - Str("def".into()), - End(Heading { - level: 1, - has_section: true, - id: "abc-def".into(), - }), - End(Section { - id: "abc-def".into() - }), + id: "abc-def".into(), + }), + "", + ), + ( + End(Section { + id: "abc-def".into(), + }), + "", + ), ); } @@ -1309,41 +2819,58 @@ mod test { "{a=b}\n", "# def\n", // ), - Start(Section { id: "abc".into() }, Attributes::new()), - Start( - Heading { + (Start(Section { id: "abc".into() }, Attributes::new()), ""), + ( + Start( + Heading { + level: 1, + has_section: true, + id: "abc".into(), + }, + Attributes::new(), + ), + "#", + ), + (Str("abc".into()), "abc"), + ( + End(Heading { level: 1, has_section: true, - id: "abc".into() - }, - Attributes::new() - ), - Str("abc".into()), - End(Heading { - level: 1, - has_section: true, - id: "abc".into(), - }), - End(Section { id: "abc".into() }), - Start( - Section { id: "def".into() }, - [("a", "b")].into_iter().collect(), - ), - Start( - Heading { + id: "abc".into(), + }), + "", + ), + (End(Section { id: "abc".into() }), ""), + ( + Start( + Section { id: "def".into() }, + [(AttributeKind::Pair { key: "a" }, "b")] + .into_iter() + .collect(), + ), + "{a=b}\n", + ), + ( + Start( + Heading { + level: 1, + has_section: true, + id: "def".into(), + }, + Attributes::new(), + ), + "#", + ), + (Str("def".into()), "def"), + ( + End(Heading { level: 1, has_section: true, - id: "def".into() - }, - Attributes::new(), - ), - Str("def".into()), - End(Heading { - level: 1, - has_section: true, - id: "def".into(), - }), - End(Section { id: "def".into() }), + id: "def".into(), + }), + "", + ), + (End(Section { id: "def".into() }), ""), ); } @@ -1355,46 +2882,64 @@ mod test { "\n", // "# Some Section", // ), - Start(Paragraph, Attributes::new()), - Str("A ".into()), - Start( - Link( + (Start(Paragraph, Attributes::new()), ""), + (Str("A ".into()), "A "), + ( + Start( + Link( + "#Some-Section".into(), + LinkType::Span(SpanLinkType::Reference), + ), + Attributes::new(), + ), + "[", + ), + (Str("link".into()), "link"), + ( + End(Link( "#Some-Section".into(), - LinkType::Span(SpanLinkType::Reference) + LinkType::Span(SpanLinkType::Reference), + )), + "][Some Section]", + ), + (Str(" to another section.".into()), " to another section."), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Start( + Section { + id: "Some-Section".into(), + }, + Attributes::new(), ), - Attributes::new() - ), - Str("link".into()), - End(Link( - "#Some-Section".into(), - LinkType::Span(SpanLinkType::Reference) - )), - Str(" to another section.".into()), - End(Paragraph), - Blankline, - Start( - Section { - id: "Some-Section".into() - }, - Attributes::new() + "", ), - Start( - Heading { + ( + Start( + Heading { + level: 1, + has_section: true, + id: "Some-Section".into(), + }, + Attributes::new(), + ), + "#", + ), + (Str("Some Section".into()), "Some Section"), + ( + End(Heading { level: 1, has_section: true, id: "Some-Section".into(), - }, - Attributes::new(), - ), - Str("Some Section".into()), - End(Heading { - level: 1, - has_section: true, - id: "Some-Section".into(), - }), - End(Section { - id: "Some-Section".into() - }), + }), + "", + ), + ( + End(Section { + id: "Some-Section".into(), + }), + "", + ), ); test_parse!( concat!( @@ -1405,55 +2950,79 @@ mod test { "\n", // "# a\n", // ), - Start(Paragraph, Attributes::new()), - Start( - Link("#a".into(), LinkType::Span(SpanLinkType::Reference)), - Attributes::new() - ), - Str("a".into()), - End(Link("#a".into(), LinkType::Span(SpanLinkType::Reference))), - Softbreak, - Start( - Link("#b".into(), LinkType::Span(SpanLinkType::Reference)), - Attributes::new() - ), - Str("b".into()), - End(Link("#b".into(), LinkType::Span(SpanLinkType::Reference))), - End(Paragraph), - Blankline, - Start(Section { id: "b".into() }, Attributes::new()), - Start( - Heading { + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("#a".into(), LinkType::Span(SpanLinkType::Reference)), + Attributes::new(), + ), + "[", + ), + (Str("a".into()), "a"), + ( + End(Link("#a".into(), LinkType::Span(SpanLinkType::Reference))), + "][]", + ), + (Softbreak, "\n"), + ( + Start( + Link("#b".into(), LinkType::Span(SpanLinkType::Reference)), + Attributes::new(), + ), + "[", + ), + (Str("b".into()), "b"), + ( + End(Link("#b".into(), LinkType::Span(SpanLinkType::Reference))), + "][]", + ), + (End(Paragraph), ""), + (Blankline, "\n"), + (Start(Section { id: "b".into() }, Attributes::new()), ""), + ( + Start( + Heading { + level: 1, + has_section: true, + id: "b".into(), + }, + Attributes::new(), + ), + "#", + ), + (Str("b".into()), "b"), + ( + End(Heading { level: 1, has_section: true, id: "b".into(), - }, - Attributes::new(), - ), - Str("b".into()), - End(Heading { - level: 1, - has_section: true, - id: "b".into(), - }), - Blankline, - End(Section { id: "b".into() }), - Start(Section { id: "a".into() }, Attributes::new()), - Start( - Heading { + }), + "", + ), + (Blankline, "\n"), + (End(Section { id: "b".into() }), ""), + (Start(Section { id: "a".into() }, Attributes::new()), ""), + ( + Start( + Heading { + level: 1, + has_section: true, + id: "a".into(), + }, + Attributes::new(), + ), + "#", + ), + (Str("a".into()), "a"), + ( + End(Heading { level: 1, has_section: true, id: "a".into(), - }, - Attributes::new(), - ), - Str("a".into()), - End(Heading { - level: 1, - has_section: true, - id: "a".into(), - }), - End(Section { id: "a".into() }), + }), + "", + ), + (End(Section { id: "a".into() }), ""), ); } @@ -1461,9 +3030,9 @@ mod test { fn blockquote() { test_parse!( ">\n", - Start(Blockquote, Attributes::new()), - Blankline, - End(Blockquote), + (Start(Blockquote, Attributes::new()), ">"), + (Blankline, "\n"), + (End(Blockquote), ""), ); } @@ -1471,25 +3040,25 @@ mod test { fn para() { test_parse!( "para", - Start(Paragraph, Attributes::new()), - Str("para".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (Str("para".into()), "para"), + (End(Paragraph), ""), ); test_parse!( "pa ra", - Start(Paragraph, Attributes::new()), - Str("pa ra".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (Str("pa ra".into()), "pa ra"), + (End(Paragraph), ""), ); test_parse!( "para0\n\npara1", - Start(Paragraph, Attributes::new()), - Str("para0".into()), - End(Paragraph), - Blankline, - Start(Paragraph, Attributes::new()), - Str("para1".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (Str("para0".into()), "para0"), + (End(Paragraph), ""), + (Blankline, "\n"), + (Start(Paragraph, Attributes::new()), ""), + (Str("para1".into()), "para1"), + (End(Paragraph), ""), ); } @@ -1497,25 +3066,25 @@ mod test { fn verbatim() { test_parse!( "`abc\ndef", - Start(Paragraph, Attributes::new()), - Start(Verbatim, Attributes::new()), - Str("abc\ndef".into()), - End(Verbatim), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (Start(Verbatim, Attributes::new()), "`"), + (Str("abc\ndef".into()), "abc\ndef"), + (End(Verbatim), ""), + (End(Paragraph), ""), ); test_parse!( concat!( "> `abc\n", "> def\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start(Verbatim, Attributes::new()), - Str("abc\n".into()), - Str("def".into()), - End(Verbatim), - End(Paragraph), - End(Blockquote), + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + (Start(Verbatim, Attributes::new()), "`"), + (Str("abc\n".into()), "abc\n"), + (Str("def".into()), "def"), + (End(Verbatim), ""), + (End(Paragraph), ""), + (End(Blockquote), ""), ); } @@ -1523,11 +3092,14 @@ mod test { fn raw_inline() { test_parse!( "``raw\nraw``{=format}", - Start(Paragraph, Attributes::new()), - Start(RawInline { format: "format" }, Attributes::new()), - Str("raw\nraw".into()), - End(RawInline { format: "format" }), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start(RawInline { format: "format" }, Attributes::new()), + "``", + ), + (Str("raw\nraw".into()), "raw\nraw"), + (End(RawInline { format: "format" }), "``{=format}"), + (End(Paragraph), ""), ); } @@ -1535,9 +3107,12 @@ mod test { fn raw_block() { test_parse!( "``` =html\n\n```", - Start(RawBlock { format: "html" }, Attributes::new()), - Str("
".into()), - End(RawBlock { format: "html" }), + ( + Start(RawBlock { format: "html" }, Attributes::new()), + "``` =html\n", + ), + (Str("
".into()), "
"), + (End(RawBlock { format: "html" }), "```"), ); } @@ -1557,19 +3132,25 @@ mod test { "\n", // "```\n", // ), - Start(RawBlock { format: "html" }, Attributes::new()), - Str("\n".into()), - Str("".into()), - End(RawBlock { format: "html" }), - Blankline, - Start(Paragraph, Attributes::new()), - Str("paragraph".into()), - End(Paragraph), - Blankline, - Start(RawBlock { format: "html" }, Attributes::new()), - Str("\n".into()), - Str("".into()), - End(RawBlock { format: "html" }), + ( + Start(RawBlock { format: "html" }, Attributes::new()), + "```=html\n", + ), + (Str("\n".into()), "\n"), + (Str("".into()), ""), + (End(RawBlock { format: "html" }), "```\n"), + (Blankline, "\n"), + (Start(Paragraph, Attributes::new()), ""), + (Str("paragraph".into()), "paragraph"), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Start(RawBlock { format: "html" }, Attributes::new()), + "```=html\n", + ), + (Str("\n".into()), "\n"), + (Str("".into()), ""), + (End(RawBlock { format: "html" }), "```\n"), ); } @@ -1577,11 +3158,11 @@ mod test { fn symbol() { test_parse!( "abc :+1: def", - Start(Paragraph, Attributes::new()), - Str("abc ".into()), - Symbol("+1".into()), - Str(" def".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (Str("abc ".into()), "abc "), + (Symbol("+1".into()), ":+1:"), + (Str(" def".into()), " def"), + (End(Paragraph), ""), ); } @@ -1589,14 +3170,20 @@ mod test { fn link_inline() { test_parse!( "[text](url)", - Start(Paragraph, Attributes::new()), - Start( - Link("url".into(), LinkType::Span(SpanLinkType::Inline)), - Attributes::new() - ), - Str("text".into()), - End(Link("url".into(), LinkType::Span(SpanLinkType::Inline))), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("url".into(), LinkType::Span(SpanLinkType::Inline)), + Attributes::new(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("url".into(), LinkType::Span(SpanLinkType::Inline))), + "](url)", + ), + (End(Paragraph), ""), ); } @@ -1607,16 +3194,22 @@ mod test { "> [text](url\n", "> url)\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start( - Link("urlurl".into(), LinkType::Span(SpanLinkType::Inline)), - Attributes::new() + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("urlurl".into(), LinkType::Span(SpanLinkType::Inline)), + Attributes::new(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("urlurl".into(), LinkType::Span(SpanLinkType::Inline))), + "](url\n> url)", ), - Str("text".into()), - End(Link("urlurl".into(), LinkType::Span(SpanLinkType::Inline))), - End(Paragraph), - End(Blockquote), + (End(Paragraph), ""), + (End(Blockquote), ""), ); test_parse!( concat!( @@ -1624,16 +3217,22 @@ mod test { "> bc\n", // "> def)\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start( - Link("abcdef".into(), LinkType::Span(SpanLinkType::Inline)), - Attributes::new() + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("abcdef".into(), LinkType::Span(SpanLinkType::Inline)), + Attributes::new(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("abcdef".into(), LinkType::Span(SpanLinkType::Inline))), + "](a\n> bc\n> def)", ), - Str("text".into()), - End(Link("abcdef".into(), LinkType::Span(SpanLinkType::Inline))), - End(Paragraph), - End(Blockquote), + (End(Paragraph), ""), + (End(Blockquote), ""), ); } @@ -1645,18 +3244,27 @@ mod test { "\n", "[tag]: url\n" // ), - Start(Paragraph, Attributes::new()), - Start( - Link("url".into(), LinkType::Span(SpanLinkType::Reference)), - Attributes::new() - ), - Str("text".into()), - End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), - End(Paragraph), - Blankline, - Start(LinkDefinition { label: "tag" }, Attributes::new()), - Str("url".into()), - End(LinkDefinition { label: "tag" }), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("url".into(), LinkType::Span(SpanLinkType::Reference)), + Attributes::new(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), + "][tag]", + ), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Start(LinkDefinition { label: "tag" }, Attributes::new()), + "[tag]:", + ), + (Str("url".into()), "url"), + (End(LinkDefinition { label: "tag" }), ""), ); test_parse!( concat!( @@ -1664,18 +3272,24 @@ mod test { "\n", "[tag]: url\n" // ), - Start(Paragraph, Attributes::new()), - Start( - Image("url".into(), SpanLinkType::Reference), - Attributes::new() - ), - Str("text".into()), - End(Image("url".into(), SpanLinkType::Reference)), - End(Paragraph), - Blankline, - Start(LinkDefinition { label: "tag" }, Attributes::new()), - Str("url".into()), - End(LinkDefinition { label: "tag" }), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Image("url".into(), SpanLinkType::Reference), + Attributes::new(), + ), + "![", + ), + (Str("text".into()), "text"), + (End(Image("url".into(), SpanLinkType::Reference)), "][tag]"), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Start(LinkDefinition { label: "tag" }, Attributes::new()), + "[tag]:", + ), + (Str("url".into()), "url"), + (End(LinkDefinition { label: "tag" }), ""), ); } @@ -1683,25 +3297,34 @@ mod test { fn link_reference_unresolved() { test_parse!( "[text][tag]", - Start(Paragraph, Attributes::new()), - Start( - Link("tag".into(), LinkType::Span(SpanLinkType::Unresolved)), - Attributes::new() - ), - Str("text".into()), - End(Link("tag".into(), LinkType::Span(SpanLinkType::Unresolved))), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("tag".into(), LinkType::Span(SpanLinkType::Unresolved)), + Attributes::new(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("tag".into(), LinkType::Span(SpanLinkType::Unresolved))), + "][tag]", + ), + (End(Paragraph), ""), ); test_parse!( "![text][tag]", - Start(Paragraph, Attributes::new()), - Start( - Image("tag".into(), SpanLinkType::Unresolved), - Attributes::new() - ), - Str("text".into()), - End(Image("tag".into(), SpanLinkType::Unresolved)), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Image("tag".into(), SpanLinkType::Unresolved), + Attributes::new(), + ), + "![", + ), + (Str("text".into()), "text"), + (End(Image("tag".into(), SpanLinkType::Unresolved)), "][tag]"), + (End(Paragraph), ""), ); } @@ -1714,20 +3337,29 @@ mod test { "\n", // "[a b]: url\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start( - Link("url".into(), LinkType::Span(SpanLinkType::Reference)), - Attributes::new() - ), - Str("text".into()), - End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), - End(Paragraph), - End(Blockquote), - Blankline, - Start(LinkDefinition { label: "a b" }, Attributes::new()), - Str("url".into()), - End(LinkDefinition { label: "a b" }), + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("url".into(), LinkType::Span(SpanLinkType::Reference)), + Attributes::new(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), + "][a\n> b]", + ), + (End(Paragraph), ""), + (End(Blockquote), ""), + (Blankline, "\n"), + ( + Start(LinkDefinition { label: "a b" }, Attributes::new()), + "[a b]:", + ), + (Str("url".into()), "url"), + (End(LinkDefinition { label: "a b" }), ""), ); } @@ -1742,32 +3374,47 @@ mod test { "\n", // "[a b]: url\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start( - Link("url".into(), LinkType::Span(SpanLinkType::Reference)), - Attributes::new() - ), - Str("a".into()), - Softbreak, - Str("b".into()), - End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), - Softbreak, - Start( - Link("url".into(), LinkType::Span(SpanLinkType::Reference)), - Attributes::new() - ), - Str("a".into()), - Escape, - Hardbreak, - Str("b".into()), - End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), - End(Paragraph), - End(Blockquote), - Blankline, - Start(LinkDefinition { label: "a b" }, Attributes::new()), - Str("url".into()), - End(LinkDefinition { label: "a b" }), + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("url".into(), LinkType::Span(SpanLinkType::Reference)), + Attributes::new(), + ), + "[", + ), + (Str("a".into()), "a"), + (Softbreak, "\n"), + (Str("b".into()), "b"), + ( + End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), + "][]", + ), + (Softbreak, "\n"), + ( + Start( + Link("url".into(), LinkType::Span(SpanLinkType::Reference)), + Attributes::new(), + ), + "[", + ), + (Str("a".into()), "a"), + (Escape, "\\"), + (Hardbreak, "\n"), + (Str("b".into()), "b"), + ( + End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), + "][]", + ), + (End(Paragraph), ""), + (End(Blockquote), ""), + (Blankline, "\n"), + ( + Start(LinkDefinition { label: "a b" }, Attributes::new()), + "[a b]:", + ), + (Str("url".into()), "url"), + (End(LinkDefinition { label: "a b" }), ""), ); } @@ -1780,19 +3427,28 @@ mod test { "[tag]: u\n", " rl\n", // ), - Start(Paragraph, Attributes::new()), - Start( - Link("url".into(), LinkType::Span(SpanLinkType::Reference)), - Attributes::new() - ), - Str("text".into()), - End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), - End(Paragraph), - Blankline, - Start(LinkDefinition { label: "tag" }, Attributes::new()), - Str("u".into()), - Str("rl".into()), - End(LinkDefinition { label: "tag" }), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("url".into(), LinkType::Span(SpanLinkType::Reference)), + Attributes::new(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), + "][tag]", + ), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Start(LinkDefinition { label: "tag" }, Attributes::new()), + "[tag]:", + ), + (Str("u".into()), "u"), + (Str("rl".into()), "rl"), + (End(LinkDefinition { label: "tag" }), ""), ); test_parse!( concat!( @@ -1802,22 +3458,31 @@ mod test { " url\n", // " cont\n", // ), - Start(Paragraph, Attributes::new()), - Start( - Link("urlcont".into(), LinkType::Span(SpanLinkType::Reference)), - Attributes::new() - ), - Str("text".into()), - End(Link( - "urlcont".into(), - LinkType::Span(SpanLinkType::Reference) - )), - End(Paragraph), - Blankline, - Start(LinkDefinition { label: "tag" }, Attributes::new()), - Str("url".into()), - Str("cont".into()), - End(LinkDefinition { label: "tag" }), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("urlcont".into(), LinkType::Span(SpanLinkType::Reference)), + Attributes::new(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link( + "urlcont".into(), + LinkType::Span(SpanLinkType::Reference), + )), + "][tag]", + ), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Start(LinkDefinition { label: "tag" }, Attributes::new()), + "[tag]:", + ), + (Str("url".into()), "url"), + (Str("cont".into()), "cont"), + (End(LinkDefinition { label: "tag" }), ""), ); } @@ -1831,24 +3496,40 @@ mod test { "[tag]: url\n", "para\n", ), - Start(Paragraph, Attributes::new()), - Start( - Link("url".into(), LinkType::Span(SpanLinkType::Reference)), - [("b", "c"), ("a", "b")].into_iter().collect(), - ), - Str("text".into()), - End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), - End(Paragraph), - Blankline, - Start( - LinkDefinition { label: "tag" }, - [("a", "b")].into_iter().collect() - ), - Str("url".into()), - End(LinkDefinition { label: "tag" }), - Start(Paragraph, Attributes::new()), - Str("para".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("url".into(), LinkType::Span(SpanLinkType::Reference)), + [ + (AttributeKind::Pair { key: "a" }, "b"), + (AttributeKind::Pair { key: "b" }, "c"), + ] + .into_iter() + .collect(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), + "][tag]{b=c}", + ), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Start( + LinkDefinition { label: "tag" }, + [(AttributeKind::Pair { key: "a" }, "b")] + .into_iter() + .collect(), + ), + "{a=b}\n[tag]:", + ), + (Str("url".into()), "url"), + (End(LinkDefinition { label: "tag" }), ""), + (Start(Paragraph, Attributes::new()), ""), + (Str("para".into()), "para"), + (End(Paragraph), ""), ); } @@ -1862,24 +3543,38 @@ mod test { "[tag]: url\n", "para\n", ), - Start(Paragraph, Attributes::new()), - Start( - Link("url".into(), LinkType::Span(SpanLinkType::Reference)), - [("class", "link"), ("class", "def")].into_iter().collect(), - ), - Str("text".into()), - End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), - End(Paragraph), - Blankline, - Start( - LinkDefinition { label: "tag" }, - [("class", "def")].into_iter().collect() - ), - Str("url".into()), - End(LinkDefinition { label: "tag" }), - Start(Paragraph, Attributes::new()), - Str("para".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("url".into(), LinkType::Span(SpanLinkType::Reference)), + [ + (AttributeKind::Class, "def"), + (AttributeKind::Class, "link"), + ] + .into_iter() + .collect(), + ), + "[", + ), + (Str("text".into()), "text"), + ( + End(Link("url".into(), LinkType::Span(SpanLinkType::Reference))), + "][tag]{.link}", + ), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Start( + LinkDefinition { label: "tag" }, + [(AttributeKind::Class, "def")].into_iter().collect(), + ), + "{.def}\n[tag]:", + ), + (Str("url".into()), "url"), + (End(LinkDefinition { label: "tag" }), ""), + (Start(Paragraph, Attributes::new()), ""), + (Str("para".into()), "para"), + (End(Paragraph), ""), ); } @@ -1887,14 +3582,17 @@ mod test { fn autolink() { test_parse!( "\n", - Start(Paragraph, Attributes::new()), - Start( - Link("proto:url".into(), LinkType::AutoLink), - Attributes::new() - ), - Str("proto:url".into()), - End(Link("proto:url".into(), LinkType::AutoLink)), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("proto:url".into(), LinkType::AutoLink), + Attributes::new(), + ), + "<", + ), + (Str("proto:url".into()), "proto:url"), + (End(Link("proto:url".into(), LinkType::AutoLink)), ">"), + (End(Paragraph), ""), ); } @@ -1902,14 +3600,17 @@ mod test { fn email() { test_parse!( "\n", - Start(Paragraph, Attributes::new()), - Start( - Link("name@domain".into(), LinkType::Email), - Attributes::new() - ), - Str("name@domain".into()), - End(Link("name@domain".into(), LinkType::Email)), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Link("name@domain".into(), LinkType::Email), + Attributes::new(), + ), + "<", + ), + (Str("name@domain".into()), "name@domain"), + (End(Link("name@domain".into(), LinkType::Email)), ">"), + (End(Paragraph), ""), ); } @@ -1917,11 +3618,11 @@ mod test { fn footnote_references() { test_parse!( "[^a][^b][^c]", - Start(Paragraph, Attributes::new()), - FootnoteReference("a"), - FootnoteReference("b"), - FootnoteReference("c"), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (FootnoteReference("a"), "[^a]"), + (FootnoteReference("b"), "[^b]"), + (FootnoteReference("c"), "[^c]"), + (End(Paragraph), ""), ); } @@ -1929,15 +3630,15 @@ mod test { fn footnote() { test_parse!( "[^a]\n\n[^a]: a\n", - Start(Paragraph, Attributes::new()), - FootnoteReference("a"), - End(Paragraph), - Blankline, - Start(Footnote { label: "a" }, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("a".into()), - End(Paragraph), - End(Footnote { label: "a" }), + (Start(Paragraph, Attributes::new()), ""), + (FootnoteReference("a"), "[^a]"), + (End(Paragraph), ""), + (Blankline, "\n"), + (Start(Footnote { label: "a" }, Attributes::new()), "[^a]:"), + (Start(Paragraph, Attributes::new()), ""), + (Str("a".into()), "a"), + (End(Paragraph), ""), + (End(Footnote { label: "a" }), ""), ); } @@ -1951,19 +3652,19 @@ mod test { "\n", " def", // ), - Start(Paragraph, Attributes::new()), - FootnoteReference("a"), - End(Paragraph), - Blankline, - Start(Footnote { label: "a" }, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("abc".into()), - End(Paragraph), - Blankline, - Start(Paragraph, Attributes::new()), - Str("def".into()), - End(Paragraph), - End(Footnote { label: "a" }), + (Start(Paragraph, Attributes::new()), ""), + (FootnoteReference("a"), "[^a]"), + (End(Paragraph), ""), + (Blankline, "\n"), + (Start(Footnote { label: "a" }, Attributes::new()), "[^a]:"), + (Start(Paragraph, Attributes::new()), ""), + (Str("abc".into()), "abc"), + (End(Paragraph), ""), + (Blankline, "\n"), + (Start(Paragraph, Attributes::new()), ""), + (Str("def".into()), "def"), + (End(Paragraph), ""), + (End(Footnote { label: "a" }), ""), ); } @@ -1978,21 +3679,21 @@ mod test { "\n", "para\n", // ), - Start(Paragraph, Attributes::new()), - FootnoteReference("a"), - End(Paragraph), - Blankline, - Start(Footnote { label: "a" }, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("note".into()), - Softbreak, - Str("cont".into()), - End(Paragraph), - Blankline, - End(Footnote { label: "a" }), - Start(Paragraph, Attributes::new()), - Str("para".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (FootnoteReference("a"), "[^a]"), + (End(Paragraph), ""), + (Blankline, "\n"), + (Start(Footnote { label: "a" }, Attributes::new()), "[^a]:"), + (Start(Paragraph, Attributes::new()), ""), + (Str("note".into()), "note"), + (Softbreak, "\n"), + (Str("cont".into()), "cont"), + (End(Paragraph), ""), + (Blankline, "\n"), + (End(Footnote { label: "a" }), ""), + (Start(Paragraph, Attributes::new()), ""), + (Str("para".into()), "para"), + (End(Paragraph), ""), ); test_parse!( concat!( @@ -2001,17 +3702,17 @@ mod test { "[^a]: note\n", // ":::\n", // ), - Start(Paragraph, Attributes::new()), - FootnoteReference("a"), - End(Paragraph), - Blankline, - Start(Footnote { label: "a" }, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("note".into()), - End(Paragraph), - End(Footnote { label: "a" }), - Start(Div { class: "" }, Attributes::new()), - End(Div { class: "" }), + (Start(Paragraph, Attributes::new()), ""), + (FootnoteReference("a"), "[^a]"), + (End(Paragraph), ""), + (Blankline, "\n"), + (Start(Footnote { label: "a" }, Attributes::new()), "[^a]:"), + (Start(Paragraph, Attributes::new()), ""), + (Str("note".into()), "note"), + (End(Paragraph), ""), + (End(Footnote { label: "a" }), ""), + (Start(Div { class: "" }, Attributes::new()), ":::\n"), + (End(Div { class: "" }), ""), ); } @@ -2019,9 +3720,81 @@ mod test { fn attr_block() { test_parse!( "{.some_class}\npara\n", - Start(Paragraph, [("class", "some_class")].into_iter().collect()), - Str("para".into()), - End(Paragraph), + ( + Start( + Paragraph, + [(AttributeKind::Class, "some_class")].into_iter().collect(), + ), + "{.some_class}\n", + ), + (Str("para".into()), "para"), + (End(Paragraph), ""), + ); + test_parse!( + concat!( + "{.a}\n", + "{#b}\n", + "para\n", // + ), + ( + Start( + Paragraph, + [(AttributeKind::Class, "a"), (AttributeKind::Id, "b")] + .into_iter() + .collect(), + ), + "{.a}\n{#b}\n", + ), + (Str("para".into()), "para"), + (End(Paragraph), ""), + ); + } + + #[test] + fn attr_block_dangling() { + test_parse!( + "{.a}", + ( + Attributes([(AttributeKind::Class, "a")].into_iter().collect()), + "{.a}", + ), + ); + test_parse!( + "para\n\n{.a}", + (Start(Paragraph, Attributes::new()), ""), + (Str("para".into()), "para"), + (End(Paragraph), ""), + (Blankline, "\n"), + ( + Attributes([(AttributeKind::Class, "a")].into_iter().collect()), + "{.a}", + ), + ); + } + + #[test] + fn attr_block_blankline() { + test_parse!( + "{.a}\n\n{.b}\n\n{.c}\npara", + ( + Attributes([(AttributeKind::Class, "a")].into_iter().collect()), + "{.a}\n", + ), + (Blankline, "\n"), + ( + Attributes([(AttributeKind::Class, "b")].into_iter().collect()), + "{.b}\n", + ), + (Blankline, "\n"), + ( + Start( + Paragraph, + [(AttributeKind::Class, "c")].into_iter().collect(), + ), + "{.c}\n", + ), + (Str("para".into()), "para"), + (End(Paragraph), ""), ); } @@ -2029,12 +3802,18 @@ mod test { fn attr_inline() { test_parse!( "abc _def_{.ghi}", - Start(Paragraph, Attributes::new()), - Str("abc ".into()), - Start(Emphasis, [("class", "ghi")].into_iter().collect()), - Str("def".into()), - End(Emphasis), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (Str("abc ".into()), "abc "), + ( + Start( + Emphasis, + [(AttributeKind::Class, "ghi")].into_iter().collect(), + ), + "_", + ), + (Str("def".into()), "def"), + (End(Emphasis), "_{.ghi}"), + (End(Paragraph), ""), ); } @@ -2042,25 +3821,44 @@ mod test { fn attr_inline_consecutive() { test_parse!( "_abc def_{.a}{.b #i}", - Start(Paragraph, Attributes::new()), - Start( - Emphasis, - [("class", "a b"), ("id", "i")].into_iter().collect(), - ), - Str("abc def".into()), - End(Emphasis), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Emphasis, + [ + (AttributeKind::Class, "a"), + (AttributeKind::Class, "b"), + (AttributeKind::Id, "i"), + ] + .into_iter() + .collect(), + ), + "_", + ), + (Str("abc def".into()), "abc def"), + (End(Emphasis), "_{.a}{.b #i}"), + (End(Paragraph), ""), ); test_parse!( "_abc def_{.a}{%%}{.b #i}", - Start(Paragraph, Attributes::new()), - Start( - Emphasis, - [("class", "a b"), ("id", "i")].into_iter().collect(), - ), - Str("abc def".into()), - End(Emphasis), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Emphasis, + [ + (AttributeKind::Class, "a"), + (AttributeKind::Comment, ""), + (AttributeKind::Class, "b"), + (AttributeKind::Id, "i"), + ] + .into_iter() + .collect(), + ), + "_", + ), + (Str("abc def".into()), "abc def"), + (End(Emphasis), "_{.a}{%%}{.b #i}"), + (End(Paragraph), ""), ); } @@ -2068,41 +3866,70 @@ mod test { fn attr_inline_consecutive_invalid() { test_parse!( "_abc def_{.a}{.b #i}{.c invalid}", - Start(Paragraph, Attributes::new()), - Start( - Emphasis, - [("class", "a b"), ("id", "i")].into_iter().collect(), - ), - Str("abc def".into()), - End(Emphasis), - Str("{.c invalid}".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Emphasis, + [ + (AttributeKind::Class, "a"), + (AttributeKind::Class, "b"), + (AttributeKind::Id, "i"), + ] + .into_iter() + .collect(), + ), + "_", + ), + (Str("abc def".into()), "abc def"), + (End(Emphasis), "_{.a}{.b #i}"), + (Str("{.c invalid}".into()), "{.c invalid}"), + (End(Paragraph), ""), ); test_parse!( "_abc def_{.a}{.b #i}{%%}{.c invalid}", - Start(Paragraph, Attributes::new()), - Start( - Emphasis, - [("class", "a b"), ("id", "i")].into_iter().collect(), - ), - Str("abc def".into()), - End(Emphasis), - Str("{.c invalid}".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Emphasis, + [ + (AttributeKind::Class, "a"), + (AttributeKind::Class, "b"), + (AttributeKind::Id, "i"), + (AttributeKind::Comment, ""), + ] + .into_iter() + .collect(), + ), + "_", + ), + (Str("abc def".into()), "abc def"), + (End(Emphasis), "_{.a}{.b #i}{%%}"), + (Str("{.c invalid}".into()), "{.c invalid}"), + (End(Paragraph), ""), ); test_parse!( concat!("_abc def_{.a}{.b #i}{%%}{.c\n", "invalid}\n"), - Start(Paragraph, Attributes::new()), - Start( - Emphasis, - [("class", "a b"), ("id", "i")].into_iter().collect(), - ), - Str("abc def".into()), - End(Emphasis), - Str("{.c".into()), - Softbreak, - Str("invalid}".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Emphasis, + [ + (AttributeKind::Class, "a"), + (AttributeKind::Class, "b"), + (AttributeKind::Id, "i"), + (AttributeKind::Comment, ""), + ] + .into_iter() + .collect(), + ), + "_", + ), + (Str("abc def".into()), "abc def"), + (End(Emphasis), "_{.a}{.b #i}{%%}"), + (Str("{.c".into()), "{.c"), + (Softbreak, "\n"), + (Str("invalid}".into()), "invalid}"), + (End(Paragraph), ""), ); } @@ -2113,13 +3940,24 @@ mod test { "> _abc_{a=b\n", // "> c=d}\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start(Emphasis, [("a", "b"), ("c", "d")].into_iter().collect()), - Str("abc".into()), - End(Emphasis), - End(Paragraph), - End(Blockquote), + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Emphasis, + [ + (AttributeKind::Pair { key: "a" }, "b"), + (AttributeKind::Pair { key: "c" }, "d"), + ] + .into_iter() + .collect(), + ), + "_", + ), + (Str("abc".into()), "abc"), + (End(Emphasis), "_{a=b\n> c=d}"), + (End(Paragraph), ""), + (End(Blockquote), ""), ); test_parse!( concat!( @@ -2127,13 +3965,24 @@ mod test { "> %%\n", // "> a=a}\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start(Span, [("a", "a")].into_iter().collect()), - Str("a".into()), - End(Span), - End(Paragraph), - End(Blockquote), + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Span, + [ + (AttributeKind::Comment, ""), + (AttributeKind::Pair { key: "a" }, "a"), + ] + .into_iter() + .collect(), + ), + "", + ), + (Str("a".into()), "a"), + (End(Span), "{\n> %%\n> a=a}"), + (End(Paragraph), ""), + (End(Blockquote), ""), ); test_parse!( concat!( @@ -2141,26 +3990,42 @@ mod test { "> b\n", // "> c\"}\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start(Span, [("a", "a b c")].into_iter().collect()), - Str("a".into()), - End(Span), - End(Paragraph), - End(Blockquote), + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Span, + [(AttributeKind::Pair { key: "a" }, "a b c")] + .into_iter() + .collect(), + ), + "", + ), + (Str("a".into()), "a"), + (End(Span), "{a=\"a\n> b\n> c\"}"), + (End(Paragraph), ""), + (End(Blockquote), ""), ); test_parse!( concat!( "> a{a=\"\n", // "> b\"}\n", // ), - Start(Blockquote, Attributes::new()), - Start(Paragraph, Attributes::new()), - Start(Span, [("a", "b")].into_iter().collect()), - Str("a".into()), - End(Span), - End(Paragraph), - End(Blockquote), + (Start(Blockquote, Attributes::new()), ">"), + (Start(Paragraph, Attributes::new()), ""), + ( + Start( + Span, + [(AttributeKind::Pair { key: "a" }, "b")] + .into_iter() + .collect(), + ), + "", + ), + (Str("a".into()), "a"), + (End(Span), "{a=\"\n> b\"}"), + (End(Paragraph), ""), + (End(Blockquote), ""), ); } @@ -2171,11 +4036,11 @@ mod test { "a{\n", // " b\n", // ), - Start(Paragraph, Attributes::new()), - Str("a{".into()), - Softbreak, - Str("b".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (Str("a{".into()), "a{"), + (Softbreak, "\n"), + (Str("b".into()), "b"), + (End(Paragraph), ""), ); } @@ -2187,13 +4052,70 @@ mod test { " b\n", // "}", // ), - Start(Paragraph, Attributes::new()), - Str("a{a=b".into()), - Softbreak, - Str("b".into()), - Softbreak, - Str("}".into()), - End(Paragraph), + (Start(Paragraph, Attributes::new()), ""), + (Str("a{a=b".into()), "a{a=b"), + (Softbreak, "\n"), + (Str("b".into()), "b"), + (Softbreak, "\n"), + (Str("}".into()), "}"), + (End(Paragraph), ""), + ); + } + + #[test] + fn attr_inline_dangling() { + test_parse!( + "*a\n{%}", + (Start(Paragraph, Attributes::new()), ""), + (Str("*a".into()), "*a"), + (Softbreak, "\n"), + ( + Attributes([(AttributeKind::Comment, "")].into_iter().collect()), + "{%}", + ), + (End(Paragraph), ""), + ); + test_parse!( + "a {.b} c", + (Start(Paragraph, Attributes::new()), ""), + (Str("a ".into()), "a "), + ( + Attributes([(AttributeKind::Class, "b")].into_iter().collect()), + "{.b}", + ), + (Str(" c".into()), " c"), + (End(Paragraph), ""), + ); + test_parse!( + "a {%cmt} c", + (Start(Paragraph, Attributes::new()), ""), + (Str("a ".into()), "a "), + ( + Attributes([(AttributeKind::Comment, "cmt")].into_iter().collect()), + "{%cmt}", + ), + (Str(" c".into()), " c"), + (End(Paragraph), ""), + ); + test_parse!( + "a {%cmt}", + (Start(Paragraph, Attributes::new()), ""), + (Str("a ".into()), "a "), + ( + Attributes([(AttributeKind::Comment, "cmt")].into_iter().collect()), + "{%cmt}", + ), + (End(Paragraph), ""), + ); + test_parse!( + "{%cmt} a", + (Start(Paragraph, Attributes::new()), ""), + ( + Attributes([(AttributeKind::Comment, "cmt")].into_iter().collect()), + "{%cmt}", + ), + (Str(" a".into()), " a"), + (End(Paragraph), ""), ); } @@ -2201,22 +4123,28 @@ mod test { fn list_item_unordered() { test_parse!( "- abc", - Start( - List { + ( + Start( + List { + kind: ListKind::Unordered, + tight: true, + }, + Attributes::new(), + ), + "", + ), + (Start(ListItem, Attributes::new()), "-"), + (Start(Paragraph, Attributes::new()), ""), + (Str("abc".into()), "abc"), + (End(Paragraph), ""), + (End(ListItem), ""), + ( + End(List { kind: ListKind::Unordered, tight: true, - }, - Attributes::new(), - ), - Start(ListItem, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("abc".into()), - End(Paragraph), - End(ListItem), - End(List { - kind: ListKind::Unordered, - tight: true, - }), + }), + "", + ), ); } @@ -2224,30 +4152,36 @@ mod test { fn list_item_ordered_decimal() { test_parse!( "123. abc", - Start( - List { + ( + Start( + List { + kind: ListKind::Ordered { + numbering: Decimal, + style: Period, + start: 123, + }, + tight: true, + }, + Attributes::new(), + ), + "", + ), + (Start(ListItem, Attributes::new()), "123."), + (Start(Paragraph, Attributes::new()), ""), + (Str("abc".into()), "abc"), + (End(Paragraph), ""), + (End(ListItem), ""), + ( + End(List { kind: ListKind::Ordered { numbering: Decimal, style: Period, - start: 123 + start: 123, }, tight: true, - }, - Attributes::new(), - ), - Start(ListItem, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("abc".into()), - End(Paragraph), - End(ListItem), - End(List { - kind: ListKind::Ordered { - numbering: Decimal, - style: Period, - start: 123 - }, - tight: true, - }), + }), + "", + ), ); } @@ -2259,32 +4193,47 @@ mod test { "- [x] b\n", // "- [X] c\n", // ), - Start( - List { + ( + Start( + List { + kind: ListKind::Task, + tight: true, + }, + Attributes::new(), + ), + "", + ), + ( + Start(TaskListItem { checked: false }, Attributes::new()), + "- [ ]", + ), + (Start(Paragraph, Attributes::new()), ""), + (Str("a".into()), "a"), + (End(Paragraph), ""), + (End(TaskListItem { checked: false }), ""), + ( + Start(TaskListItem { checked: true }, Attributes::new()), + "- [x]", + ), + (Start(Paragraph, Attributes::new()), ""), + (Str("b".into()), "b"), + (End(Paragraph), ""), + (End(TaskListItem { checked: true }), ""), + ( + Start(TaskListItem { checked: true }, Attributes::new()), + "- [X]", + ), + (Start(Paragraph, Attributes::new()), ""), + (Str("c".into()), "c"), + (End(Paragraph), ""), + (End(TaskListItem { checked: true }), ""), + ( + End(List { kind: ListKind::Task, tight: true, - }, - Attributes::new(), - ), - Start(TaskListItem { checked: false }, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("a".into()), - End(Paragraph), - End(TaskListItem { checked: false }), - Start(TaskListItem { checked: true }, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("b".into()), - End(Paragraph), - End(TaskListItem { checked: true }), - Start(TaskListItem { checked: true }, Attributes::new()), - Start(Paragraph, Attributes::new()), - Str("c".into()), - End(Paragraph), - End(TaskListItem { checked: true }), - End(List { - kind: ListKind::Task, - tight: true, - }), + }), + "", + ), ); } diff --git a/tests/afl/src/lib.rs b/tests/afl/src/lib.rs index 697ba744..591f46c5 100644 --- a/tests/afl/src/lib.rs +++ b/tests/afl/src/lib.rs @@ -15,17 +15,6 @@ pub fn parse(data: &[u8]) { // no overlap, out of order assert!( last.1.end <= range.start - // block attributes may overlap with start event - || ( - matches!(last.0, jotdown::Event::Blankline) - && ( - matches!( - event, - jotdown::Event::Start(ref cont, ..) if cont.is_block() - ) - || matches!(event, jotdown::Event::ThematicBreak(..)) - ) - ) // caption event is before table rows but src is after || ( matches!( diff --git a/tests/html-ut/skip b/tests/html-ut/skip index 52e70b09..b25d6889 100644 --- a/tests/html-ut/skip +++ b/tests/html-ut/skip @@ -1,6 +1,5 @@ 38d85f9:multi-line block attributes 6c14561:multi-line block attributes -f4f22fc:attribute key class order ae6fc15:bugged left/right quote 168469a:bugged left/right quote 2fa94d1:bugged left/right quote diff --git a/tests/html-ut/ut/attributes.test b/tests/html-ut/ut/attributes.test new file mode 100644 index 00000000..07a64f3a --- /dev/null +++ b/tests/html-ut/ut/attributes.test @@ -0,0 +1,18 @@ +Classes should be concatenated + +``` +word{.a #a class=b #b .c} +. +

word

+``` + +Automatic and explicit classes should be merged correctly + +``` +{.a} +::: b +::: +. +
+
+```