Lines
95.52 %
Functions
26.91 %
Branches
50.62 %
//! Parser for an Accept-Language HTTP header.
use language_tags::{LanguageTag, ParseError};
use locale_config::{LanguageRange, Locale};
use std::error;
use std::fmt;
use std::str::FromStr;
#[cfg(doc)]
use crate::api::CairoRenderer;
/// Used to set the language for rendering.
///
/// SVG documents can use the `<switch>` element, whose children have a `systemLanguage`
/// attribute; only the first child which has a `systemLanguage` that matches the
/// preferred languages will be rendered.
/// This enum, used with [`CairoRenderer::with_language`], configures how to obtain the
/// user's prefererred languages.
pub enum Language {
/// Use the Unix environment variables `LANGUAGE`, `LC_ALL`, `LC_MESSAGES` and `LANG` to obtain the
/// user's language.
/// This uses [`g_get_language_names()`][ggln] underneath.
/// [ggln]: https://docs.gtk.org/glib/func.get_language_names.html
FromEnvironment,
/// Use a list of languages in the form of an HTTP Accept-Language header, like `es, en;q=0.8`.
/// This is convenient when you want to select an explicit set of languages, instead of
/// assuming that the Unix environment has the language you want.
AcceptLanguage(AcceptLanguage),
}
/// `Language` but with the environment's locale converted to something we can use.
#[derive(Clone)]
pub enum UserLanguage {
LanguageTags(LanguageTags),
#[derive(Clone, Debug, PartialEq)]
struct Weight(Option<f32>);
impl Weight {
fn numeric(&self) -> f32 {
self.0.unwrap_or(1.0)
struct Item {
tag: LanguageTag,
weight: Weight,
/// Stores a parsed version of an HTTP Accept-Language header.
/// RFC 7231: <https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5>
pub struct AcceptLanguage(Box<[Item]>);
/// Errors when parsing an `AcceptLanguage`.
#[derive(Debug, PartialEq)]
enum AcceptLanguageError {
NoElements,
InvalidCharacters,
InvalidLanguageTag(ParseError),
InvalidWeight,
impl error::Error for AcceptLanguageError {}
impl fmt::Display for AcceptLanguageError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoElements => write!(f, "no language tags in list"),
Self::InvalidCharacters => write!(f, "invalid characters in language list"),
Self::InvalidLanguageTag(e) => write!(f, "invalid language tag: {e}"),
Self::InvalidWeight => write!(f, "invalid q= weight"),
/// Optional whitespace, Space or Tab, per RFC 7230.
/// RFC 7230: <https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.3>
const OWS: [char; 2] = ['\x20', '\x09'];
impl AcceptLanguage {
/// Parses the payload of an HTTP Accept-Language header.
/// For example, a valid header looks like `es, en;q=0.8`, and means, "I prefer Spanish,
/// but will also accept English".
/// Use this function to construct a [`Language::AcceptLanguage`]
/// variant to pass to the [`CairoRenderer::with_language`] function.
/// See RFC 7231 for details: <https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5>
pub fn parse(s: &str) -> Result<AcceptLanguage, String> {
AcceptLanguage::parse_internal(s).map_err(|e| format!("{}", e))
/// Internal constructor. We don't expose [`AcceptLanguageError`] in the public API;
/// there we just use a [`String`].
fn parse_internal(s: &str) -> Result<AcceptLanguage, AcceptLanguageError> {
if !s.is_ascii() {
return Err(AcceptLanguageError::InvalidCharacters);
let mut items = Vec::new();
for val in s.split(',') {
let trimmed = val.trim_matches(&OWS[..]);
if trimmed.is_empty() {
continue;
items.push(Item::parse(trimmed)?);
if items.is_empty() {
Err(AcceptLanguageError::NoElements)
} else {
Ok(AcceptLanguage(items.into_boxed_slice()))
fn iter(&self) -> impl Iterator<Item = (&LanguageTag, f32)> {
self.0.iter().map(|item| (&item.tag, item.weight.numeric()))
fn any_matches(&self, tag: &LanguageTag) -> bool {
self.iter().any(|(self_tag, _weight)| tag.matches(self_tag))
impl Item {
fn parse(s: &str) -> Result<Item, AcceptLanguageError> {
let semicolon_pos = s.find(';');
let (before_semicolon, after_semicolon) = if let Some(semi) = semicolon_pos {
(&s[..semi], Some(&s[semi + 1..]))
(s, None)
};
let tag = LanguageTag::parse(before_semicolon)
.map_err(AcceptLanguageError::InvalidLanguageTag)?;
let weight = if let Some(quality) = after_semicolon {
let quality = quality.trim_start_matches(&OWS[..]);
let number = if let Some(qvalue) = quality.strip_prefix("q=") {
if qvalue.starts_with(&['0', '1'][..]) {
let first_digit = qvalue.chars().next().unwrap();
if let Some(decimals) = qvalue[1..].strip_prefix('.') {
if (first_digit == '0'
&& decimals.len() <= 3
&& decimals.chars().all(|c| c.is_ascii_digit()))
|| (first_digit == '1'
&& decimals.chars().all(|c| c == '0'))
{
qvalue
return Err(AcceptLanguageError::InvalidWeight);
Weight(Some(
f32::from_str(number).map_err(|_| AcceptLanguageError::InvalidWeight)?,
))
Weight(None)
Ok(Item { tag, weight })
/// A list of BCP47 language tags.
/// RFC 5664: <https://www.rfc-editor.org/info/rfc5664>
#[derive(Debug, Clone, PartialEq)]
pub struct LanguageTags(Vec<LanguageTag>);
impl LanguageTags {
pub fn empty() -> Self {
LanguageTags(Vec::new())
/// Converts a `Locale` to a set of language tags.
pub fn from_locale(locale: &Locale) -> Result<LanguageTags, String> {
let mut tags = Vec::new();
for locale_range in locale.tags_for("messages") {
if locale_range == LanguageRange::invariant() {
let str_locale_range = locale_range.as_ref();
let locale_tag = LanguageTag::from_str(str_locale_range).map_err(|e| {
format!("invalid language tag \"{str_locale_range}\" in locale: {e}")
})?;
if !locale_tag.is_language_range() {
return Err(format!(
"language tag \"{locale_tag}\" is not a language range"
));
tags.push(locale_tag);
Ok(LanguageTags(tags))
pub fn from(tags: Vec<LanguageTag>) -> LanguageTags {
LanguageTags(tags)
pub fn iter(&self) -> impl Iterator<Item = &LanguageTag> {
self.0.iter()
pub fn any_matches(&self, language_tag: &LanguageTag) -> bool {
self.0.iter().any(|tag| tag.matches(language_tag))
impl UserLanguage {
pub fn any_matches(&self, tags: &LanguageTags) -> bool {
match *self {
UserLanguage::LanguageTags(ref language_tags) => {
tags.iter().any(|tag| language_tags.any_matches(tag))
UserLanguage::AcceptLanguage(ref accept_language) => {
tags.iter().any(|tag| accept_language.any_matches(tag))
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_accept_language() {
// plain tag
assert_eq!(
AcceptLanguage::parse_internal("es-MX").unwrap(),
AcceptLanguage(
vec![Item {
tag: LanguageTag::parse("es-MX").unwrap(),
weight: Weight(None)
}]
.into_boxed_slice()
)
);
// with quality
AcceptLanguage::parse_internal("es-MX;q=1").unwrap(),
weight: Weight(Some(1.0))
AcceptLanguage::parse_internal("es-MX;q=0").unwrap(),
weight: Weight(Some(0.0))
// zero decimals are allowed
AcceptLanguage::parse_internal("es-MX;q=0.").unwrap(),
AcceptLanguage::parse_internal("es-MX;q=1.").unwrap(),
// one decimal
AcceptLanguage::parse_internal("es-MX;q=1.0").unwrap(),
// two decimals
AcceptLanguage::parse_internal("es-MX;q=1.00").unwrap(),
// three decimals
AcceptLanguage::parse_internal("es-MX;q=1.000").unwrap(),
// multiple elements
AcceptLanguage::parse_internal("es-MX, en; q=0.5").unwrap(),
vec![
Item {
},
tag: LanguageTag::parse("en").unwrap(),
weight: Weight(Some(0.5))
]
// superfluous whitespace
AcceptLanguage::parse_internal(",es-MX;q=1.000 , en; q=0.125 , ,").unwrap(),
weight: Weight(Some(0.125))
fn empty_lists() {
assert!(matches!(
AcceptLanguage::parse_internal(""),
AcceptLanguage::parse_internal(","),
AcceptLanguage::parse_internal(", , ,,,"),
fn ascii_only() {
AcceptLanguage::parse_internal("ës"),
Err(AcceptLanguageError::InvalidCharacters)
fn invalid_tag() {
AcceptLanguage::parse_internal("no_underscores"),
Err(AcceptLanguageError::InvalidLanguageTag(_))
fn invalid_weight() {
AcceptLanguage::parse_internal("es;"),
Err(AcceptLanguageError::InvalidWeight)
AcceptLanguage::parse_internal("es;q"),
AcceptLanguage::parse_internal("es;q="),
AcceptLanguage::parse_internal("es;q=2"),
AcceptLanguage::parse_internal("es;q=1.1"),
AcceptLanguage::parse_internal("es;q=1.12"),
AcceptLanguage::parse_internal("es;q=1.123"),
// Up to three decimals allowed per RFC 7231
AcceptLanguage::parse_internal("es;q=0.1234"),
fn iter() {
let accept_language = AcceptLanguage::parse_internal("es-MX, en; q=0.5").unwrap();
let mut iter = accept_language.iter();
let (tag, weight) = iter.next().unwrap();
assert_eq!(*tag, LanguageTag::parse("es-MX").unwrap());
assert_eq!(weight, 1.0);
assert_eq!(*tag, LanguageTag::parse("en").unwrap());
assert_eq!(weight, 0.5);
assert!(iter.next().is_none());