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