Skip to main content

metadata_gen/
metatags.rs

1//! Meta tag generation and extraction module.
2//!
3//! This module provides functionality for generating HTML meta tags from metadata
4//! and extracting meta tags from HTML content.
5
6use crate::error::MetadataError;
7use scraper::{Html, Selector};
8use std::{collections::HashMap, fmt};
9
10/// Holds collections of meta tags for different platforms and categories.
11///
12/// # Example
13///
14/// ```
15/// use metadata_gen::metatags::generate_metatags;
16/// use std::collections::HashMap;
17///
18/// let mut metadata = HashMap::new();
19/// metadata.insert("description".to_string(), "A sample page".to_string());
20/// metadata.insert("og:title".to_string(), "Sample".to_string());
21///
22/// let tags = generate_metatags(&metadata);
23/// assert!(tags.primary.contains("description"));
24/// assert!(tags.og.contains("og:title"));
25/// ```
26#[derive(Debug, Default, PartialEq, Eq, Hash, Clone)]
27pub struct MetaTagGroups {
28    /// The `apple` meta tags.
29    pub apple: String,
30    /// The primary meta tags.
31    pub primary: String,
32    /// The `og` meta tags.
33    pub og: String,
34    /// The `ms` meta tags.
35    pub ms: String,
36    /// The `twitter` meta tags.
37    pub twitter: String,
38}
39
40/// Represents a single meta tag.
41///
42/// # Example
43///
44/// ```
45/// use metadata_gen::metatags::MetaTag;
46///
47/// let tag = MetaTag {
48///     name: "description".to_string(),
49///     content: "A sample page".to_string(),
50/// };
51/// assert_eq!(tag.name, "description");
52/// ```
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct MetaTag {
55    /// The name or property of the meta tag.
56    pub name: String,
57    /// The content of the meta tag.
58    pub content: String,
59}
60
61impl MetaTagGroups {
62    /// Adds a custom meta tag to the appropriate group.
63    ///
64    /// # Arguments
65    ///
66    /// * `name` - The name of the meta tag.
67    /// * `content` - The content of the meta tag.
68    pub fn add_custom_tag(&mut self, name: &str, content: &str) {
69        let formatted_tag = self.format_meta_tag(name, content);
70
71        // Match based on specific prefixes for Apple, MS, OG, Twitter, etc.
72        if name.starts_with("apple-")
73            || name == "mobile-web-app-capable"
74        {
75            self.apple.push_str(&formatted_tag);
76        } else if name.starts_with("msapplication-") {
77            // println!("Adding MS meta tag: {}", formatted_tag);  // Debugging output
78            self.ms.push_str(&formatted_tag);
79        } else if name.starts_with("og:") {
80            // println!("Adding OG meta tag: {}", formatted_tag);  // Debugging output
81            self.og.push_str(&formatted_tag);
82        } else if name.starts_with("twitter:") {
83            // println!("Adding Twitter meta tag: {}", formatted_tag);  // Debugging output
84            self.twitter.push_str(&formatted_tag);
85        } else {
86            // println!("Adding Primary meta tag: {}", formatted_tag);  // Debugging output
87            self.primary.push_str(&formatted_tag);
88        }
89    }
90
91    /// Formats a single meta tag.
92    ///
93    /// # Arguments
94    ///
95    /// * `name` - The name of the meta tag.
96    /// * `content` - The content of the meta tag.
97    ///
98    /// # Returns
99    ///
100    /// A formatted meta tag string.
101    pub fn format_meta_tag(&self, name: &str, content: &str) -> String {
102        format!(
103            r#"<meta name="{}" content="{}">"#,
104            name,
105            content.replace('"', "&quot;")
106        )
107    }
108
109    /// Generates meta tags for Apple devices.
110    ///
111    /// # Arguments
112    ///
113    /// * `metadata` - A reference to a HashMap containing the metadata.
114    pub fn generate_apple_meta_tags(
115        &mut self,
116        metadata: &HashMap<String, String>,
117    ) {
118        const APPLE_TAGS: [&str; 4] = [
119            "apple-mobile-web-app-capable",
120            "mobile-web-app-capable",
121            "apple-mobile-web-app-status-bar-style",
122            "apple-mobile-web-app-title",
123        ];
124        self.apple = self.generate_tags(metadata, &APPLE_TAGS);
125    }
126
127    /// Generates primary meta tags like `author`, `description`, and `keywords`.
128    ///
129    /// # Arguments
130    ///
131    /// * `metadata` - A reference to a HashMap containing the metadata.
132    pub fn generate_primary_meta_tags(
133        &mut self,
134        metadata: &HashMap<String, String>,
135    ) {
136        const PRIMARY_TAGS: [&str; 4] =
137            ["author", "description", "keywords", "viewport"];
138        self.primary = self.generate_tags(metadata, &PRIMARY_TAGS);
139    }
140
141    /// Generates Open Graph (`og`) meta tags for social media.
142    ///
143    /// # Arguments
144    ///
145    /// * `metadata` - A reference to a HashMap containing the metadata.
146    pub fn generate_og_meta_tags(
147        &mut self,
148        metadata: &HashMap<String, String>,
149    ) {
150        const OG_TAGS: [&str; 5] = [
151            "og:title",
152            "og:description",
153            "og:image",
154            "og:url",
155            "og:type",
156        ];
157        self.og = self.generate_tags(metadata, &OG_TAGS);
158    }
159
160    /// Generates Microsoft-specific meta tags.
161    ///
162    /// # Arguments
163    ///
164    /// * `metadata` - A reference to a HashMap containing the metadata.
165    pub fn generate_ms_meta_tags(
166        &mut self,
167        metadata: &HashMap<String, String>,
168    ) {
169        const MS_TAGS: [&str; 2] =
170            ["msapplication-TileColor", "msapplication-TileImage"];
171        self.ms = self.generate_tags(metadata, &MS_TAGS);
172    }
173
174    /// Generates Twitter meta tags for embedding rich media in tweets.
175    ///
176    /// # Arguments
177    ///
178    /// * `metadata` - A reference to a HashMap containing the metadata.
179    pub fn generate_twitter_meta_tags(
180        &mut self,
181        metadata: &HashMap<String, String>,
182    ) {
183        const TWITTER_TAGS: [&str; 5] = [
184            "twitter:card",
185            "twitter:site",
186            "twitter:title",
187            "twitter:description",
188            "twitter:image",
189        ];
190        self.twitter = self.generate_tags(metadata, &TWITTER_TAGS);
191    }
192
193    /// Generates meta tags based on the provided list of tag names.
194    ///
195    /// # Arguments
196    ///
197    /// * `metadata` - A reference to a `HashMap` containing the metadata.
198    /// * `tags` - A reference to an array of tag names.
199    ///
200    /// # Returns
201    ///
202    /// A string containing the generated meta tags.
203    pub fn generate_tags(
204        &self,
205        metadata: &HashMap<String, String>,
206        tags: &[&str],
207    ) -> String {
208        tags.iter()
209            .filter_map(|&tag| {
210                metadata
211                    .get(tag)
212                    .map(|value| self.format_meta_tag(tag, value))
213            })
214            .collect::<Vec<_>>()
215            .join("\n")
216    }
217}
218
219/// Implement `Display` for `MetaTagGroups`.
220impl fmt::Display for MetaTagGroups {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        write!(
223            f,
224            "{}\n{}\n{}\n{}\n{}",
225            self.apple, self.primary, self.og, self.ms, self.twitter
226        )
227    }
228}
229
230/// Generates HTML meta tags based on the provided metadata.
231///
232/// This function takes metadata from a `HashMap` and generates meta tags for various platforms (e.g., Apple, Open Graph, Twitter).
233///
234/// # Arguments
235///
236/// * `metadata` - A reference to a `HashMap` containing the metadata.
237///
238/// # Returns
239///
240/// A `MetaTagGroups` structure with meta tags grouped by platform.
241pub fn generate_metatags(
242    metadata: &HashMap<String, String>,
243) -> MetaTagGroups {
244    let mut meta_tag_groups = MetaTagGroups::default();
245    meta_tag_groups.generate_apple_meta_tags(metadata);
246    meta_tag_groups.generate_primary_meta_tags(metadata);
247    meta_tag_groups.generate_og_meta_tags(metadata);
248    meta_tag_groups.generate_ms_meta_tags(metadata);
249    meta_tag_groups.generate_twitter_meta_tags(metadata);
250    meta_tag_groups
251}
252
253/// Extracts meta tags from HTML content.
254///
255/// This function parses the given HTML content and extracts all meta tags,
256/// including both `name` and `property` attributes.
257///
258/// # Arguments
259///
260/// * `html_content` - A string slice containing the HTML content to parse.
261///
262/// # Returns
263///
264/// Returns a `Result` containing a `Vec<MetaTag>` if successful, or a `MetadataError` if parsing fails.
265///
266/// # Errors
267///
268/// This function will return a `MetadataError` if:
269/// - The HTML content cannot be parsed.
270/// - The meta tag selector cannot be created.
271pub fn extract_meta_tags(
272    html_content: &str,
273) -> Result<Vec<MetaTag>, MetadataError> {
274    let document = Html::parse_document(html_content);
275
276    let meta_selector = Selector::parse("meta").map_err(|e| {
277        MetadataError::ExtractionError {
278            message: format!(
279                "Failed to create meta tag selector: {}",
280                e
281            ),
282        }
283    })?;
284
285    let mut meta_tags = Vec::new();
286
287    for element in document.select(&meta_selector) {
288        let name = element
289            .value()
290            .attr("name")
291            .or_else(|| element.value().attr("property"))
292            .or_else(|| element.value().attr("http-equiv"));
293
294        let content = element.value().attr("content");
295
296        if let (Some(name), Some(content)) = (name, content) {
297            meta_tags.push(MetaTag {
298                name: name.to_string(),
299                content: content.to_string(),
300            });
301        }
302    }
303
304    Ok(meta_tags)
305}
306
307/// Converts a vector of MetaTags into a HashMap for easier access.
308///
309/// # Arguments
310///
311/// * `meta_tags` - A vector of MetaTag structs.
312///
313/// # Returns
314///
315/// A HashMap where the keys are the meta tag names and the values are the contents.
316pub fn meta_tags_to_hashmap(
317    meta_tags: Vec<MetaTag>,
318) -> HashMap<String, String> {
319    meta_tags
320        .into_iter()
321        .map(|tag| (tag.name, tag.content))
322        .collect()
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    #[test]
330    fn test_generate_metatags() {
331        let mut metadata = HashMap::new();
332        metadata.insert("title".to_string(), "Test Page".to_string());
333        metadata.insert(
334            "description".to_string(),
335            "A test page".to_string(),
336        );
337        metadata
338            .insert("og:title".to_string(), "OG Test Page".to_string());
339
340        let meta_tags = generate_metatags(&metadata);
341
342        assert!(meta_tags.primary.contains("description"));
343        assert!(meta_tags.og.contains("og:title"));
344    }
345
346    #[test]
347    fn test_extract_meta_tags() {
348        let html = r#"
349        <html>
350          <head>
351            <meta name="description" content="A sample page">
352            <meta property="og:title" content="Sample Title">
353            <meta http-equiv="content-type" content="text/html; charset=UTF-8">
354          </head>
355          <body>
356            <p>Some content</p>
357          </body>
358        </html>
359        "#;
360
361        let meta_tags = extract_meta_tags(html).unwrap();
362        assert_eq!(meta_tags.len(), 3);
363        assert!(meta_tags.iter().any(|tag| tag.name == "description"
364            && tag.content == "A sample page"));
365        assert!(meta_tags.iter().any(|tag| tag.name == "og:title"
366            && tag.content == "Sample Title"));
367        assert!(meta_tags.iter().any(|tag| tag.name == "content-type"
368            && tag.content == "text/html; charset=UTF-8"));
369    }
370
371    #[test]
372    fn test_extract_meta_tags_empty_html() {
373        let html = "<html><head></head><body></body></html>";
374        let meta_tags = extract_meta_tags(html).unwrap();
375        assert_eq!(meta_tags.len(), 0);
376    }
377
378    #[test]
379    fn test_meta_tags_to_hashmap() {
380        let meta_tags = vec![
381            MetaTag {
382                name: "description".to_string(),
383                content: "A sample page".to_string(),
384            },
385            MetaTag {
386                name: "og:title".to_string(),
387                content: "Sample Title".to_string(),
388            },
389        ];
390
391        let hashmap = meta_tags_to_hashmap(meta_tags);
392        assert_eq!(hashmap.len(), 2);
393        assert_eq!(
394            hashmap.get("description"),
395            Some(&"A sample page".to_string())
396        );
397        assert_eq!(
398            hashmap.get("og:title"),
399            Some(&"Sample Title".to_string())
400        );
401    }
402
403    #[test]
404    fn test_meta_tag_groups_display() {
405        let groups = MetaTagGroups {
406    apple: "<meta name=\"apple-mobile-web-app-capable\" content=\"yes\">".to_string(),
407    primary: "<meta name=\"description\" content=\"A test page\">".to_string(),
408    og: "<meta property=\"og:title\" content=\"Test Page\">".to_string(),
409    ms: "<meta name=\"msapplication-TileColor\" content=\"#ffffff\">".to_string(),
410    twitter: "<meta name=\"twitter:card\" content=\"summary\">".to_string(),
411};
412
413        let display = groups.to_string();
414        assert!(display.contains("apple-mobile-web-app-capable"));
415        assert!(display.contains("description"));
416        assert!(display.contains("og:title"));
417        assert!(display.contains("msapplication-TileColor"));
418        assert!(display.contains("twitter:card"));
419    }
420
421    #[test]
422    fn test_format_meta_tag() {
423        let groups = MetaTagGroups::default();
424        let tag = groups.format_meta_tag("test", "Test \"Value\"");
425        assert_eq!(
426            tag,
427            r#"<meta name="test" content="Test &quot;Value&quot;">"#
428        );
429    }
430}