1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
use std::fmt;

use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::ext::IdentExt as _;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;

mod kw {
    syn::custom_keyword!(DOCTYPE);
    syn::custom_keyword!(html);
}

/// Represents an identifier separated by dashes, e.g., `foo-bar-baz`.
struct DashIdent(Punctuated<syn::Ident, syn::Token![-]>);

impl fmt::Display for DashIdent {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Iterate through the punctuated identifiers and format them with
        // dashes in between.
        for pair in self.0.pairs() {
            std::write!(f, "{}", pair.value())?;
            // If there's a punctuation, it's a dash, so we append it.
            if pair.punct().is_some() {
                std::write!(f, "-")?;
            }
        }
        Ok(())
    }
}

impl Parse for DashIdent {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        // Parse a non-empty sequence of identifiers separated by dashes.
        Ok(Self(Punctuated::<syn::Ident, syn::Token![-]>::parse_separated_nonempty_with(input, syn::Ident::parse_any)?))
    }
}

/// Represents the DOCTYPE declaration in HTML, e.g., `<!DOCTYPE html>`.
struct Doctype;

impl Parse for Doctype {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        // Parse each component of the DOCTYPE declaration.
        input.parse::<syn::Token![<]>()?;
        input.parse::<syn::Token![!]>()?;
        input.parse::<kw::DOCTYPE>()?;
        input.parse::<kw::html>()?;
        input.parse::<syn::Token![>]>()?;

        Ok(Self)
    }
}

impl fmt::Display for Doctype {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        std::write!(f, "<!DOCTYPE html>")
    }
}

#[derive(Clone)]
enum Value {
    /// A text literal, such as "Hello, World!"
    Text(syn::LitStr),

    /// A 'braced' value, such as `{1 + 1}`.
    Braced(syn::Expr),
}

impl fmt::Display for Value {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        std::write!(f, "{{}}")
    }
}

impl Parse for Value {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        if input.peek(syn::LitStr) {
            Ok(Self::Text(input.parse()?))
        } else {
            // Parse braced expressions.
            let expr;
            syn::braced!(expr in input);
            Ok(Self::Braced(expr.parse()?))
        }
    }
}

impl ToTokens for Value {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        match self {
            Value::Text(lit) => lit.to_tokens(tokens),
            Value::Braced(expr) => expr.to_tokens(tokens),
        }
    }
}

/// Represents an HTML attribute, consisting of a name-value pair.
struct Attribute {
    name: DashIdent,
    value: Value,
}

impl Parse for Attribute {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let name = input.parse()?;
        input.parse::<syn::Token![=]>()?;
        let value = input.parse()?;

        Ok(Self { name, value })
    }
}

/// Represents an HTML tag, which can be either a start tag with attributes and
/// optional self-closing flag, or an end tag.
enum Tag {
    Start {
        name: DashIdent,
        attributes: Vec<Attribute>,
        self_closing: bool,
    },
    End {
        name: DashIdent,
    },
}

impl fmt::Display for Tag {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Tag::Start {
                name,
                attributes,
                self_closing,
            } => {
                std::write!(f, "<{}", name)?;
                for attr in attributes {
                    std::write!(f, " {}=\"{}\"", attr.name, attr.value)?;
                }
                if *self_closing {
                    std::write!(f, "/")?;
                }
                std::write!(f, ">")
            }
            Tag::End { name } => std::write!(f, "</{}>", name),
        }
    }
}

impl Parse for Tag {
    /// Parses an HTML tag, which could be a start tag with attributes and an
    /// optional self-closing indicator, or an end tag.
    fn parse(input: ParseStream) -> syn::Result<Self> {
        // Parse the opening angle bracket
        input.parse::<syn::Token![<]>()?;

        // Check if it's an end tag
        let forward_slash = input.peek(syn::Token![/]);
        if forward_slash {
            input.parse::<syn::Token![/]>()?;

            // Parse the tag name
            let name: DashIdent = input.parse()?;

            // Parse the closing '>'
            input.parse::<syn::Token![>]>()?;

            // Return the end tag variant
            return Ok(Tag::End { name });
        }

        // Parse the tag name for start tag
        let name: DashIdent = input.parse()?;

        // Parse attributes if any
        let mut attributes = Vec::new();
        while !(input.peek(syn::Token![>])
            || (input.peek(syn::Token![/]) && input.peek2(syn::Token![>])))
        {
            attributes.push(input.parse()?);
        }

        // Check for self-closing tag
        let self_closing = input.peek(syn::Token![/]);
        if self_closing {
            // Consume '/>'
            input.parse::<syn::Token![/]>()?;
            input.parse::<syn::Token![>]>()?;
        } else {
            // Consume '>'
            input.parse::<syn::Token![>]>()?;
        }

        // Return the start tag variant
        Ok(Tag::Start {
            name,
            attributes,
            self_closing,
        })
    }
}

enum Segment {
    /// Represents an HTML `<!DOCTYPE html>` declaration.
    Doctype(Doctype),

    /// Represents an HTML tag, which may be a start tag, an end tag or a
    /// self-closing tag.
    Tag(Tag),

    /// Represents text or interpolated values within an HTML document.
    Value(Value),
}

impl fmt::Display for Segment {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Segment::Doctype(doctype) => doctype.fmt(f),
            Segment::Tag(tag) => tag.fmt(f),
            Segment::Value(value) => value.fmt(f),
        }
    }
}

impl Parse for Segment {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        if input.peek(syn::Token![<])
            && input.peek2(syn::Token![!])
            && input.peek3(kw::DOCTYPE)
        {
            Ok(Segment::Doctype(input.parse()?))
        } else if input.peek(syn::Token![<]) {
            Ok(Segment::Tag(input.parse()?))
        } else if input.peek(syn::LitStr) || input.peek(syn::token::Brace) {
            Ok(Segment::Value(input.parse()?))
        } else {
            Err(input.error("unexpected token"))
        }
    }
}

struct Template {
    // Contains the segments of the HTML template.
    segments: Vec<Segment>,

    /// Stores values that are interpolated into the template.
    values: Vec<Value>,
}

impl fmt::Display for Template {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for segment in &self.segments {
            segment.fmt(f)?;
        }
        Ok(())
    }
}

impl Parse for Template {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let mut segments = vec![];
        let mut values = vec![];

        while !input.is_empty() {
            let segment = input.parse::<Segment>()?;

            if let Segment::Tag(Tag::Start { attributes, .. }) = &segment {
                for attr in attributes {
                    values.push(attr.value.clone());
                }
            }

            if let Segment::Value(value) = &segment {
                values.push(value.clone());
            }

            segments.push(segment);
        }

        Ok(Self { segments, values })
    }
}

struct WriteInput {
    formatter: syn::Expr,
    template: Template,
}

impl Parse for WriteInput {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let formatter = input.parse()?;
        input.parse::<syn::Token![,]>()?;
        let template = input.parse()?;

        Ok(Self {
            formatter,
            template,
        })
    }
}

/// Writes HTML content with embedded Rust expressions into a buffer.
///
/// This macro simplifies writing HTML content by allowing Rust expressions to
/// be embedded directly within HTML tags. It uses `std::write!` internally,
/// thereby supporting all formatting capabilities and constraints of
/// `std::write!`.
///
/// ## Syntax
///
/// The `fhtml::write!` macro syntax closely resembles regular HTML, but allows
/// Rust expressions to be inserted within curly braces `{}`. These expressions
/// are evaluated and their results are inserted into the HTML output at the
/// corresponding location.
///
/// ## Examples
///
/// Basic usage:
///
/// ```rust
/// use std::fmt::Write;
///
/// let mut buffer = String::new();
/// fhtml_macros::write!(buffer, <div>{1 + 1}</div>);
/// // This expands to:
/// // std::write!(buffer, "<div>{}</div>", 1 + 1);
///
/// assert_eq!(buffer, "<div>2</div>");
/// ```
///
/// This example demonstrates how to use `fhtml::write!` within an
/// implementation of `std::fmt::Display`:
///
/// ```rust
/// use std::fmt;
/// use std::fmt::Write;
///
/// struct Heading {
///     title: String,
/// }
///
/// impl fmt::Display for Heading {
///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
///         fhtml_macros::write!(f, <h1>{self.title}</h1>)
///     }
/// }
///
/// let heading = Heading { title: "Hello, World!".into() }.to_string();
/// assert_eq!(heading, "<h1>Hello, World!</h1>");
/// ```
///
/// ## Notes
///
/// - The macro outputs HTML directly into the provided buffer.
/// - Similar to `std::write!`, errors during writing are handled by returning a
///   `Result` indicating either success (`Ok`) or an error (`Err`).
/// - Ensure proper escaping or sanitization of user-generated content to
///   prevent injection attacks when outputting HTML.
#[proc_macro]
pub fn write(input: TokenStream) -> TokenStream {
    let WriteInput {
        formatter,
        template,
    } = syn::parse_macro_input!(input as WriteInput);

    let values = &template.values;
    let template = template.to_string();

    let output = quote! {
        ::std::write!(#formatter, #template, #(#values),*)
    };

    output.into()
}

#[proc_macro]
pub fn formatcp(input: TokenStream) -> TokenStream {
    let template = syn::parse_macro_input!(input as Template);

    let values = &template.values;
    let template = template.to_string();

    let output = quote! {
        ::fhtml::const_format::formatcp!(#template, #(#values),*)
    };

    output.into()
}