Skip to content

Commit e499a37

Browse files
committed
feat: add extension attributes
1 parent b293a34 commit e499a37

9 files changed

Lines changed: 298 additions & 119 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ resolver = "2"
1313
license = "MIT"
1414
readme = "README.md"
1515
repository = "https://github.com/vidhanio/html-node"
16-
version = "0.1.16"
16+
version = "0.1.17"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ A HTML to node macro powered by [rstml](https://github.com/rs-tml/rstml).
66

77
- Text escaping
88
- Pretty-printing
9-
- type-safe elements and attributes
9+
- T``ype-safe elements and attributes
1010
- completely optional, and can be mixed with untyped elements when needed!
1111

1212
## Example

html-node-core/src/typed/mod.rs

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ pub trait TypedElement: Default {
1818
/// Create an element from its attributes.
1919
fn from_attributes(
2020
attributes: Self::Attributes,
21-
data_attributes: Vec<(String, Option<String>)>,
22-
aria_attributes: Vec<(String, Option<String>)>,
21+
other_attributes: Vec<(String, Option<String>)>,
2322
) -> Self;
2423

2524
/// Convert the typed element into an [`Element`].
@@ -57,7 +56,7 @@ macro_rules! typed_elements {
5756
macro_rules! typed_element {
5857
($vis:vis $ElementName:ident $(($name:literal))? $([$AttributeName:ident])? $({ $($attribute:ident $(: $atype:ty)?),* $(,)? })?) => {
5958
$crate::typed_attributes!{
60-
$vis $ElementName $([$vis $AttributeName])? $({
59+
($vis $ElementName) $([$vis $AttributeName])? $({
6160
accesskey,
6261
autocapitalize,
6362
autofocus,
@@ -98,26 +97,23 @@ macro_rules! typed_element {
9897
#[allow(missing_docs)]
9998
$vis struct $ElementName {
10099
$vis attributes: <Self as $crate::typed::TypedElement>::Attributes,
101-
$vis data_attributes: ::std::vec::Vec<(::std::string::String, ::std::option::Option<::std::string::String>)>,
102-
$vis aria_attributes: ::std::vec::Vec<(::std::string::String, ::std::option::Option<::std::string::String>)>,
100+
$vis other_attributes: ::std::vec::Vec<(::std::string::String, ::std::option::Option<::std::string::String>)>,
103101
}
104102

105103
impl $crate::typed::TypedElement for $ElementName {
106104
const NAME: &'static str = $crate::typed_element!(@NAME_STR $ElementName$(($name))?);
107-
type Attributes = $crate::typed_attributes!(@NAME $ElementName $([$AttributeName])?);
105+
type Attributes = $crate::typed_attributes!(@NAME ($ElementName) $([$AttributeName])?);
108106

109107
fn from_attributes(
110108
attributes: Self::Attributes,
111-
data_attributes: ::std::vec::Vec<(::std::string::String, ::std::option::Option<::std::string::String>)>,
112-
aria_attributes: ::std::vec::Vec<(::std::string::String, ::std::option::Option<::std::string::String>)>,
109+
other_attributes: ::std::vec::Vec<(::std::string::String, ::std::option::Option<::std::string::String>)>,
113110
) -> Self {
114-
Self { attributes, data_attributes, aria_attributes }
111+
Self { attributes, other_attributes }
115112
}
116113

117114
fn into_element(mut self, children: ::std::option::Option<::std::vec::Vec<$crate::Node>>) -> $crate::Element {
118115
let mut attributes = $crate::typed::TypedAttributes::into_attributes(self.attributes);
119-
attributes.append(&mut self.data_attributes);
120-
attributes.append(&mut self.aria_attributes);
116+
attributes.append(&mut self.other_attributes);
121117

122118
$crate::Element {
123119
name: Self::NAME.into(),
@@ -139,13 +135,13 @@ macro_rules! typed_element {
139135
#[macro_export]
140136
macro_rules! typed_attributes {
141137
{
142-
$($vise:vis $ElementName:ident)? $([$visa:vis $AttributeName:ident])? {
138+
$(($vise:vis $ElementName:ident))? $([$visa:vis $AttributeName:ident])? {
143139
$($attribute:ident $(: $atype:ty)?),* $(,)?
144140
}
145141
} => {
146-
$crate::typed_attributes!(@STRUCT $($vise $ElementName)? $([$visa $AttributeName])? { $($attribute $(: $atype)?),* });
142+
$crate::typed_attributes!(@STRUCT $(($vise $ElementName))? $([$visa $AttributeName])? { $($attribute $(: $atype)?),* });
147143

148-
impl $crate::typed::TypedAttributes for $crate::typed_attributes!(@NAME $($ElementName)? $([$AttributeName])?) {
144+
impl $crate::typed::TypedAttributes for $crate::typed_attributes!(@NAME $(($ElementName))? $([$AttributeName])?) {
149145
fn into_attributes(self) -> ::std::vec::Vec<(::std::string::String, ::std::option::Option<::std::string::String>)> {
150146
[$((::std::stringify!($attribute), self.$attribute.map(|opt| opt.map(|a| ::std::string::ToString::to_string(&a))))),*]
151147
.into_iter()
@@ -156,15 +152,15 @@ macro_rules! typed_attributes {
156152
}
157153
}
158154
};
159-
($($_vise:vis $_ElementName:ident)? $([$_visa:vis $_AttributeName:ident])?) => {};
160-
(@NAME $ElementName:ident) => {
155+
(($_vise:vis $_ElementName:ident) $([$_visa:vis $_AttributeName:ident])?) => {};
156+
(@NAME ($ElementName:ident)) => {
161157
$crate::typed::paste!([< $ElementName:camel Attributes >])
162158
};
163-
(@NAME $ElementName:ident [$AttributeName:ident]) => {
159+
(@NAME $(($ElementName:ident))? [$AttributeName:ident]) => {
164160
$AttributeName
165161
};
166162
{
167-
@STRUCT $vis:vis $ElementName:ident {
163+
@STRUCT ($vis:vis $ElementName:ident) {
168164
$($attribute:ident $(:$atype:ty)?),* $(,)?
169165
}
170166
} => {
@@ -177,7 +173,7 @@ macro_rules! typed_attributes {
177173
}
178174
};
179175
{
180-
@STRUCT $_vis:vis $ElementName:ident [$vis:vis $AttributeName:ident] {
176+
@STRUCT $(($_vis:vis $ElementName:ident))? [$vis:vis $AttributeName:ident] {
181177
$($attribute:ident $(: $atype:ty)?),* $(,)?
182178
}
183179
} => {

html-node-macro/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ proc-macro2-diagnostics = { version = "0.10", default-features = false }
2424
quote = "1"
2525
rstml = { version = "0.11", default-features = false }
2626
syn = "2"
27+
syn_derive = { version = "0.1", optional = true }
2728

2829
[features]
29-
typed = []
30+
typed = ["dep:syn_derive"]

html-node-macro/src/lib.rs

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,98 @@
77

88
mod node_handlers;
99

10-
use std::collections::HashSet;
10+
use std::collections::{HashMap, HashSet};
1111

1212
use node_handlers::{
1313
handle_block, handle_comment, handle_doctype, handle_element, handle_fragment, handle_raw_text,
1414
handle_text,
1515
};
1616
use proc_macro::TokenStream;
17-
use proc_macro2::TokenStream as TokenStream2;
17+
use proc_macro2::{Ident, TokenStream as TokenStream2};
1818
use proc_macro2_diagnostics::Diagnostic;
1919
use quote::quote;
2020
use rstml::{node::Node, Parser, ParserConfig};
21+
use syn::Type;
2122

2223
#[proc_macro]
2324
pub fn html(tokens: TokenStream) -> TokenStream {
24-
html_inner(tokens, false)
25+
html_inner(tokens.into(), None)
2526
}
2627

2728
#[cfg(feature = "typed")]
2829
#[proc_macro]
2930
pub fn typed_html(tokens: TokenStream) -> TokenStream {
30-
html_inner(tokens, true)
31+
use syn::{punctuated::Punctuated, token::Paren, Token};
32+
33+
#[derive(syn_derive::Parse)]
34+
struct ColonAndType {
35+
_colon_token: syn::Token![:],
36+
ty: Type,
37+
}
38+
39+
#[derive(syn_derive::Parse)]
40+
enum MaybeColonAndType {
41+
#[parse(peek = Token![:])]
42+
ColonAndType(ColonAndType),
43+
Nothing,
44+
}
45+
46+
#[derive(syn_derive::Parse)]
47+
struct Extension {
48+
prefix: Ident,
49+
colon_and_type: MaybeColonAndType,
50+
}
51+
52+
#[derive(syn_derive::Parse)]
53+
struct Extensions {
54+
#[syn(parenthesized)]
55+
#[allow(dead_code)]
56+
paren_token: Paren,
57+
58+
#[syn(in = paren_token)]
59+
#[parse(Punctuated::parse_terminated)]
60+
extensions: Punctuated<Extension, syn::Token![,]>,
61+
}
62+
63+
#[derive(syn_derive::Parse)]
64+
enum MaybeExtensions {
65+
#[parse(peek = Paren)]
66+
Extensions(Extensions),
67+
Nothing,
68+
}
69+
70+
#[derive(syn_derive::Parse)]
71+
struct TypedHtmlOptions {
72+
extensions: MaybeExtensions,
73+
tokens: TokenStream2,
74+
}
75+
76+
let options = syn::parse_macro_input!(tokens as TypedHtmlOptions);
77+
78+
let mut extensions = match options.extensions {
79+
MaybeExtensions::Extensions(extensions) => extensions
80+
.extensions
81+
.into_iter()
82+
.map(|extension| match extension.colon_and_type {
83+
MaybeColonAndType::ColonAndType(ColonAndType { ty, .. }) => {
84+
(extension.prefix, Some(ty))
85+
}
86+
MaybeColonAndType::Nothing => (extension.prefix, None),
87+
})
88+
.collect::<HashMap<_, _>>(),
89+
MaybeExtensions::Nothing => HashMap::new(),
90+
};
91+
92+
extensions.insert(Ident::new("data", proc_macro2::Span::call_site()), None);
93+
extensions.insert(Ident::new("aria", proc_macro2::Span::call_site()), None);
94+
95+
html_inner(options.tokens, Some(&extensions))
3196
}
3297

33-
fn html_inner(tokens: TokenStream, typed: bool) -> TokenStream {
98+
fn html_inner(
99+
tokens: TokenStream2,
100+
extensions: Option<&HashMap<Ident, Option<Type>>>,
101+
) -> TokenStream {
34102
// from: https://html.spec.whatwg.org/dev/syntax.html#void-elements
35103
let void_elements = [
36104
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source",
@@ -50,7 +118,7 @@ fn html_inner(tokens: TokenStream, typed: bool) -> TokenStream {
50118
let parser = Parser::new(config);
51119
let (parsed_nodes, parsing_diagnostics) = parser.parse_recoverable(tokens).split_vec();
52120
let (tokenized_nodes, tokenization_diagnostics) =
53-
tokenize_nodes(&void_elements, typed, &parsed_nodes);
121+
tokenize_nodes(&void_elements, extensions, &parsed_nodes);
54122

55123
let node = match &*tokenized_nodes {
56124
[node] => quote!(#node),
@@ -81,16 +149,16 @@ fn html_inner(tokens: TokenStream, typed: bool) -> TokenStream {
81149

82150
fn tokenize_nodes(
83151
void_elements: &HashSet<&str>,
84-
typed: bool,
152+
extensions: Option<&HashMap<Ident, Option<Type>>>,
85153
nodes: &[Node],
86154
) -> (Vec<TokenStream2>, Vec<Diagnostic>) {
87155
let (token_streams, diagnostics) = nodes
88156
.iter()
89157
.map(|node| match node {
90158
Node::Comment(comment) => (handle_comment(comment), vec![]),
91159
Node::Doctype(doctype) => (handle_doctype(doctype), vec![]),
92-
Node::Fragment(fragment) => handle_fragment(void_elements, typed, fragment),
93-
Node::Element(element) => handle_element(void_elements, typed, element),
160+
Node::Fragment(fragment) => handle_fragment(void_elements, extensions, fragment),
161+
Node::Element(element) => handle_element(void_elements, extensions, element),
94162
Node::Block(block) => (handle_block(block), vec![]),
95163
Node::Text(text) => (handle_text(text), vec![]),
96164
Node::RawText(text) => (handle_raw_text(text), vec![]),

html-node-macro/src/node_handlers/mod.rs

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
#[cfg(feature = "typed")]
22
mod typed;
33

4-
use std::collections::HashSet;
4+
use std::collections::{HashMap, HashSet};
55

6-
use proc_macro2::{Literal, TokenStream as TokenStream2};
6+
use proc_macro2::{Ident, Literal, TokenStream as TokenStream2};
77
use proc_macro2_diagnostics::{Diagnostic, SpanDiagnosticExt};
88
use quote::{quote, ToTokens};
99
use rstml::node::{
1010
KeyedAttribute, NodeAttribute, NodeBlock, NodeComment, NodeDoctype, NodeElement, NodeFragment,
1111
NodeName, NodeText, RawText,
1212
};
13-
use syn::spanned::Spanned;
13+
use syn::{spanned::Spanned, Type};
1414

1515
use crate::tokenize_nodes;
1616

@@ -40,10 +40,11 @@ pub fn handle_doctype(doctype: &NodeDoctype) -> TokenStream2 {
4040

4141
pub fn handle_fragment(
4242
void_elements: &HashSet<&str>,
43-
typed: bool,
43+
extensions: Option<&HashMap<Ident, Option<Type>>>,
4444
fragment: &NodeFragment,
4545
) -> (TokenStream2, Vec<Diagnostic>) {
46-
let (inner_nodes, inner_diagnostics) = tokenize_nodes(void_elements, typed, &fragment.children);
46+
let (inner_nodes, inner_diagnostics) =
47+
tokenize_nodes(void_elements, extensions, &fragment.children);
4748

4849
let children = quote!(::std::vec![#(#inner_nodes),*]);
4950

@@ -61,14 +62,13 @@ pub fn handle_fragment(
6162

6263
pub fn handle_element(
6364
void_elements: &HashSet<&str>,
64-
typed: bool,
65+
extensions: Option<&HashMap<Ident, Option<Type>>>,
6566
element: &NodeElement,
6667
) -> (TokenStream2, Vec<Diagnostic>) {
67-
if typed {
68-
typed::handle_element(void_elements, element)
69-
} else {
70-
handle_element_untyped(void_elements, element)
71-
}
68+
extensions.map_or_else(
69+
|| handle_element_untyped(void_elements, element),
70+
|extensions| typed::handle_element(void_elements, extensions, element),
71+
)
7272
}
7373

7474
pub fn handle_element_untyped(
@@ -124,17 +124,17 @@ pub fn handle_element_untyped(
124124
}
125125
},
126126
void_elements,
127-
false,
127+
None,
128128
element,
129129
)
130130
}
131131

132132
fn handle_element_inner<T>(
133-
handle_block: fn(&NodeBlock) -> (T, Option<Diagnostic>),
134-
handle_keyed: fn(&KeyedAttribute) -> (T, Option<Diagnostic>),
135-
to_element: fn(&NodeElement, Vec<T>, TokenStream2) -> TokenStream2,
133+
handle_block: impl Fn(&NodeBlock) -> (T, Option<Diagnostic>),
134+
handle_keyed: impl Fn(&KeyedAttribute) -> (T, Option<Diagnostic>),
135+
to_element: impl Fn(&NodeElement, Vec<T>, TokenStream2) -> TokenStream2,
136136
void_elements: &HashSet<&str>,
137-
typed: bool,
137+
extensions: Option<&HashMap<Ident, Option<Type>>>,
138138
element: &NodeElement,
139139
) -> (TokenStream2, Vec<Diagnostic>) {
140140
let (attributes, attribute_diagnostics) = element
@@ -160,7 +160,7 @@ fn handle_element_inner<T>(
160160
(quote!(::std::option::Option::None), diagnostic)
161161
} else {
162162
let (inner_nodes, inner_diagnostics) =
163-
tokenize_nodes(void_elements, typed, &element.children);
163+
tokenize_nodes(void_elements, extensions, &element.children);
164164

165165
(
166166
quote!(::std::option::Option::Some(::std::vec![#(#inner_nodes),*])),
@@ -224,11 +224,16 @@ fn node_name_to_literal(node_name: &NodeName) -> TokenStream2 {
224224

225225
#[cfg(not(feature = "typed"))]
226226
mod typed {
227-
use std::collections::HashSet;
227+
use std::collections::{HashMap, HashSet};
228228

229229
use rstml::node::NodeElement;
230+
use syn::{Ident, Type};
230231

231-
pub fn handle_element(_void_elements: &HashSet<&str>, _element: &NodeElement) -> ! {
232+
pub fn handle_element(
233+
_void_elements: &HashSet<&str>,
234+
_extensions: &HashMap<Ident, Option<Type>>,
235+
_element: &NodeElement,
236+
) -> ! {
232237
unreachable!("`typed::handle_element` should be unreachable without the `typed` feature")
233238
}
234239
}

0 commit comments

Comments
 (0)