1#![doc = include_str!("../README.md")]
2
3use std::{fmt, str};
4use subslice::SubsliceExt;
5use thiserror::Error;
6
7pub 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#[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 pub fn pronunciations_to_string(&self) -> String {
64 self.pronunciations.join(",")
65 }
66
67 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}