feat: highlight link to other quest or copy item name in quest details
This commit is contained in:
@ -208,12 +208,20 @@ pub fn set_always_on_top(app: AppHandle, value: bool) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum RichSegment {
|
||||
Text { text: String },
|
||||
QuestLink { text: String, href: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct QuestStep {
|
||||
pub index: usize,
|
||||
pub text: String,
|
||||
pub images: Vec<String>,
|
||||
pub launch_position: Option<String>,
|
||||
pub rich_text: Vec<RichSegment>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -272,13 +280,14 @@ fn parse_quest_steps(html: &str) -> Result<Vec<QuestStep>, String> {
|
||||
let pos = extract_launch_position(&child, &position_sel);
|
||||
if pos.is_some() {
|
||||
let images = collect_images_from(&child, &img_link_sel, BASE_URL);
|
||||
steps.push(QuestStep { index: steps.len(), text: String::new(), images, launch_position: pos });
|
||||
steps.push(QuestStep { index: steps.len(), text: String::new(), images, launch_position: pos, rich_text: vec![] });
|
||||
}
|
||||
continue; // always skip as a regular step
|
||||
}
|
||||
}
|
||||
|
||||
steps.push(QuestStep { index: steps.len(), text, images: vec![], launch_position: None });
|
||||
let rich_text = element_to_rich_text(&child);
|
||||
steps.push(QuestStep { index: steps.len(), text, images: vec![], launch_position: None, rich_text });
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -304,16 +313,17 @@ fn parse_quest_steps(html: &str) -> Result<Vec<QuestStep>, String> {
|
||||
let pos = extract_launch_position(para, &position_sel);
|
||||
if pos.is_some() {
|
||||
let imgs = images.clone();
|
||||
steps.push(QuestStep { index: steps.len(), text: String::new(), images: imgs, launch_position: pos });
|
||||
steps.push(QuestStep { index: steps.len(), text: String::new(), images: imgs, launch_position: pos, rich_text: vec![] });
|
||||
}
|
||||
first_para = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let rich_text = element_to_rich_text(para);
|
||||
let imgs = if first_para { images.clone() } else { vec![] };
|
||||
first_para = false;
|
||||
steps.push(QuestStep { index: steps.len(), text, images: imgs, launch_position: None });
|
||||
steps.push(QuestStep { index: steps.len(), text, images: imgs, launch_position: None, rich_text });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -410,6 +420,105 @@ fn collect_images_from(
|
||||
images
|
||||
}
|
||||
|
||||
fn is_quest_link(href: &str) -> bool {
|
||||
href.starts_with('/')
|
||||
&& href.ends_with(".html")
|
||||
&& !href.contains("/uploads/")
|
||||
&& !href.contains('#')
|
||||
}
|
||||
|
||||
fn collect_rich_impl(
|
||||
node: ego_tree::NodeRef<scraper::Node>,
|
||||
segments: &mut Vec<RichSegment>,
|
||||
buf: &mut String,
|
||||
) {
|
||||
for child in node.children() {
|
||||
match child.value() {
|
||||
scraper::Node::Text(t) => {
|
||||
let s = t.text.trim();
|
||||
if !s.is_empty() {
|
||||
if !buf.is_empty() && !buf.ends_with(|c: char| c.is_whitespace()) {
|
||||
buf.push(' ');
|
||||
}
|
||||
buf.push_str(s);
|
||||
}
|
||||
}
|
||||
scraper::Node::Element(e) => {
|
||||
let tag = e.name();
|
||||
if matches!(tag, "script" | "style" | "noscript") {
|
||||
// skip subtree
|
||||
} else if tag == "a" {
|
||||
if let Some(href) = e.attr("href") {
|
||||
if is_quest_link(href) {
|
||||
let t = std::mem::take(buf);
|
||||
let t = t.trim().to_string();
|
||||
if !t.is_empty() {
|
||||
segments.push(RichSegment::Text { text: t });
|
||||
}
|
||||
let link_text: String = child
|
||||
.descendants()
|
||||
.filter_map(|n| {
|
||||
if let scraper::Node::Text(txt) = n.value() {
|
||||
let s = txt.text.trim();
|
||||
if s.is_empty() { None } else { Some(s.to_string()) }
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
if !link_text.is_empty() {
|
||||
segments.push(RichSegment::QuestLink {
|
||||
text: link_text,
|
||||
href: href.to_string(),
|
||||
});
|
||||
}
|
||||
// Don't recurse into the <a>
|
||||
} else {
|
||||
collect_rich_impl(child, segments, buf);
|
||||
}
|
||||
} else {
|
||||
collect_rich_impl(child, segments, buf);
|
||||
}
|
||||
} else {
|
||||
if matches!(tag, "p" | "br" | "li" | "div" | "tr" | "h1" | "h2" | "h3" | "h4") {
|
||||
if !buf.is_empty() && !buf.ends_with('\n') {
|
||||
buf.push('\n');
|
||||
}
|
||||
}
|
||||
collect_rich_impl(child, segments, buf);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn element_to_rich_text(el: &scraper::ElementRef) -> Vec<RichSegment> {
|
||||
let mut segments: Vec<RichSegment> = Vec::new();
|
||||
let mut buf = String::new();
|
||||
collect_rich_impl(**el, &mut segments, &mut buf);
|
||||
// Flush remaining text
|
||||
let mut result = String::new();
|
||||
let mut prev_empty = false;
|
||||
for line in buf.lines() {
|
||||
let t = line.trim();
|
||||
if t.is_empty() {
|
||||
if !prev_empty { result.push('\n'); }
|
||||
prev_empty = true;
|
||||
} else {
|
||||
result.push_str(t);
|
||||
result.push('\n');
|
||||
prev_empty = false;
|
||||
}
|
||||
}
|
||||
let t = result.trim().to_string();
|
||||
if !t.is_empty() {
|
||||
segments.push(RichSegment::Text { text: t });
|
||||
}
|
||||
segments
|
||||
}
|
||||
|
||||
fn is_date_meta(text: &str) -> bool {
|
||||
let lower = text.to_lowercase();
|
||||
// Matches Weebly blog metadata lines like "Publié le 01/01/2024" or "Mis à jour le …"
|
||||
|
||||
Reference in New Issue
Block a user