Skip to content

Commit

Permalink
Merge pull request stoically#22 from rs-tml/support-numbers-in-dashed…
Browse files Browse the repository at this point in the history
…-attribute

Feat: add number support in NodeName attribute
  • Loading branch information
vldm authored Jul 18, 2023
2 parents 400f047 + e6fbea4 commit 390fb37
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 86 deletions.
159 changes: 147 additions & 12 deletions src/node/node_name.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,60 @@
use std::{convert::TryFrom, fmt};
use std::{
convert::TryFrom,
fmt::{self, Display},
};

use proc_macro2::Punct;
use syn::{
ext::IdentExt,
parse::{discouraged::Speculative, Parse},
parse::{discouraged::Speculative, Parse, ParseStream, Peek},
punctuated::{Pair, Punctuated},
token::{Brace, Colon, PathSep},
Block, ExprPath, Ident, Path, PathSegment,
token::{Brace, Colon, Dot, PathSep},
Block, ExprPath, Ident, LitInt, Path, PathSegment,
};

use super::{atoms::tokens::Dash, path_to_string};
use crate::{node::parse::block_expr, Error, Parser};
use crate::{node::parse::block_expr, Error};

#[derive(Clone, Debug, syn_derive::Parse, syn_derive::ToTokens)]
pub enum NodeNameFragment {
#[parse(peek = Ident::peek_any)]
Ident(#[parse(Ident::parse_any)] Ident),
#[parse(peek = LitInt)]
Literal(LitInt),
// In case when name contain more than one Punct in series
Empty,
}
impl NodeNameFragment {
fn peek_any(input: ParseStream) -> bool {
input.peek(Ident::peek_any) || input.peek(LitInt)
}
}

impl PartialEq<NodeNameFragment> for NodeNameFragment {
fn eq(&self, other: &NodeNameFragment) -> bool {
match (self, other) {
(NodeNameFragment::Ident(s), NodeNameFragment::Ident(o)) => s == o,
// compare literals by their string representation
// So 0x00 and 0 is would be different literals.
(NodeNameFragment::Literal(s), NodeNameFragment::Literal(o)) => {
s.to_string() == o.to_string()
}
(NodeNameFragment::Empty, NodeNameFragment::Empty) => true,
_ => false,
}
}
}
impl Eq for NodeNameFragment {}

impl Display for NodeNameFragment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NodeNameFragment::Ident(i) => i.fmt(f),
NodeNameFragment::Literal(l) => l.fmt(f),
NodeNameFragment::Empty => Ok(()),
}
}
}

/// Name of the node.
#[derive(Clone, Debug, syn_derive::ToTokens)]
Expand All @@ -19,9 +63,25 @@ pub enum NodeName {
/// be separated by double colons, e.g. `<foo::bar />`.
Path(ExprPath),

///
/// Name separated by punctuation, e.g. `<div data-foo="bar" />` or `<div
/// data:foo="bar" />`.
Punctuated(Punctuated<Ident, Punct>),
///
/// It is fully compatible with SGML (ID/NAME) tokens format.
/// Which is described as follow:
/// ID and NAME tokens must begin with a letter ([A-Za-z]) and may be
/// followed by any number of letters, digits ([0-9]), hyphens ("-"),
/// underscores ("_"), colons (":"), and periods (".").
///
/// Support more than one punctuation in series, in this case
/// `NodeNameFragment::Empty` would be used.
///
/// Note: that punct and `NodeNameFragment` has different `Spans` and IDE
/// (rust-analyzer/idea) can controll them independently.
/// So if one needs to add semantic highlight or go-to definition to entire
/// `NodeName` it should emit helper statements for each `Punct` and
/// `NodeNameFragment` (excludeing `Empty` fragment).
Punctuated(Punctuated<NodeNameFragment, Punct>),

/// Arbitrary rust code in braced `{}` blocks.
Block(Block),
Expand Down Expand Up @@ -68,6 +128,75 @@ impl NodeName {
_ => false,
}
}

/// Parse the stream as punctuated idents.
///
/// We can't replace this with [`Punctuated::parse_separated_nonempty`]
/// since that doesn't support reserved keywords. Might be worth to
/// consider a PR upstream.
///
/// [`Punctuated::parse_separated_nonempty`]: https://docs.rs/syn/1.0.58/syn/punctuated/struct.Punctuated.html#method.parse_separated_nonempty
pub(crate) fn node_name_punctuated_ident<T: Parse, F: Peek, X: From<Ident>>(
input: ParseStream,
punct: F,
) -> syn::Result<Punctuated<X, T>> {
let fork = &input.fork();
let mut segments = Punctuated::<X, T>::new();

while !fork.is_empty() && fork.peek(Ident::peek_any) {
let ident = Ident::parse_any(fork)?;
segments.push_value(ident.clone().into());

if fork.peek(punct) {
segments.push_punct(fork.parse()?);
} else {
break;
}
}

if segments.len() > 1 {
input.advance_to(fork);
Ok(segments)
} else {
Err(fork.error("expected punctuated node name"))
}
}

/// Parse the stream as punctuated idents, with two possible punctuations
/// available
pub(crate) fn node_name_punctuated_ident_with_two_alternate<
T: Parse,
F: Peek,
G: Peek,
H: Peek,
X: From<NodeNameFragment>,
>(
input: ParseStream,
punct: F,
alternate_punct: G,
alternate_punct2: H,
) -> syn::Result<Punctuated<X, T>> {
let fork = &input.fork();
let mut segments = Punctuated::<X, T>::new();

while !fork.is_empty() && NodeNameFragment::peek_any(fork) {
let ident = NodeNameFragment::parse(fork)?;
segments.push_value(ident.clone().into());

if fork.peek(punct) || fork.peek(alternate_punct) || fork.peek(alternate_punct2) {
segments.push_punct(fork.parse()?);
} else {
break;
}
}

if segments.len() > 1 {
input.advance_to(fork);
Ok(segments)
} else {
Err(fork.error("expected punctuated node name"))
}
}
}

impl TryFrom<&NodeName> for Block {
Expand Down Expand Up @@ -142,8 +271,13 @@ impl fmt::Display for NodeName {

impl Parse for NodeName {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
if input.peek2(PathSep) {
Parser::node_name_punctuated_ident::<PathSep, fn(_) -> PathSep, PathSegment>(
if input.peek(LitInt) {
Err(syn::Error::new(
input.span(),
"Name must start with latin character",
))
} else if input.peek2(PathSep) {
NodeName::node_name_punctuated_ident::<PathSep, fn(_) -> PathSep, PathSegment>(
input, PathSep,
)
.map(|segments| {
Expand All @@ -156,13 +290,14 @@ impl Parse for NodeName {
},
})
})
} else if input.peek2(Colon) || input.peek2(Dash) {
Parser::node_name_punctuated_ident_with_alternate::<
} else if input.peek2(Colon) || input.peek2(Dash) || input.peek2(Dot) {
NodeName::node_name_punctuated_ident_with_two_alternate::<
Punct,
fn(_) -> Colon,
fn(_) -> Dash,
Ident,
>(input, Colon, Dash)
fn(_) -> Dot,
NodeNameFragment,
>(input, Colon, Dash, Dot)
.map(NodeName::Punctuated)
} else if input.peek(Brace) {
let fork = &input.fork();
Expand Down
75 changes: 1 addition & 74 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,7 @@ use std::vec;

use proc_macro2::TokenStream;
use proc_macro2_diagnostics::Diagnostic;
use syn::{
ext::IdentExt,
parse::{discouraged::Speculative, Parse, ParseStream, Peek},
punctuated::Punctuated,
spanned::Spanned,
Ident, Result,
};
use syn::{parse::ParseStream, spanned::Spanned, Result};

pub mod recoverable;

Expand Down Expand Up @@ -110,71 +104,4 @@ impl Parser {
let nodes = if nodes.is_empty() { None } else { Some(nodes) };
ParsingResult::from_parts(nodes, errors)
}

/// Parse the stream as punctuated idents.
///
/// We can't replace this with [`Punctuated::parse_separated_nonempty`]
/// since that doesn't support reserved keywords. Might be worth to
/// consider a PR upstream.
///
/// [`Punctuated::parse_separated_nonempty`]: https://docs.rs/syn/1.0.58/syn/punctuated/struct.Punctuated.html#method.parse_separated_nonempty
pub(crate) fn node_name_punctuated_ident<T: Parse, F: Peek, X: From<Ident>>(
input: ParseStream,
punct: F,
) -> Result<Punctuated<X, T>> {
let fork = &input.fork();
let mut segments = Punctuated::<X, T>::new();

while !fork.is_empty() && fork.peek(Ident::peek_any) {
let ident = Ident::parse_any(fork)?;
segments.push_value(ident.clone().into());

if fork.peek(punct) {
segments.push_punct(fork.parse()?);
} else {
break;
}
}

if segments.len() > 1 {
input.advance_to(fork);
Ok(segments)
} else {
Err(fork.error("expected punctuated node name"))
}
}

/// Parse the stream as punctuated idents, with two possible punctuations
/// available
pub(crate) fn node_name_punctuated_ident_with_alternate<
T: Parse,
F: Peek,
G: Peek,
X: From<Ident>,
>(
input: ParseStream,
punct: F,
alternate_punct: G,
) -> Result<Punctuated<X, T>> {
let fork = &input.fork();
let mut segments = Punctuated::<X, T>::new();

while !fork.is_empty() && fork.peek(Ident::peek_any) {
let ident = Ident::parse_any(fork)?;
segments.push_value(ident.clone().into());

if fork.peek(punct) || fork.peek(alternate_punct) {
segments.push_punct(fork.parse()?);
} else {
break;
}
}

if segments.len() > 1 {
input.advance_to(fork);
Ok(segments)
} else {
Err(fork.error("expected punctuated node name"))
}
}
}
24 changes: 24 additions & 0 deletions tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,30 @@ fn test_dashed_attribute_name() -> Result<()> {
Ok(())
}

#[test]
#[should_panic = "Name must start with latin character"]
fn test_dashed_attribute_name_integers_not_supported_at_beginning() {
let tokens = quote! {
<div 12-foo="bar" />
};

let _ = parse2(tokens).unwrap();
}

#[test]
fn test_dashed_attribute_name_with_long_integer_suffixes() -> Result<()> {
let tokens = quote! {
<div data-14-32px-32mzxksq="bar" />
};

let nodes = parse2(tokens)?;
let attribute = get_element_attribute(&nodes, 0, 0);

assert_eq!(attribute.key.to_string(), "data-14-32px-32mzxksq");

Ok(())
}

#[test]
fn test_coloned_attribute_name() -> Result<()> {
let tokens = quote! {
Expand Down

0 comments on commit 390fb37

Please sign in to comment.