1use anyhow::{Context, Result};
2use std::fs;
3use std::io::IsTerminal;
4use std::path::{Path, PathBuf};
5
6pub fn normalize_baseurl(baseurl: &str) -> String {
8 baseurl.trim_end_matches('/').to_string()
9}
10
11pub fn slugify(input: &str) -> String {
13 let mut out = String::new();
14 let mut last_dash = false;
15 for ch in input.chars() {
16 if ch.is_ascii_alphanumeric() {
17 out.push(ch.to_ascii_lowercase());
18 last_dash = false;
19 } else if !last_dash {
20 out.push('-');
21 last_dash = true;
22 }
23 }
24 while out.starts_with('-') {
25 out.remove(0);
26 }
27 while out.ends_with('-') {
28 out.pop();
29 }
30 if out.is_empty() {
31 "untitled".to_string()
32 } else {
33 out
34 }
35}
36
37pub fn ensure_dir(path: &Path) -> Result<()> {
39 fs::create_dir_all(path).with_context(|| format!("creating {}", path.display()))?;
40 Ok(())
41}
42
43pub fn resolve_topic_path(
45 provided: Option<&Path>,
46 title: &str,
47 default_dir: &Path,
48) -> Result<PathBuf> {
49 let filename = format!("{}.md", slugify(title));
50 match provided {
51 Some(path) if path.exists() && path.is_dir() => Ok(path.join(filename)),
52 Some(path) if path.extension().is_some() => Ok(path.to_path_buf()),
53 Some(path) => Ok(path.join(filename)),
54 None => Ok(default_dir.join(filename)),
55 }
56}
57
58pub fn read_markdown(path: &Path) -> Result<String> {
60 let raw = fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
61 Ok(raw)
62}
63
64pub fn write_markdown(path: &Path, content: &str) -> Result<()> {
66 if let Some(parent) = path.parent() {
67 ensure_dir(parent)?;
68 }
69 fs::write(path, content).with_context(|| format!("writing {}", path.display()))?;
70 Ok(())
71}
72
73fn color_mode() -> &'static str {
74 match std::env::var("DSC_COLOR") {
75 Ok(value) => match value.trim().to_ascii_lowercase().as_str() {
76 "always" => "always",
77 "never" => "never",
78 _ => "auto",
79 },
80 Err(_) => "auto",
81 }
82}
83
84fn color_allowed_for_stdout() -> bool {
85 if std::env::var_os("NO_COLOR").is_some() {
86 return false;
87 }
88 match color_mode() {
89 "always" => true,
90 "never" => false,
91 _ => std::io::stdout().is_terminal(),
92 }
93}
94
95fn discourse_color_code(key: &str) -> u8 {
96 const COLORS: [u8; 12] = [31, 32, 33, 34, 35, 36, 91, 92, 93, 94, 95, 96];
97 let hash = key.bytes().fold(0usize, |acc, b| {
98 acc.wrapping_mul(31).wrapping_add(b as usize)
99 });
100 COLORS[hash % COLORS.len()]
101}
102
103pub fn color_discourse_label(label: &str, key: &str) -> String {
104 if !color_allowed_for_stdout() {
105 return label.to_string();
106 }
107 let code = discourse_color_code(key);
108 format!("\x1b[1;{}m{}\x1b[0m", code, label)
109}