shindan_maker/
internal.rs

1use 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    // Strategy 1: Try parsing the `data-blocks` JSON attribute
126    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    // Strategy 2: Fallback to DOM traversal
162    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("&nbsp;", " ");
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}