Repository: Y2Z/monolith (rust)

Issue:
Missing stylesheet when rel="preload stylesheet"
Some websites (for example https://wonderfall.dev/ or https://privsec.dev/, both by the same perso) have `rel=preload stylesheet"` instead of `rel="stylesheet"` to link to external CSS.

```html
<link crossorigin="anonymous" href="/assets/css/stylesheet.5d45b8bd1a3cf526e72959d51f1bdc688d8e97fa0df2a697a93df6bdc746feb4.css" integrity="sha256-XUW4vRo89SbnKVnVHxvcaI2Ol/oN8qaXqT32vcdG/rQ=" rel="preload stylesheet" as="style">
```

According to [this Stack Overflow thread](https://stackoverflow.com/questions/1878657/multiple-values-for-rel-attribute) and [the HTML specs](https://www.w3.org/TR/html401/struct/links.html#adef-rel), it is a valid syntax. 

By quickly looking at the monolith codebase, the issue probably comes from [here](https://github.com/Y2Z/monolith/blob/master/src/html.rs#L150). I'm not sure what the best approach would be to fix this bug and potentially similar-looking ones in the future. 

Pull request:
Add support for parsing multiple link type (rel) attribute values
Closes #420 

Patch:
diff --git a/src/html.rs b/src/html.rs
index 6a80626..2eb2e9d 100644
--- a/src/html.rs
+++ b/src/html.rs
@@ -23,6 +23,15 @@ use crate::url::{
 };
 use crate::utils::{parse_content_type, retrieve_asset};
 
+#[derive(PartialEq, Eq)]
+pub enum LinkType {
+    Alternate,
+    DnsPrefetch,
+    Icon,
+    Preload,
+    Stylesheet,
+}
+
 struct SrcSetItem<'a> {
     path: &'a str,
     descriptor: &'a str,
@@ -141,26 +150,6 @@ pub fn create_metadata_tag(url: &Url) -> String {
     )
 }
 
-pub fn determine_link_node_type(node: &Handle) -> &str {
-    let mut link_type: &str = "unknown";
-
-    if let Some(link_attr_rel_value) = get_node_attr(node, "rel") {
-        if is_icon(&link_attr_rel_value) {
-            link_type = "icon";
-        } else if link_attr_rel_value.eq_ignore_ascii_case("stylesheet")
-            || link_attr_rel_value.eq_ignore_ascii_case("alternate stylesheet")
-        {
-            link_type = "stylesheet";
-        } else if link_attr_rel_value.eq_ignore_ascii_case("preload") {
-            link_type = "preload";
-        } else if link_attr_rel_value.eq_ignore_ascii_case("dns-prefetch") {
-            link_type = "dns-prefetch";
-        }
-    }
-
-    link_type
-}
-
 pub fn embed_srcset(
     cache: &mut HashMap<String, Vec<u8>>,
     client: &Client,
@@ -454,6 +443,26 @@ pub fn is_icon(attr_value: &str) -> bool {
     ICON_VALUES.contains(&attr_value.to_lowercase().as_str())
 }
 
+pub fn parse_link_type(link_attr_rel_value: &str) -> Vec<LinkType> {
+    let mut types: Vec<LinkType> = vec![];
+
+    for link_attr_rel_type in link_attr_rel_value.split_whitespace() {
+        if link_attr_rel_type.eq_ignore_ascii_case("alternate") {
+            types.push(LinkType::Alternate);
+        } else if link_attr_rel_type.eq_ignore_ascii_case("dns-prefetch") {
+            types.push(LinkType::DnsPrefetch);
+        } else if link_attr_rel_type.eq_ignore_ascii_case("preload") {
+            types.push(LinkType::Preload);
+        } else if link_attr_rel_type.eq_ignore_ascii_case("stylesheet") {
+            types.push(LinkType::Stylesheet);
+        } else if is_icon(&link_attr_rel_type) {
+            types.push(LinkType::Icon);
+        }
+    }
+
+    types
+}
+
 pub fn set_base_url(document: &Handle, desired_base_href: String) -> RcDom {
     let mut buf: Vec<u8> = Vec::new();
     serialize(
@@ -665,7 +674,10 @@ pub fn retrieve_and_embed_asset(
                     s = String::from_utf8_lossy(&data).to_string();
                 }
 
-                if node_name == "link" && determine_link_node_type(node) == "stylesheet" {
+                if node_name == "link"
+                    && parse_link_type(&get_node_attr(node, "rel").unwrap_or(String::from("")))
+                        .contains(&LinkType::Stylesheet)
+                {
                     // Stylesheet LINK elements require special treatment
                     let css: String = embed_css(cache, client, &final_url, &s, options);
 
@@ -757,9 +769,10 @@ pub fn walk_and_embed_assets(
                     }
                 }
                 "link" => {
-                    let link_type: &str = determine_link_node_type(node);
+                    let link_node_types: Vec<LinkType> =
+                        parse_link_type(&get_node_attr(node, "rel").unwrap_or(String::from("")));
 
-                    if link_type == "icon" {
+                    if link_node_types.contains(&LinkType::Icon) {
                         // Find and resolve LINK's href attribute
                         if let Some(link_attr_href_value) = get_node_attr(node, "href") {
                             if !options.no_images && !link_attr_href_value.is_empty() {
@@ -776,7 +789,7 @@ pub fn walk_and_embed_assets(
                                 set_node_attr(node, "href", None);
                             }
                         }
-                    } else if link_type == "stylesheet" {
+                    } else if link_node_types.contains(&LinkType::Stylesheet) {
                         // Resolve LINK's href attribute
                         if let Some(link_attr_href_value) = get_node_attr(node, "href") {
                             if options.no_css {
@@ -797,7 +810,9 @@ pub fn walk_and_embed_assets(
                                 }
                             }
                         }
-                    } else if link_type == "preload" || link_type == "dns-prefetch" {
+                    } else if link_node_types.contains(&LinkType::Preload)
+                        || link_node_types.contains(&LinkType::DnsPrefetch)
+                    {
                         // Since all resources are embedded as data URLs, preloading and prefetching are not necessary
                         set_node_attr(node, "rel", None);
                     } else {


Test patch:
diff --git a/tests/html/mod.rs b/tests/html/mod.rs
index 955d28d..91e4354 100644
--- a/tests/html/mod.rs
+++ b/tests/html/mod.rs
@@ -9,6 +9,7 @@ mod get_node_attr;
 mod get_node_name;
 mod has_favicon;
 mod is_icon;
+mod parse_link_type;
 mod serialize_document;
 mod set_node_attr;
 mod walk_and_embed_assets;
diff --git a/tests/html/parse_link_type.rs b/tests/html/parse_link_type.rs
new file mode 100644
index 0000000..230b427
--- /dev/null
+++ b/tests/html/parse_link_type.rs
@@ -0,0 +1,58 @@
+//  ██████╗  █████╗ ███████╗███████╗██╗███╗   ██╗ ██████╗
+//  ██╔══██╗██╔══██╗██╔════╝██╔════╝██║████╗  ██║██╔════╝
+//  ██████╔╝███████║███████╗███████╗██║██╔██╗ ██║██║  ███╗
+//  ██╔═══╝ ██╔══██║╚════██║╚════██║██║██║╚██╗██║██║   ██║
+//  ██║     ██║  ██║███████║███████║██║██║ ╚████║╚██████╔╝
+//  ╚═╝     ╚═╝  ╚═╝╚══════╝╚══════╝╚═╝╚═╝  ╚═══╝ ╚═════╝
+
+#[cfg(test)]
+mod passing {
+    use monolith::html;
+
+    #[test]
+    fn icon() {
+        assert!(html::parse_link_type("icon").contains(&html::LinkType::Icon));
+    }
+
+    #[test]
+    fn shortcut_icon_capitalized() {
+        assert!(html::parse_link_type("Shortcut Icon").contains(&html::LinkType::Icon));
+    }
+
+    #[test]
+    fn stylesheet() {
+        assert!(html::parse_link_type("stylesheet").contains(&html::LinkType::Stylesheet));
+    }
+
+    #[test]
+    fn preload_stylesheet() {
+        assert!(html::parse_link_type("preload stylesheet").contains(&html::LinkType::Stylesheet));
+    }
+}
+
+//  ███████╗ █████╗ ██╗██╗     ██╗███╗   ██╗ ██████╗
+//  ██╔════╝██╔══██╗██║██║     ██║████╗  ██║██╔════╝
+//  █████╗  ███████║██║██║     ██║██╔██╗ ██║██║  ███╗
+//  ██╔══╝  ██╔══██║██║██║     ██║██║╚██╗██║██║   ██║
+//  ██║     ██║  ██║██║███████╗██║██║ ╚████║╚██████╔╝
+//  ╚═╝     ╚═╝  ╚═╝╚═╝╚══════╝╚═╝╚═╝  ╚═══╝ ╚═════╝
+
+#[cfg(test)]
+mod failing {
+    use monolith::html;
+
+    #[test]
+    fn mask_icon() {
+        assert!(html::parse_link_type("mask-icon").is_empty());
+    }
+
+    #[test]
+    fn fluid_icon() {
+        assert!(html::parse_link_type("fluid-icon").is_empty());
+    }
+
+    #[test]
+    fn empty_string() {
+        assert!(html::parse_link_type("").is_empty());
+    }
+}