shindan_maker/
internal.rs1use anyhow::{Context, Result};
2use scraper::{Html, Node, Selector};
3use std::sync::OnceLock;
4
5static SELECTORS: OnceLock<Selectors> = OnceLock::new();
6
7struct Selectors {
8 shindan_title: Selector,
9 shindan_description: Selector,
10 form_inputs: Vec<Selector>,
11 input_parts: Selector,
12 #[cfg(feature = "segments")]
13 shindan_result: Selector,
14 #[cfg(feature = "html")]
15 title_and_result: Selector,
16 #[cfg(feature = "html")]
17 script: Selector,
18 #[cfg(feature = "html")]
19 effects: Vec<Selector>,
20 #[cfg(feature = "html")]
21 merged_image: Selector,
22}
23
24impl Selectors {
25 fn get() -> &'static Self {
26 SELECTORS.get_or_init(|| Self {
27 shindan_title: Selector::parse("#shindanTitle").expect("Valid Selector"),
28 shindan_description: Selector::parse("#shindanDescriptionDisplay")
29 .expect("Valid Selector"),
30 form_inputs: vec![
31 Selector::parse("input[name=_token]").unwrap(),
32 Selector::parse("input[name=randname]").unwrap(),
33 Selector::parse("input[name=type]").unwrap(),
34 ],
35 input_parts: Selector::parse(r#"input[name^="parts["]"#).unwrap(),
36
37 #[cfg(feature = "segments")]
38 shindan_result: Selector::parse("#shindanResult").expect("Valid Selector"),
39
40 #[cfg(feature = "html")]
41 title_and_result: Selector::parse("#title_and_result").expect("Valid Selector"),
42 #[cfg(feature = "html")]
43 script: Selector::parse("script").expect("Valid Selector"),
44 #[cfg(feature = "html")]
45 effects: vec![
46 Selector::parse("span.shindanEffects[data-mode=ef_typing]").unwrap(),
47 Selector::parse("span.shindanEffects[data-mode=ef_shuffle]").unwrap(),
48 ],
49 #[cfg(feature = "html")]
50 merged_image: Selector::parse("span.v1-merged-image").unwrap(),
51 })
52 }
53}
54
55pub(crate) fn extract_title(dom: &Html) -> Result<String> {
56 Ok(dom
57 .select(&Selectors::get().shindan_title)
58 .next()
59 .context("Failed to find shindanTitle element")?
60 .value()
61 .attr("data-shindan_title")
62 .context("Missing data-shindan_title attribute")?
63 .to_string())
64}
65
66pub(crate) fn extract_description(dom: &Html) -> Result<String> {
67 let mut desc = Vec::new();
68 let element = dom
69 .select(&Selectors::get().shindan_description)
70 .next()
71 .context("Failed to find description element")?;
72
73 for child in element.children() {
74 match child.value() {
75 Node::Text(text) => desc.push(text.to_string()),
76 Node::Element(el) if el.name() == "br" => desc.push("\n".to_string()),
77 Node::Element(_) => {
78 if let Some(node) = child.children().next()
79 && let Node::Text(text) = node.value()
80 {
81 desc.push(text.to_string());
82 }
83 }
84 _ => {}
85 }
86 }
87 Ok(desc.join(""))
88}
89
90pub(crate) fn extract_form_data(dom: &Html, name: &str) -> Result<Vec<(String, String)>> {
91 let selectors = Selectors::get();
92 let fields = ["_token", "randname", "type"];
93 let mut form_data = Vec::with_capacity(fields.len() + 2);
94
95 for (i, &field) in fields.iter().enumerate() {
96 let val = dom
97 .select(&selectors.form_inputs[i])
98 .next()
99 .and_then(|el| el.value().attr("value"))
100 .unwrap_or("")
101 .to_string();
102 form_data.push((field.to_string(), val));
103 }
104
105 form_data.push(("user_input_value_1".to_string(), name.to_string()));
106
107 for el in dom.select(&selectors.input_parts) {
108 if let Some(input_name) = el.value().attr("name") {
109 form_data.push((input_name.to_string(), name.to_string()));
110 }
111 }
112 Ok(form_data)
113}
114
115#[cfg(feature = "segments")]
116pub(crate) fn parse_segments(response_text: &str) -> Result<crate::models::Segments> {
117 use crate::models::{Segment, Segments};
118 use scraper::ElementRef;
119 use serde_json::{Value, json};
120
121 let dom = Html::parse_document(response_text);
122 let mut segments = Vec::new();
123
124 let container_ref = dom
125 .select(&Selectors::get().shindan_result)
126 .next()
127 .context("Failed to find shindanResult")?;
128
129 if let Some(blocks_json) = container_ref.value().attr("data-blocks")
131 && let Ok(blocks) = serde_json::from_str::<Vec<Value>>(blocks_json)
132 {
133 for block in blocks {
134 let type_ = block["type"].as_str().unwrap_or("");
135 match type_ {
136 "text" => {
137 if let Some(content) = block.get("content").and_then(|v| v.as_str()) {
138 segments.push(Segment::new("text", json!({ "text": content })));
139 }
140 }
141 "user_input" => {
142 if let Some(val) = block.get("value").and_then(|v| v.as_str()) {
143 segments.push(Segment::new("text", json!({ "text": val })));
144 }
145 }
146 "image" => {
147 let src = block
148 .get("source")
149 .or(block.get("src"))
150 .or(block.get("url"))
151 .or(block.get("file"))
152 .and_then(|v| v.as_str());
153 if let Some(s) = src {
154 segments.push(Segment::new("image", json!({ "file": s })));
155 }
156 }
157 _ => {}
158 }
159 }
160 if !segments.is_empty() {
161 return Ok(Segments(segments));
162 }
163 }
164
165 fn extract_nodes(node: ElementRef, segments: &mut Vec<Segment>) {
167 for child in node.children() {
168 match child.value() {
169 Node::Text(text) => {
170 let t = text.replace(" ", " ");
171 if !t.is_empty() {
172 segments.push(Segment::new("text", json!({ "text": t })));
173 }
174 }
175 Node::Element(el) => {
176 if el.name() == "br" {
177 segments.push(Segment::new("text", json!({ "text": "\n" })));
178 } else if el.name() == "img" {
179 let src = el.attr("data-src").or_else(|| el.attr("src"));
180 if let Some(s) = src {
181 segments.push(Segment::new("image", json!({ "file": s })));
182 }
183 } else if let Some(child_el) = ElementRef::wrap(child) {
184 extract_nodes(child_el, segments);
185 }
186 }
187 _ => {}
188 }
189 }
190 }
191
192 extract_nodes(container_ref, &mut segments);
193
194 Ok(Segments(segments))
195}
196
197#[cfg(feature = "html")]
198pub(crate) fn construct_html_result(
199 id: &str,
200 response_text: &str,
201 base_url: &str,
202) -> Result<String> {
203 use anyhow::anyhow;
204 use scraper::Element;
205 use serde_json;
206
207 static APP_CSS: &str = include_str!("../static/app.css");
208 static SHINDAN_JS: &str = include_str!("../static/shindan.js");
209 static APP_JS: &str = include_str!("../static/app.js");
210 static CHART_JS: &str = include_str!("../static/chart.js");
211
212 let dom = Html::parse_document(response_text);
213 let selectors = Selectors::get();
214
215 let mut title_and_result = dom
216 .select(&selectors.title_and_result)
217 .next()
218 .context("Failed to get result element")?
219 .html();
220
221 for selector in &selectors.effects {
223 for effect in dom.select(selector) {
224 if let Some(next) = effect.next_sibling_element() {
225 if next.value().name() == "noscript" {
226 title_and_result = title_and_result
227 .replace(&effect.html(), "")
228 .replace(&next.html(), &next.inner_html());
229 }
230 }
231 }
232 }
233
234 for element in dom.select(&selectors.merged_image) {
237 if let Some(urls_json) = element.value().attr("data-image-urls") {
238 if let Ok(urls) = serde_json::from_str::<Vec<String>>(urls_json) {
239 let mut img_tags = String::new();
240 for url in urls {
241 img_tags.push_str(&format!(
242 r#"<img src="{}" class="shindanResult_image" style="max-width: 100%; height: auto; display: inline-block;">"#,
243 url
244 ));
245 }
246 title_and_result = title_and_result.replace(&element.html(), &img_tags);
247 }
248 }
249 }
250
251 let mut specific_script = String::new();
252 for element in dom.select(&selectors.script) {
253 let html = element.html();
254 if html.contains(id) {
255 specific_script = html;
256 break;
257 }
258 }
259 if specific_script.is_empty() {
260 return Err(anyhow!("Failed to find script with id {}", id));
261 }
262
263 let mut html = format!(
264 r#"<!DOCTYPE html><html lang="zh" style="height:100%"><head><style>{}</style><meta http-equiv="Content-Type" content="text/html;charset=utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0"><base href="{}"><title>ShindanMaker</title></head><body class="" style="position:relative;min-height:100%;top:0"><div id="main-container"><div id="main">{}</div></div></body><script>{}</script><!-- SCRIPTS --></html>"#,
265 APP_CSS, base_url, title_and_result, SHINDAN_JS
266 );
267
268 if response_text.contains("chart.js") {
269 let scripts = format!(
270 "<script>{}</script>\n<script>{}</script>\n{}",
271 APP_JS, CHART_JS, specific_script
272 );
273 html = html.replace("<!-- SCRIPTS -->", &scripts);
274 }
275
276 Ok(html)
277}