bing_dict/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{fmt, str};
4use subslice::SubsliceExt;
5use thiserror::Error;
6
7/// Translate a word / phrase. Return `Ok(None)` if the word can not be found in Bing Dictionary
8pub async fn translate(input: &str) -> Result<Option<Paraphrase>, Error> {
9    let url = format!("https://www.bing.com/dict/search?mkt=zh-cn&q={input}");
10
11    let resp = reqwest::get(url).await?.bytes().await?;
12
13    let desc = resp
14        .find(br#"<meta name="description" content=""#)
15        .and_then(|start| {
16            resp[start + 34..]
17                .find(br#"" />"#)
18                .map(|end| &resp[start + 34..start + end + 34])
19        })
20        .ok_or(Error::PageError)?;
21
22    let input_len = html_escape::encode_text(&input).len() + 36;
23
24    if desc.len() > input_len && desc.starts_with(b"\xE5\xBF\x85\xE5\xBA\x94\xE8\xAF\x8D\xE5\x85\xB8\xE4\xB8\xBA\xE6\x82\xA8\xE6\x8F\x90\xE4\xBE\x9B") {
25        let res = str::from_utf8(&desc[input_len..])?.trim();
26        Ok(Some(Paraphrase::parse(input, res)))
27    } else {
28        Ok(None)
29    }
30}
31
32/// The paraphrase of a word / phrase
33#[derive(Debug, Clone)]
34pub struct Paraphrase {
35    pub input: String,
36    pub pronunciations: Vec<String>,
37    pub genders: Vec<String>,
38}
39
40impl Paraphrase {
41    fn parse(input: &str, paraphrase: &str) -> Self {
42        let mut pronunciations = Vec::new();
43        let mut genders = Vec::new();
44
45        for part in paraphrase.split(',') {
46            if part.starts_with('英') || part.starts_with('美') || part.starts_with("拼音") {
47                pronunciations.push(part.to_string());
48            } else {
49                for gender in part.split("; ") {
50                    genders.push(gender.trim_end_matches(';').to_string())
51                }
52            }
53        }
54
55        Self {
56            input: input.to_owned(),
57            pronunciations,
58            genders,
59        }
60    }
61
62    /// Get pronunciations as a `String`
63    pub fn pronunciations_to_string(&self) -> String {
64        self.pronunciations.join(",")
65    }
66
67    /// Get genders as a `String`
68    pub fn genders_to_string(&self) -> String {
69        self.genders.join("\n")
70    }
71}
72
73impl fmt::Display for Paraphrase {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        let pronunciations = if !self.pronunciations.is_empty() {
76            let mut pronunciations = self.pronunciations_to_string();
77            pronunciations.push('\n');
78            pronunciations
79        } else {
80            String::new()
81        };
82
83        let input = &self.input;
84        let genders = self.genders_to_string();
85
86        write!(f, "{input}\n{pronunciations}{genders}")
87    }
88}
89
90#[derive(Error, Debug)]
91pub enum Error {
92    #[error(r#"no <meta name="description" /> found in page"#)]
93    PageError,
94    #[error(transparent)]
95    ReqwestError(#[from] reqwest::Error),
96    #[error(transparent)]
97    Utf8Error(#[from] str::Utf8Error),
98}
99
100#[cfg(test)]
101mod tests {
102    use crate::translate;
103
104    #[tokio::test]
105    async fn chi_to_eng() {
106        assert!(translate("词典").await.unwrap().is_some());
107    }
108
109    #[tokio::test]
110    async fn chi_to_eng_no_result() {
111        assert!(translate("没有在必应词典中找到结果")
112            .await
113            .unwrap()
114            .is_none());
115    }
116
117    #[tokio::test]
118    async fn eng_to_chi() {
119        assert!(translate("dictionary").await.unwrap().is_some());
120    }
121
122    #[tokio::test]
123    async fn eng_to_chi_no_result() {
124        assert!(translate("yranoitcid").await.unwrap().is_none());
125    }
126}