html_site_generator/html/
hyperlink.rs

1use std::io::Write;
2
3use anyhow::Result;
4use derive_builder::Builder;
5use html_site_generator_macro::DeriveSetHtmlAttributes;
6
7use crate::attributes::HtmlAttributes;
8use crate::html::{IntoHtmlNode, IsParagraph};
9
10/// Specifies which referrer information to send with the link
11#[derive(Debug, Clone)]
12pub enum ReferrerPolicy {
13    NoReferrer,
14    NoReferrerWhenDowngrade,
15    Origin,
16    OriginWhenCrossOrigin,
17    SameOrigin,
18    StrictOriginWhenCrossOrigin,
19    UnsafeUrl,
20}
21
22impl ReferrerPolicy {
23    pub(crate) fn to_html_string(&self) -> &'static str {
24        match self {
25            ReferrerPolicy::NoReferrer => "no-referrer",
26            ReferrerPolicy::NoReferrerWhenDowngrade => "no-referrer-when-downgrade",
27            ReferrerPolicy::Origin => "origin",
28            ReferrerPolicy::OriginWhenCrossOrigin => "origin-when-cross-origin",
29            ReferrerPolicy::SameOrigin => "same-origin",
30            ReferrerPolicy::StrictOriginWhenCrossOrigin => "strict-origin-when-cross-origin",
31            ReferrerPolicy::UnsafeUrl => "unsafe-url",
32        }
33    }
34}
35
36/// Specifies the relationship between the current document and the linked document
37#[derive(Debug, Clone)]
38pub enum Relationship {
39    Alternate,
40    Author,
41    Bookmark,
42    External,
43    Help,
44    License,
45    Next,
46    NoFollow,
47    NoReferrer,
48    NoOpener,
49    Prev,
50    Search,
51    Tag,
52}
53
54impl Relationship {
55    fn to_html_string(&self) -> &'static str {
56        match self {
57            Relationship::Alternate => "alternate",
58            Relationship::Author => "author",
59            Relationship::Bookmark => "bookmark",
60            Relationship::External => "external",
61            Relationship::Help => "help",
62            Relationship::License => "license",
63            Relationship::Next => "next",
64            Relationship::NoFollow => "nofollow",
65            Relationship::NoReferrer => "noreferrer",
66            Relationship::NoOpener => "noopener",
67            Relationship::Prev => "prev",
68            Relationship::Search => "search",
69            Relationship::Tag => "tag",
70        }
71    }
72}
73
74/// Specifies where to open the linked document
75#[derive(Debug, Clone)]
76pub enum Target {
77    Blank,
78    Parent,
79    /// => `Self`
80    SSelf,
81    Top,
82}
83
84impl Target {
85    fn to_html_string(&self) -> &'static str {
86        match self {
87            Target::Blank => "_blank",
88            Target::Parent => "_parent",
89            Target::SSelf => "_self",
90            Target::Top => "_top",
91        }
92    }
93}
94
95/// HTML Attribute: [`<a>`](https://www.w3schools.com/tags/tag_a.asp)
96#[derive(Debug, Builder, DeriveSetHtmlAttributes)]
97pub struct Hyperlink {
98    /// Specifies that the target will be downloaded when a user clicks on the hyperlink
99    #[builder(setter(strip_option, into), default)]
100    download: Option<String>,
101
102    /// Specifies the URL of the page the link goes to
103    #[builder(setter(strip_option, into), default)]
104    href: Option<String>,
105
106    /// Specifies the language of the linked document
107    #[builder(setter(strip_option, into), default)]
108    href_lang: Option<String>,
109
110    /// Specifies what media/device the linked document is optimized for
111    #[builder(setter(strip_option, into), default)]
112    media: Option<String>,
113
114    /// Specifies a space-separated list of URLs to which, when the link is followed,
115    /// post requests with the body ping will be sent by the browser (in the background).
116    /// Typically used for tracking.
117    #[builder(setter(strip_option, into), default)]
118    ping: Option<String>,
119
120    /// Specifies which referrer information to send with the link
121    #[builder(setter(strip_option, into), default)]
122    referrer_policy: Option<ReferrerPolicy>,
123
124    /// Specifies the relationship between the current document and the linked document
125    #[builder(default = "Some(vec![Relationship::NoOpener, Relationship::NoReferrer])")]
126    rel: Option<Vec<Relationship>>,
127
128    /// Specifies where to open the linked document
129    #[builder(default = "Some(Target::Blank)")]
130    target: Option<Target>,
131
132    // TODO maybe use a crate with mime types
133    /// Specifies the media type of the linked document
134    #[builder(setter(strip_option, into), default)]
135    mime_type: Option<String>,
136
137    #[builder(setter(skip))]
138    children: Vec<Box<dyn IntoHtmlNode>>,
139
140    #[builder(setter(skip))]
141    _attributes: HtmlAttributes,
142}
143
144impl Hyperlink {
145    pub fn add_element(&mut self, item: impl IntoHtmlNode + 'static) {
146        self.children.push(Box::new(item))
147    }
148}
149
150impl IntoHtmlNode for Hyperlink {
151    fn transform_into_html_node(&self, buffer: &mut dyn Write) -> Result<()> {
152        write!(buffer, "<a")?;
153        self._attributes.transform_into_html_node(buffer)?;
154
155        if let Some(value) = &self.download {
156            write!(buffer, " download=\"{}\"", value)?;
157        }
158        if let Some(value) = &self.href {
159            write!(buffer, " href=\"{}\"", value)?;
160        }
161        if let Some(value) = &self.href_lang {
162            write!(buffer, " hreflang=\"{}\"", value)?;
163        }
164        if let Some(value) = &self.media {
165            write!(buffer, " media=\"{}\"", value)?;
166        }
167        if let Some(value) = &self.ping {
168            write!(buffer, " ping=\"{}\"", value)?;
169        }
170        if let Some(value) = &self.referrer_policy {
171            write!(buffer, " referrerpolicy=\"{}\"", value.to_html_string())?;
172        }
173        if let Some(value) = &self.rel {
174            let raw = value
175                .iter()
176                .map(|item| item.to_html_string())
177                .enumerate()
178                .fold("".to_string(), |acc, (i, item)| {
179                    acc + if i == 0 { "" } else { " " } + item
180                });
181
182            write!(buffer, " rel=\"{}\"", raw)?;
183        }
184        if let Some(value) = &self.target {
185            write!(buffer, " target=\"{}\"", value.to_html_string())?;
186        }
187        if let Some(value) = &self.mime_type {
188            write!(buffer, " type=\"{}\"", value)?;
189        }
190
191        write!(buffer, ">")?;
192
193        for child in &self.children {
194            child.transform_into_html_node(buffer)?;
195        }
196
197        writeln!(buffer, "</a>")?;
198
199        Ok(())
200    }
201}
202
203// TODO I'm not quit sure if `Hyperlink` implements the trait `IsParagraph`.
204// But at the moment this is the only way to use `Hyperlink` in a `Paragraph`
205// Maybe there is another, and better way, to do this?
206// same as `Image`, `LineBreak`
207impl IsParagraph for Hyperlink {
208    fn to_raw(&self) -> String {
209        let mut vec = Vec::new();
210
211        self.transform_into_html_node(&mut vec).unwrap();
212
213        String::from_utf8(vec).unwrap()
214    }
215}