use crate::utils::escape_attribute;
use crate::MetadataMap;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
#[derive(Debug, Default, PartialEq, Eq, Hash, Clone)]
#[non_exhaustive]
pub struct MetaTagGroups {
pub apple: String,
pub primary: String,
pub og: String,
pub ms: String,
pub twitter: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MetaAttribute {
Name,
Property,
HttpEquiv,
}
impl MetaAttribute {
pub const fn as_str(self) -> &'static str {
match self {
Self::Name => "name",
Self::Property => "property",
Self::HttpEquiv => "http-equiv",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct MetaTag {
pub name: String,
pub content: String,
}
const PROPERTY_PREFIXES: [&str; 7] = [
"og:", "article:", "fb:", "profile:", "book:", "music:", "video:",
];
impl MetaTag {
pub fn new(
name: impl Into<String>,
content: impl Into<String>,
) -> Self {
Self {
name: name.into(),
content: content.into(),
}
}
pub fn attribute(&self) -> MetaAttribute {
if PROPERTY_PREFIXES.iter().any(|p| self.name.starts_with(p)) {
MetaAttribute::Property
} else {
MetaAttribute::Name
}
}
pub fn render(&self) -> String {
format!(
r#"<meta {}="{}" content="{}">"#,
self.attribute().as_str(),
escape_attribute(&self.name),
escape_attribute(&self.content)
)
}
}
impl fmt::Display for MetaTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.render())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Group {
Apple,
Primary,
Og,
Ms,
Twitter,
}
const TAG_TABLE: [(Group, &str); 20] = [
(Group::Apple, "apple-mobile-web-app-capable"),
(Group::Apple, "mobile-web-app-capable"),
(Group::Apple, "apple-mobile-web-app-status-bar-style"),
(Group::Apple, "apple-mobile-web-app-title"),
(Group::Primary, "author"),
(Group::Primary, "description"),
(Group::Primary, "keywords"),
(Group::Primary, "viewport"),
(Group::Og, "og:title"),
(Group::Og, "og:description"),
(Group::Og, "og:image"),
(Group::Og, "og:url"),
(Group::Og, "og:type"),
(Group::Ms, "msapplication-TileColor"),
(Group::Ms, "msapplication-TileImage"),
(Group::Twitter, "twitter:card"),
(Group::Twitter, "twitter:site"),
(Group::Twitter, "twitter:title"),
(Group::Twitter, "twitter:description"),
(Group::Twitter, "twitter:image"),
];
fn group_for(name: &str) -> Group {
if name.starts_with("apple-") || name == "mobile-web-app-capable" {
Group::Apple
} else if name.starts_with("msapplication-") {
Group::Ms
} else if PROPERTY_PREFIXES.iter().any(|p| name.starts_with(p)) {
Group::Og
} else if name.starts_with("twitter:") {
Group::Twitter
} else {
Group::Primary
}
}
impl MetaTagGroups {
fn field_mut(&mut self, group: Group) -> &mut String {
match group {
Group::Apple => &mut self.apple,
Group::Primary => &mut self.primary,
Group::Og => &mut self.og,
Group::Ms => &mut self.ms,
Group::Twitter => &mut self.twitter,
}
}
pub fn add_custom_tag(&mut self, name: &str, content: &str) {
let rendered = MetaTag::new(name, content).render();
let field = self.field_mut(group_for(name));
if !field.is_empty() {
field.push('\n');
}
field.push_str(&rendered);
}
pub fn format_meta_tag(&self, name: &str, content: &str) -> String {
MetaTag::new(name, content).render()
}
pub fn generate_apple_meta_tags(&mut self, metadata: &MetadataMap) {
self.apple = self.generate_group(metadata, Group::Apple);
}
pub fn generate_primary_meta_tags(
&mut self,
metadata: &MetadataMap,
) {
self.primary = self.generate_group(metadata, Group::Primary);
}
pub fn generate_og_meta_tags(&mut self, metadata: &MetadataMap) {
self.og = self.generate_group(metadata, Group::Og);
}
pub fn generate_ms_meta_tags(&mut self, metadata: &MetadataMap) {
self.ms = self.generate_group(metadata, Group::Ms);
}
pub fn generate_twitter_meta_tags(
&mut self,
metadata: &MetadataMap,
) {
self.twitter = self.generate_group(metadata, Group::Twitter);
}
fn generate_group(
&self,
metadata: &MetadataMap,
group: Group,
) -> String {
let names: Vec<&str> = TAG_TABLE
.iter()
.filter(|(g, _)| *g == group)
.map(|(_, name)| *name)
.collect();
self.generate_tags(metadata, &names)
}
pub fn generate_tags(
&self,
metadata: &MetadataMap,
tags: &[&str],
) -> String {
tags.iter()
.filter_map(|&tag| {
metadata.get(tag).map(|value| {
MetaTag::new(tag, value.as_str()).render()
})
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn iter(&self) -> impl Iterator<Item = MetaTag> + '_ {
[
&self.apple,
&self.primary,
&self.og,
&self.ms,
&self.twitter,
]
.into_iter()
.flat_map(|group| group.lines())
.filter_map(parse_rendered)
}
}
fn parse_rendered(line: &str) -> Option<MetaTag> {
let rest = line.strip_prefix("<meta ")?;
let (_, rest) = rest.split_once("=\"")?;
let (name, rest) = rest.split_once("\" content=\"")?;
let content = rest.strip_suffix("\">")?;
Some(MetaTag::new(
crate::utils::unescape_html(name),
crate::utils::unescape_html(content),
))
}
impl fmt::Display for MetaTagGroups {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}\n{}\n{}\n{}\n{}",
self.apple, self.primary, self.og, self.ms, self.twitter
)
}
}
pub fn generate_metatags(metadata: &MetadataMap) -> MetaTagGroups {
let mut meta_tag_groups = MetaTagGroups::default();
meta_tag_groups.generate_apple_meta_tags(metadata);
meta_tag_groups.generate_primary_meta_tags(metadata);
meta_tag_groups.generate_og_meta_tags(metadata);
meta_tag_groups.generate_ms_meta_tags(metadata);
meta_tag_groups.generate_twitter_meta_tags(metadata);
meta_tag_groups
}
#[cfg(feature = "html")]
mod html;
#[cfg(feature = "html")]
#[cfg_attr(docsrs, doc(cfg(feature = "html")))]
pub use html::{extract_meta_tags, extract_meta_tags_lenient};
pub fn meta_tags_to_hashmap(meta_tags: Vec<MetaTag>) -> MetadataMap {
meta_tags
.into_iter()
.map(|tag| (tag.name, tag.content))
.collect()
}
#[cfg(all(test, feature = "html"))]
mod tests;