Packages
rbt_weave
0.2.67
0.2.67
0.2.66
0.2.65
0.2.64
0.2.63
0.2.62
0.2.61
0.2.60
0.2.59
0.2.58
0.2.57
0.2.56
0.2.55
0.2.54
0.2.53
0.2.52
0.2.51
0.2.50
0.2.49
0.2.48
0.2.47
0.2.46
0.2.45
0.2.44
0.2.43
0.2.42
0.2.41
0.2.40
0.2.39
0.2.38
0.2.37
0.2.36
0.2.35
0.2.34
0.2.33
0.2.32
0.2.31
0.2.30
0.2.29
0.2.28
0.2.27
0.2.26
0.2.25
0.2.24
0.2.23
0.2.22
0.2.21
0.2.20
0.2.19
0.2.18
0.2.17
0.2.16
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Graph conflict detection and image processing NIFs for OSINT knowledge graphs, powered by Rust.
Current section
Files
Jump to
Current section
Files
crates/weave-content/src/html/pages.rs
//! Tag, country, and sitemap page renderers.
use crate::domain::AmountEntry;
use super::countries::{country_name, extract_country_from_case_slug};
use super::escape::{escape, escape_attr};
/// A case entry associated with a tag, used for tag page rendering.
pub struct TagCaseEntry {
/// Display slug for the case link (e.g. `cases/id/corruption/2024/test-case`).
pub slug: String,
/// Case title.
pub title: String,
/// Structured amounts for display badge.
pub amounts: Vec<AmountEntry>,
}
/// Generate a tag page HTML fragment listing all cases with this tag.
///
/// # Errors
///
/// Returns an error if the rendered HTML exceeds the size limit.
pub fn render_tag_page(tag: &str, cases: &[TagCaseEntry]) -> Result<String, String> {
render_tag_page_with_path(tag, &format!("/tags/{}", escape_attr(tag)), cases)
}
pub fn render_tag_page_scoped(
tag: &str,
country: &str,
cases: &[TagCaseEntry],
) -> Result<String, String> {
let display_tag = format!("{} ({})", tag.replace('-', " "), country.to_uppercase());
render_tag_page_with_path(
&display_tag,
&format!("/tags/{}/{}", escape_attr(country), escape_attr(tag)),
cases,
)
}
fn render_tag_page_with_path(
display: &str,
og_url: &str,
cases: &[TagCaseEntry],
) -> Result<String, String> {
let mut html = String::with_capacity(2048);
let og_title = format!("Cases tagged \"{display}\"");
html.push_str(&format!(
"<article class=\"loom-tag-page\" \
data-og-title=\"{}\" \
data-og-description=\"{} cases tagged with {}\" \
data-og-type=\"website\" \
data-og-url=\"{}\">\n",
escape_attr(&og_title),
cases.len(),
escape_attr(display),
escape_attr(og_url),
));
html.push_str(&format!(
" <header class=\"loom-tag-header\">\n \
<h1>{}</h1>\n \
<p class=\"loom-tag-count\">{} cases</p>\n \
</header>\n",
escape(display),
cases.len(),
));
html.push_str(" <ul class=\"loom-case-list\">\n");
for entry in cases {
let country_badge = extract_country_from_case_slug(&entry.slug)
.map(|cc| {
format!(
" <a href=\"/countries/{}\" class=\"loom-country-badge\">{}</a>",
escape_attr(&cc),
escape(&country_name(&cc))
)
})
.unwrap_or_default();
let amount_badges = if entry.amounts.is_empty() {
String::new()
} else {
let badges: Vec<String> = entry
.amounts
.iter()
.map(|a| {
format!(
" <span class=\"loom-amount-badge\">{}</span>",
escape(&a.format_display())
)
})
.collect();
badges.join("")
};
html.push_str(&format!(
" <li><a href=\"/{}\">{}</a>{}{}</li>\n",
escape_attr(&entry.slug),
escape(&entry.title),
country_badge,
amount_badges,
));
}
html.push_str(" </ul>\n");
html.push_str("</article>\n");
super::check_size(&html)
}
/// An entry for the countries index page.
pub struct CountryIndexEntry {
/// ISO 3166-1 alpha-2 country code (lowercase).
pub code: String,
/// Display name of the country.
pub name: String,
/// Number of cases in this country.
pub case_count: usize,
}
/// An entry for the tags index page.
pub struct TagIndexEntry {
/// Tag slug (e.g. `bribery`).
pub tag: String,
/// Number of cases with this tag.
pub case_count: usize,
}
/// Generate a countries index page listing all countries with case counts.
///
/// # Errors
///
/// Returns an error if the rendered HTML exceeds the size limit.
pub fn render_countries_index(countries: &[CountryIndexEntry]) -> Result<String, String> {
let total: usize = countries.iter().map(|c| c.case_count).sum();
let mut html = String::with_capacity(4096);
html.push_str(
"<article class=\"loom-countries-index\" \
data-og-title=\"Countries\" \
data-og-description=\"Browse cases by country\" \
data-og-type=\"website\" \
data-og-url=\"/countries\">\n",
);
html.push_str(&format!(
" <header class=\"loom-countries-header\">\n \
<h1>Countries</h1>\n \
<p class=\"loom-countries-count\">{total} cases across {} countries</p>\n \
</header>\n",
countries.len(),
));
html.push_str(" <ul class=\"loom-country-list\">\n");
for entry in countries {
html.push_str(&format!(
" <li><a href=\"/countries/{}\">{}</a> \
<span class=\"loom-count-badge\">{}</span></li>\n",
escape_attr(&entry.code),
escape(&entry.name),
entry.case_count,
));
}
html.push_str(" </ul>\n");
html.push_str("</article>\n");
super::check_size(&html)
}
/// Generate a tags index page listing all tags with case counts.
///
/// # Errors
///
/// Returns an error if the rendered HTML exceeds the size limit.
pub fn render_tags_index(tags: &[TagIndexEntry]) -> Result<String, String> {
let total: usize = tags.iter().map(|t| t.case_count).sum();
let mut html = String::with_capacity(4096);
html.push_str(
"<article class=\"loom-tags-index\" \
data-og-title=\"Tags\" \
data-og-description=\"Browse cases by tag\" \
data-og-type=\"website\" \
data-og-url=\"/tags\">\n",
);
html.push_str(&format!(
" <header class=\"loom-tags-header\">\n \
<h1>Tags</h1>\n \
<p class=\"loom-tags-count\">{total} cases across {} tags</p>\n \
</header>\n",
tags.len(),
));
html.push_str(" <ul class=\"loom-tag-list\">\n");
for entry in tags {
html.push_str(&format!(
" <li><a href=\"/tags/{}\">{}</a> \
<span class=\"loom-count-badge\">{}</span></li>\n",
escape_attr(&entry.tag),
escape(&entry.tag.replace('-', " ")),
entry.case_count,
));
}
html.push_str(" </ul>\n");
html.push_str("</article>\n");
super::check_size(&html)
}
/// Generate a country page HTML fragment listing all cases for a country.
///
/// # Errors
///
/// Returns an error if the rendered HTML exceeds the size limit.
pub fn render_country_page(country_code: &str, cases: &[TagCaseEntry]) -> Result<String, String> {
let name = country_name(country_code);
let og_url = format!("/countries/{}", escape_attr(country_code));
let mut html = String::with_capacity(2048);
let og_title = format!("Cases in {name}");
html.push_str(&format!(
"<article class=\"loom-country-page\" \
data-og-title=\"{}\" \
data-og-description=\"{} cases in {}\" \
data-og-type=\"website\" \
data-og-url=\"{}\">\n",
escape_attr(&og_title),
cases.len(),
escape_attr(&name),
escape_attr(&og_url),
));
html.push_str(&format!(
" <header class=\"loom-country-header\">\n \
<h1>{}</h1>\n \
<p class=\"loom-country-count\">{} cases</p>\n \
</header>\n",
escape(&name),
cases.len(),
));
html.push_str(" <ul class=\"loom-case-list\">\n");
for entry in cases {
let amount_badges = if entry.amounts.is_empty() {
String::new()
} else {
let badges: Vec<String> = entry
.amounts
.iter()
.map(|a| {
format!(
" <span class=\"loom-amount-badge\">{}</span>",
escape(&a.format_display())
)
})
.collect();
badges.join("")
};
html.push_str(&format!(
" <li><a href=\"/{}\">{}</a>{}</li>\n",
escape_attr(&entry.slug),
escape(&entry.title),
amount_badges,
));
}
html.push_str(" </ul>\n");
html.push_str("</article>\n");
super::check_size(&html)
}
/// Generate a sitemap XML string.
///
/// All tuples are `(slug, display_name)` where slug is the full file-path slug
/// (e.g. `cases/id/corruption/2024/hambalang-case`).
pub fn render_sitemap(
cases: &[(String, String)],
people: &[(String, String)],
organizations: &[(String, String)],
country_codes: &[String],
tags: &[String],
base_url: &str,
) -> String {
let mut xml = String::with_capacity(4096);
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
xml.push_str("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n");
for (slug, _) in cases {
xml.push_str(&format!(
" <url><loc>{base_url}/{}</loc></url>\n",
escape(slug)
));
}
for (slug, _) in people {
xml.push_str(&format!(
" <url><loc>{base_url}/{}</loc></url>\n",
escape(slug)
));
}
for (slug, _) in organizations {
xml.push_str(&format!(
" <url><loc>{base_url}/{}</loc></url>\n",
escape(slug)
));
}
// Index pages
xml.push_str(&format!(" <url><loc>{base_url}/countries</loc></url>\n"));
xml.push_str(&format!(" <url><loc>{base_url}/tags</loc></url>\n"));
for cc in country_codes {
xml.push_str(&format!(
" <url><loc>{base_url}/countries/{}</loc></url>\n",
escape(cc)
));
}
for tag in tags {
xml.push_str(&format!(
" <url><loc>{base_url}/tags/{}</loc></url>\n",
escape(tag)
));
}
xml.push_str("</urlset>\n");
xml
}