use percent_encoding::{percent_decode_str, NON_ALPHANUMERIC};
use std::path::Path;
pub fn split_anchor(input: &str) -> (&str, Option<String>) {
if let Some(idx) = input.find('#') {
let path = &input[..idx];
let anchor = input[idx + 1..].to_string();
let decoded_anchor = percent_decode_str(&anchor).decode_utf8_lossy().to_string();
(path, Some(decoded_anchor))
} else {
(input, None)
}
}
pub fn normalize_case(path: &str) -> String {
#[cfg(target_os = "macos")]
{
path.to_lowercase()
}
#[cfg(target_os = "windows")]
{
path.to_lowercase()
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
path.to_string()
}
}
pub fn normalize_path(path: &Path) -> Option<String> {
let mut components = Vec::new();
for component in path.components() {
match component {
std::path::Component::Normal(c) => components.push(c.to_string_lossy().to_string()),
std::path::Component::ParentDir => {
if components.is_empty() {
return None;
}
components.pop();
}
std::path::Component::CurDir => {}
std::path::Component::RootDir | std::path::Component::Prefix(_) => {
components.push(String::new());
}
}
}
Some(components.join("/"))
}
#[allow(dead_code)]
pub fn is_safe_path(path: &str) -> bool {
if path.is_empty() || path == "." {
return true;
}
if path.starts_with('/') {
return false;
}
let path_obj = Path::new(path);
for component in path_obj.components() {
if matches!(component, std::path::Component::ParentDir) {
return false;
}
}
true
}
pub fn extract_wiki_links(text: &str) -> Vec<String> {
let mut links = Vec::new();
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
if c == '[' {
if let Some(&next_c) = chars.peek() {
if next_c == '[' {
chars.next(); let mut target = String::new();
let mut depth = 1;
for c in chars.by_ref() {
if c == '[' {
depth += 1;
target.push(c);
} else if c == ']' {
depth -= 1;
if depth == 0 {
if let Some(pipe_idx) = target.find('|') {
target.truncate(pipe_idx);
}
links.push(target.trim().to_string());
break;
}
target.push(c);
} else {
target.push(c);
}
}
}
}
}
}
links
}