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
//! This crate offers convenience macros for [gvdb](https://!github.com/felinira/gvdb-rs).
//! The macros are [`include_gresource_from_xml!()`] and
//! [`include_gresource_from_dir!()`]
//!
//! ## Examples
//!
//! Compile a GResource XML file and include the bytes in the file.
//!
//! ```
//! use gvdb_macros::include_gresource_from_xml;
//! static GRESOURCE_BYTES: &[u8] = include_gresource_from_xml!("test-data/gresource/test3.gresource.xml");
//! ```
//!
//! Scan a directory and create a GResource file with all the contents of the directory.
//!
//! ```
//! use gvdb_macros::include_gresource_from_dir;
//! static GRESOURCE_BYTES: &[u8] = include_gresource_from_dir!("/gvdb/rs/test", "test-data/gresource");
//! ```

#![warn(missing_docs)]
#![doc = include_str!("../README.md")]

extern crate proc_macro;

use litrs::{Literal, StringLit};
use proc_macro2::TokenTree;
use quote::quote;
use std::path::PathBuf;

fn quote_bytes(bytes: &[u8]) -> proc_macro2::TokenStream {
    let bytes_lit = proc_macro2::Literal::byte_string(bytes);

    quote! {
        {{
            #[repr(align(16))]
            #[doc(hidden)]
            struct __GVDB_Aligned<T: ?Sized>(T);
            #[doc(hidden)]
            static __GVDB_DATA: &'static __GVDB_Aligned<[u8]> = &__GVDB_Aligned(*#bytes_lit);

            &__GVDB_DATA.0
        }}
    }
}

fn include_gresource_from_xml_with_filename(filename: &str) -> proc_macro2::TokenStream {
    let path = PathBuf::from(filename);
    let xml = gvdb::gresource::GResourceXMLDocument::from_file(&path).unwrap();
    let builder = gvdb::gresource::GResourceBuilder::from_xml(xml).unwrap();
    let data = builder.build().unwrap();

    quote_bytes(&data)
}

fn include_gresource_from_xml_inner(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
    let mut iter = input.into_iter();

    let first = iter
        .next()
        .expect("Expected exactly one string literal argument (gresource file location)");
    let second = iter.next();
    if let Some(second) = second {
        panic!("Unexpected token '{}', expected exactly one string literal argument (gresource file location)", second)
    }

    match Literal::try_from(first) {
        Err(e) => proc_macro2::TokenStream::from(e.to_compile_error()),
        Ok(Literal::String(str)) => {
            include_gresource_from_xml_with_filename(str.value())
        }
        Ok(other) => panic!("Unexpected token '{:?}', expected exactly one string literal argument (gresource file location)", other)
    }
}

/// Compile a GResource XML file to its binary representation and include it in the source file.
///
/// ```
/// use gvdb_macros::include_gresource_from_xml;
/// static GRESOURCE_BYTES: &[u8] = include_gresource_from_xml!("test-data/gresource/test3.gresource.xml");
/// ```
#[proc_macro]
pub fn include_gresource_from_xml(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = proc_macro2::TokenStream::from(input);
    let output = include_gresource_from_xml_inner(input);
    proc_macro::TokenStream::from(output)
}

fn include_gresource_from_dir_str(prefix: &str, directory: &str) -> proc_macro2::TokenStream {
    let path = PathBuf::from(directory);
    let builder =
        gvdb::gresource::GResourceBuilder::from_directory(prefix, &path, true, true).unwrap();
    let data = builder.build().unwrap();

    quote_bytes(&data)
}

fn include_gresource_from_dir_inner(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
    let err_msg = "expected exactly two string literal arguments (prefix, gresource directory)";
    let (prefix, directory) = match &*input.into_iter().collect::<Vec<_>>() {
        [TokenTree::Literal(str1), TokenTree::Punct(comma), TokenTree::Literal(str2)] => {
            if comma.as_char() != ',' {
                panic!("{}", err_msg);
            }

            (
                StringLit::try_from(str1).expect(err_msg),
                StringLit::try_from(str2).expect(err_msg),
            )
        }
        _ => panic!("{}", err_msg),
    };

    include_gresource_from_dir_str(prefix.value(), directory.value())
}

/// Scan a directory and create a GResource file with all the contents of the directory.
///
/// This will ignore any files that end with gresource.xml and meson.build, as
/// those are most likely not needed inside the GResource.
///
/// This is equivalent to the following XML:
///
/// ```xml
/// <gresources>
///   <gresource prefix="`prefix`">
///     <!-- file entries for each file with path beginning from `directory` as root -->
///   </gresource>
/// </gresources>
/// ```
///
/// The first argument to this macro is the prefix for the GResource file. The second argument is
/// the path to the folder containing the files to include in the file.
///
/// This acts as if every xml file uses the option `xml-stripblanks` in the GResource XML and every
/// JSON file uses `json-stripblanks`.
///
/// JSON files are all files with the extension '.json'.
/// XML files are all files with the extensions '.xml', '.ui', '.svg'
///
/// All files that end with `.ui` and `.css` are compressed.
/// ```
/// use gvdb_macros::include_gresource_from_dir;
/// static GRESOURCE_BYTES: &[u8] = include_gresource_from_dir!("/gvdb/rs/tests/data", "test-data/gresource");
/// ```
#[proc_macro]
pub fn include_gresource_from_dir(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let input = proc_macro2::TokenStream::from(input);
    let output = include_gresource_from_dir_inner(input);
    proc_macro::TokenStream::from(output)
}

#[cfg(test)]
mod tests {
    use super::*;
    use quote::quote;

    #[test]
    fn include_gresource_from_xml() {
        let tokens =
            include_gresource_from_xml_inner(quote! {"test-data/gresource/test3.gresource.xml"});
        assert!(tokens.to_string().contains(r#"b"GVariant"#));
    }

    #[test]
    #[should_panic]
    fn include_gresource_from_xml_panic() {
        include_gresource_from_xml_inner(quote! {4});
    }

    #[test]
    #[should_panic]
    fn include_gresource_from_xml_panic2() {
        include_gresource_from_xml_inner(quote! { "test", 4 });
    }

    #[test]
    #[should_panic]
    fn include_gresource_from_xml_panic3() {
        include_gresource_from_xml_inner(quote! { test });
    }

    #[test]
    fn include_gresource_from_dir() {
        let tokens =
            include_gresource_from_dir_inner(quote! {"/gvdb/rs/test", "test-data/gresource"});
        assert!(tokens.to_string().contains(r#"b"GVariant"#));
    }

    #[test]
    #[should_panic]
    fn include_gresource_from_dir_panic1() {
        include_gresource_from_dir_inner(quote! {"/gvdb/rs/test",});
    }

    #[test]
    #[should_panic]
    fn include_gresource_from_dir_panic2() {
        include_gresource_from_dir_inner(quote! {"/gvdb/rs/test"});
    }

    #[test]
    #[should_panic]
    fn include_gresource_from_dir_panic3() {
        include_gresource_from_dir_inner(quote! {"/gvdb/rs/test","bla","bla"});
    }

    #[test]
    #[should_panic]
    fn include_gresource_from_dir_panic4() {
        include_gresource_from_dir_inner(quote! {"/gvdb/rs/test","INVALID_DIRECTORY"});
    }

    #[test]
    #[should_panic]
    fn include_gresource_from_dir_panic5() {
        include_gresource_from_dir_inner(quote! {"/gvdb/rs/test"."test-data/gresource"});
    }
}