Ram Mohana Reddy Guddeti

69 papers B 3C 4Misc 4Journal 33Unranked 25
YearRankTypeTitle / Venue / Authors
2025 J jnl
CoRR
Aryan Kashyap Naveen, Bhuvanesh Singla, Raajan Rajesh Wankhade, Shreesha M, Ramu S, Ram Mohana Reddy Guddeti
2025 J jnl
Multim. Tools Appl.
S. Ramu, Ram Mohana Reddy Guddeti, Biju R. Mohan
2025 J jnl
IEEE Access
Madhusmita Das, Biju R. Mohan, Ram Mohana Reddy Guddeti
2025 conf
ICOIN
Madhusmita Das, Biju R. Mohan, Ram Mohana Reddy Guddeti
2024 conf
ICCCNT
Sona Mundody, Ram Mohana Reddy Guddeti
2024 J jnl
Multim. Tools Appl.
Sona Mundody, Ram Mohana Reddy Guddeti
2024 conf
ICCCNT
Nikhil Verma, Sona Mundody, Ram Mohana Reddy Guddeti
2024 conf
ICCCNT
Shivesh Bhardwaj, S. Ramu, Ram Mohana Reddy Guddeti
2024 J jnl
Neural Comput. Appl.
Rashmi M., Ram Mohana Reddy Guddeti
2024 Misc conf
FRUCT
Smruthi Bhat, V. Ragha Sai, Sona Mundody, Ram Mohana Reddy Guddeti
2024 conf
ICCCNT
Madhusmita Das, Biju R. Mohan, Ram Mohana Reddy Guddeti, Mohd Dilshad, Fahim Ahmed, Sudarshan Zunja, K. P. Minank
2023 conf
ICCCNT
Harshit Chopra, Sona Mundody, Ram Mohana Reddy Guddeti
2023 conf
ICCCNT
Nidhi Ekka, Sona Mundody, Ram Mohana Reddy Guddeti
2023 J jnl
IEEE Access
M. Revanesh, Bhawana Rudra, Ram Mohana Reddy Guddeti
2023 J jnl
Multim. Tools Appl.
Sampat Kumar Ghosh, Rashmi M., Biju R. Mohan, Ram Mohana Reddy Guddeti
2023 J jnl
Appl. Intell.
Rashmi M., Ram Mohana Reddy Guddeti
2023 conf
ICCCNT
Madhusmita Das, Biju R. Mohan, Ram Mohana Reddy Guddeti
2022 C conf
SNPD
Madhusmita Das, Biju R. Mohan, Ram Mohana Reddy Guddeti
2022 conf
ICAISC (2)
S. Ramu, Ram Mohana Reddy Guddeti, Biju R. Mohan
2022 J jnl
J. Vis. Commun. Image Represent.
Rashmi M., Ram Mohana Reddy Guddeti
2022 J jnl
J. Netw. Syst. Manag.
Natesha B. V., Ram Mohana Reddy Guddeti
2021 J jnl
IEEE Access
Hafeez Ali A., Sanjeev U. Rao, Swaroop Ranganath, T. S. Ashwin, Ram Mohana Reddy Guddeti
2021 J jnl
Wirel. Pers. Commun.
Sugandh Kumar Chaudhary, Syed Yousuff, N. P. Meghana, T. S. Ashwin, Ram Mohana Reddy Guddeti
2021 J jnl
J. Netw. Comput. Appl.
Natesha B. V., Ram Mohana Reddy Guddeti
2021 conf
PReMI
Niranjan, Natesha B. V., M. Rashmi, Ram Mohana Reddy Guddeti
2021 conf
ACLING
Saurabh Agarwala, Aniketh Anagawadi, Ram Mohana Reddy Guddeti
2021 J jnl
IEEE Trans. Ind. Informatics
Natesha B. V., Ram Mohana Reddy Guddeti
2021 conf
PReMI
K. N. Gagandeep, Atharv R. Belagali, M. Rashmi, Ram Mohana Reddy Guddeti
2021 J jnl
Multim. Tools Appl.
M. Rashmi, T. S. Ashwin, Ram Mohana Reddy Guddeti
2020 J jnl
IEEE Trans. Serv. Comput.
Shridhar G. Domanal, Ram Mohana Reddy Guddeti, Rajkumar Buyya
2020 J jnl
CoRR
Subhayan Mukherjee, Ram Mohana Reddy Guddeti
2020 J jnl
Future Gener. Comput. Syst.
Ashwin T. S., Ram Mohana Reddy Guddeti
2020 J jnl
Educ. Inf. Technol.
T. S. Ashwin, Ram Mohana Reddy Guddeti
2020 J jnl
CoRR
Subhayan Mukherjee, Ram Mohana Reddy Guddeti
2020 conf
FICTA (1)
Natesha B. V., Ram Mohana Reddy Guddeti
2020 J jnl
User Model. User Adapt. Interact.
T. S. Ashwin, Ram Mohana Reddy Guddeti
2020 J jnl
Signal Image Video Process.
Sayani Banerjee, T. S. Ashwin, Ram Mohana Reddy Guddeti
2020 Misc conf
COMSNETS
M. Rashmi, Ram Mohana Reddy Guddeti
2019 C conf
TENCON
Sayani Banerjee, T. S. Ashwin, Ram Mohana Reddy Guddeti
2019 J jnl
IEEE Access
Abhishek Tripathi, Ashwin T. S., Ram Mohana Reddy Guddeti
2019 C conf
TENCON
Vinita Yadav, Natesha B. V., Ram Mohana Reddy Guddeti
2019 C conf
TENCON
A. V. Nandini, Aniket Dwivedi, Nilita Anil Kumar, T. S. Ashwin, V. Vishnuvardhan, Ram Mohana Reddy Guddeti
2019 J jnl
Multim. Tools Appl.
Sujit Kumar Gupta, Ashwin T. S., Ram Mohana Reddy Guddeti
2019 J jnl
Multim. Tools Appl.
Md Shahzad Alam, Natesha B. V., Ashwin T. S., Ram Mohana Reddy Guddeti
2019 J jnl
IEEE Access
T. S. Ashwin, Ram Mohana Reddy Guddeti
2018 B conf
ICALT
Abhishek Tripathi, Ashwin T. S., Ram Mohana Reddy Guddeti
2018 Misc conf
COMSNETS
Aparna Sarswat, Ram Mohana Reddy Guddeti
2018 B conf
ICALT
Sujit Kumar Gupta, Ashwin T. S., Ram Mohana Reddy Guddeti
2018 conf
RAIT
Neeraj Kumar Sharma, Priyanka Sharma, Ram Mohana Reddy Guddeti
2018 conf
SKG
Natesha B. V., Neeraj Kumar Sharma, Shridhar G. Domanal, Ram Mohana Reddy Guddeti
2018 conf
UCC Companion
Natesha B. V., Ram Mohana Reddy Guddeti
2018 conf
ANTS
G. Rakshith, M. V. Rahul, G. S. Sanjay, Natesha B. V., Ram Mohana Reddy Guddeti
2018 B conf
ICALT
Ashwin T. S., Ram Mohana Reddy Guddeti
2017 J jnl
Soc. Netw. Anal. Min.
Aparna Sarswat, Vidyadhari Jami, Ram Mohana Reddy Guddeti
2017 J jnl
CoRR
Subhayan Mukherjee, Irene Cheng, Ram Mohana Reddy Guddeti, Anup Basu
2017 conf
PReMI
Vivek Dani, Aparna Sarswat, Vishnu Swaroop, Shridhar G. Domanal, Ram Mohana Reddy Guddeti
2016 conf
IVMSP
Subhayan Mukherjee, Irene Cheng, Ram Mohana Reddy Guddeti, Anup Basu
2016 J jnl
Comput. Commun.
Beerappa R. Chandavarkar, Ram Mohana Reddy Guddeti
2015 conf
ICSC
Monisha Kanakaraj, Ram Mohana Reddy Guddeti
2015 J jnl
Wirel. Pers. Commun.
Beerappa R. Chandavarkar, Ram Mohana Reddy Guddeti
2014 J jnl
Soc. Netw. Anal. Min.
Malhar Anjaria, Ram Mohana Reddy Guddeti
2014 Misc conf
COMSNETS
Malhar Anjaria, Ram Mohana Reddy Guddeti
2014 conf
ICACCI
Hitesh Singhal, Ram Mohana Reddy Guddeti
2014 conf
ICACCI
Kokane Amol T., Ram Mohana Reddy Guddeti
2013 J jnl
CoRR
Sunil Joy Dommati, Ruchi Agrawal, Ram Mohana Reddy Guddeti, S. Sowmya Kamath
2013 conf
ICACCI
Mahesh Chikkanna, Ram Mohana Reddy Guddeti
2013 J jnl
IET Networks
Kiran Manjappa, Ram Mohana Reddy Guddeti
2013 conf
ICACCI
Saarthak Chandra, Sagar M, Pushpanjali Rout, Pranay Khattri, Shridhar G. Domanal, Ram Mohana Reddy Guddeti
2005 conf
ICASSP (5)
Ram Mohana Reddy Guddeti, Bernard Mulgrew
src/main.rs
← Index src/main.rs rust
mod highlight;

use std::collections::BTreeMap;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::time::Instant;

use walkdir::WalkDir;

use highlight::highlight_code;

/// Above this threshold the file is read in full and classified
/// (text or binary) based on its complete content.
const FULL_READ_LIMIT: u64 = 4_000_000; // 4 MB
/// For files larger than the threshold above, we only read a preview
/// of this size (so we never load huge files fully into memory).
const PREVIEW_BYTES: usize = 16_384;
/// How many bytes of binary content to show in the hex dump.
const HEXDUMP_BYTES: usize = 4096;

struct FileResult {
    rel_path: PathBuf, // relative path of the original file
    out_rel: PathBuf,  // relative path of the generated .html file (inside out/)
    label: String,     // label shown in the badge (language, or "binary")
}

enum Unreadable {
    IoError(PathBuf),
}

enum Content {
    /// Successfully decoded text. `truncated` indicates whether the file
    /// is larger than what was actually read (very large files: preview only).
    Text { content: String, truncated: bool, total_size: u64 },
    /// Content that isn't valid UTF-8 text: we show a hex dump instead
    /// of trying to decode it.
    Binary { preview: Vec<u8>, total_size: u64, truncated: bool },
}

/// Reads a file safely: for small files, reads everything and classifies
/// text/binary based on the full content; for large files, reads only a
/// prefix, to avoid loading huge files into memory.
fn read_content(path: &Path, total_size: u64) -> std::io::Result<Content> {
    if total_size <= FULL_READ_LIMIT {
        let bytes = fs::read(path)?;
        match String::from_utf8(bytes) {
            Ok(s) => Ok(Content::Text {
                content: s,
                truncated: false,
                total_size,
            }),
            Err(e) => Ok(Content::Binary {
                preview: e.into_bytes(),
                total_size,
                truncated: false,
            }),
        }
    } else {
        let mut f = fs::File::open(path)?;
        let mut buf = vec![0u8; PREVIEW_BYTES];
        let n = f.read(&mut buf)?;
        buf.truncate(n);

        match std::str::from_utf8(&buf) {
            Ok(s) => Ok(Content::Text {
                content: s.to_string(),
                truncated: true,
                total_size,
            }),
            Err(e) => {
                let valid_up_to = e.valid_up_to();
                // If the error is only due to cutting a multi-byte
                // character in half (common when reading an arbitrary
                // prefix of a text file), still treat it as text.
                if valid_up_to > 0 && buf.len() - valid_up_to <= 4 {
                    let s = std::str::from_utf8(&buf[..valid_up_to]).unwrap().to_string();
                    Ok(Content::Text {
                        content: s,
                        truncated: true,
                        total_size,
                    })
                } else {
                    Ok(Content::Binary {
                        preview: buf,
                        total_size,
                        truncated: true,
                    })
                }
            }
        }
    }
}

/// Tree node used to build the index (index.html)
#[derive(Default)]
struct Node {
    dirs: BTreeMap<String, Node>,
    files: Vec<(String, String, PathBuf)>, // (display_name, label, href relative to the index)
}

impl Node {
    fn insert_file(&mut self, components: &[String], display_name: String, label: String, href: PathBuf) {
        if components.is_empty() {
            self.files.push((display_name, label, href));
        } else {
            let head = &components[0];
            let child = self.dirs.entry(head.clone()).or_default();
            child.insert_file(&components[1..], display_name, label, href);
        }
    }

    fn render(&self, out: &mut String) {
        out.push_str("<ul class=\"tree\">\n");
        for (name, node) in &self.dirs {
            out.push_str(&format!(
                "<li class=\"dir\"><span class=\"dir-name\">\u{1F4C1} {}</span>\n",
                escape_html(name)
            ));
            node.render(out);
            out.push_str("</li>\n");
        }
        for (name, label, href) in &self.files {
            out.push_str(&format!(
                "<li class=\"file\"><a href=\"{}\">\u{1F4C4} {}</a> <span class=\"badge\">{}</span></li>\n",
                href.to_string_lossy().replace('\\', "/"),
                escape_html(name),
                escape_html(label)
            ));
        }
        out.push_str("</ul>\n");
    }
}

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let input_dir = args.get(1).cloned().unwrap_or_else(|| ".".to_string());
    let output_dir = args.get(2).cloned().unwrap_or_else(|| "out".to_string());
    // Extra directory names to exclude, optional, comma-separated
    // (e.g. "node_modules,.git"). Nothing is excluded by default:
    // the generator includes everything it finds, including
    // __pycache__, compiled files, binaries, etc.
    let extra_excludes: Vec<String> = args
        .get(3)
        .map(|s| s.split(',').map(|p| p.trim().to_string()).filter(|p| !p.is_empty()).collect())
        .unwrap_or_default();

    let input = PathBuf::from(&input_dir);
    let output = PathBuf::from(&output_dir);

    if !input.is_dir() {
        eprintln!("Error: '{}' is not a valid directory.", input.display());
        std::process::exit(1);
    }

    if let Err(e) = fs::create_dir_all(&output) {
        eprintln!("Error creating the output directory: {e}");
        std::process::exit(1);
    }

    // Absolute path of the output directory, so we can exclude it from the
    // scan in case it's nested inside the input directory (e.g. input=".",
    // output="out"). This is the only automatic exclusion and it can't be
    // turned off: it only prevents the program from processing the files
    // it is currently writing.
    let output_abs = fs::canonicalize(&output).unwrap_or_else(|_| output.clone());

    println!("Scanning '{}' -> '{}'", input.display(), output.display());
    if !extra_excludes.is_empty() {
        println!("Directories excluded on request: {}", extra_excludes.join(", "));
    }
    let start = Instant::now();

    let mut results: Vec<FileResult> = Vec::new();
    let mut unreadable: Vec<Unreadable> = Vec::new();
    let mut n_binary = 0usize;
    let mut n_truncated = 0usize;

    for entry in WalkDir::new(&input)
        .into_iter()
        .filter_entry(|e| {
            if e.file_type().is_dir() {
                if let Some(name) = e.file_name().to_str() {
                    if extra_excludes.iter().any(|x| x == name) {
                        return false;
                    }
                }
                if let Ok(abs) = fs::canonicalize(e.path()) {
                    if abs == output_abs {
                        return false;
                    }
                }
            }
            true
        })
    {
        let entry = match entry {
            Ok(e) => e,
            Err(_) => continue,
        };

        if !entry.file_type().is_file() {
            continue;
        }

        let path = entry.path();
        let rel_path = match path.strip_prefix(&input) {
            Ok(p) => p.to_path_buf(),
            Err(_) => path.to_path_buf(),
        };

        let meta = match fs::metadata(path) {
            Ok(m) => m,
            Err(_) => {
                unreadable.push(Unreadable::IoError(rel_path));
                continue;
            }
        };

        let content = match read_content(path, meta.len()) {
            Ok(c) => c,
            Err(_) => {
                unreadable.push(Unreadable::IoError(rel_path));
                continue;
            }
        };

        let syntax_lang = detect_language(path);

        let (body_html, label, note): (String, String, Option<String>) = match &content {
            Content::Text { content, truncated, total_size } => {
                let body = highlight_code(content, syntax_lang);
                let note = if *truncated {
                    n_truncated += 1;
                    Some(format!(
                        "Truncated preview: showing the first {} bytes out of {} total.",
                        content.len(),
                        total_size
                    ))
                } else {
                    None
                };
                (body, syntax_lang.to_string(), note)
            }
            Content::Binary { preview, total_size, truncated } => {
                n_binary += 1;
                let dump = hex_dump(preview, HEXDUMP_BYTES);
                let shown = preview.len().min(HEXDUMP_BYTES);
                let note = Some(format!(
                    "Binary file: {} bytes total. Showing the first {} bytes as hex.{}",
                    total_size,
                    shown,
                    if *truncated { " (only the first bytes of the file were read)" } else { "" }
                ));
                (escape_html(&dump), "binary".to_string(), note)
            }
        };

        let mut out_rel = rel_path.clone();
        let new_name = format!(
            "{}.html",
            out_rel.file_name().unwrap_or_default().to_string_lossy()
        );
        out_rel.set_file_name(new_name);

        let out_path = output.join(&out_rel);
        if let Some(parent) = out_path.parent() {
            let _ = fs::create_dir_all(parent);
        }

        let depth = out_rel.components().count().saturating_sub(1);
        let back_to_index = "../".repeat(depth);

        let html = render_file_html(
            &rel_path.to_string_lossy(),
            &label,
            &body_html,
            note.as_deref(),
            &back_to_index,
        );

        if fs::write(&out_path, html).is_ok() {
            results.push(FileResult {
                rel_path,
                out_rel,
                label,
            });
        }
    }

    // Build the tree for the index
    let mut root = Node::default();
    for r in &results {
        let components: Vec<String> = r
            .rel_path
            .parent()
            .map(|p| {
                p.components()
                    .map(|c| c.as_os_str().to_string_lossy().to_string())
                    .filter(|s| !s.is_empty())
                    .collect()
            })
            .unwrap_or_default();
        let display_name = r
            .rel_path
            .file_name()
            .unwrap_or_default()
            .to_string_lossy()
            .to_string();
        root.insert_file(
            &components,
            display_name,
            r.label.clone(),
            r.out_rel.clone(),
        );
    }

    let mut tree_html = String::new();
    root.render(&mut tree_html);

    let index_html = render_index_html(&tree_html, results.len(), n_binary, n_truncated);
    let _ = fs::write(output.join("index.html"), index_html);

    let elapsed = start.elapsed();
    println!(
        "Done in {:.2?}: {} files included ({} binary, {} with truncated preview), {} unreadable.",
        elapsed,
        results.len(),
        n_binary,
        n_truncated,
        unreadable.len()
    );
    for u in &unreadable {
        match u {
            Unreadable::IoError(p) => println!("  unreadable (I/O error): {}", p.display()),
        }
    }
    println!("Open {}/index.html in your browser.", output.display());
}

/// Produces a hex dump (in the style of `xxd -C`) of the first `limit` bytes.
fn hex_dump(bytes: &[u8], limit: usize) -> String {
    let show = &bytes[..bytes.len().min(limit)];
    let mut out = String::with_capacity(show.len() * 4);
    for (i, chunk) in show.chunks(16).enumerate() {
        let offset = i * 16;
        out.push_str(&format!("{:08x}  ", offset));
        for (j, b) in chunk.iter().enumerate() {
            out.push_str(&format!("{:02x} ", b));
            if j == 7 {
                out.push(' ');
            }
        }
        let missing = 16usize.saturating_sub(chunk.len());
        for _ in 0..missing {
            out.push_str("   ");
        }
        out.push_str(" |");
        for b in chunk {
            let c = *b;
            if (0x20..0x7f).contains(&c) {
                out.push(c as char);
            } else {
                out.push('.');
            }
        }
        out.push_str("|\n");
    }
    out
}

/// Determines the language (used to pick syntax-highlighting rules)
/// from the file's name/extension. This is only used when the content
/// turns out to actually be text; binary files are classified as such
/// regardless by read_content.
fn detect_language(path: &Path) -> &'static str {
    let file_name = path
        .file_name()
        .map(|s| s.to_string_lossy().to_lowercase())
        .unwrap_or_default();

    match file_name.as_str() {
        "dockerfile" => return "dockerfile",
        "makefile" | "gnumakefile" => return "makefile",
        "cmakelists.txt" => return "cmake",
        ".gitignore" | ".dockerignore" | ".npmignore" => return "plaintext",
        "cargo.toml" | "cargo.lock" => return "ini",
        _ => {}
    }

    let ext = path
        .extension()
        .map(|s| s.to_string_lossy().to_lowercase())
        .unwrap_or_default();

    match ext.as_str() {
        "py" | "pyw" | "pyi" => "python",
        "rs" => "rust",
        "js" | "mjs" | "cjs" => "javascript",
        "jsx" => "javascript",
        "ts" => "typescript",
        "tsx" => "typescript",
        "java" => "java",
        "c" => "c",
        "h" => "c",
        "hpp" | "hh" | "hxx" => "cpp",
        "cpp" | "cc" | "cxx" => "cpp",
        "cs" => "csharp",
        "go" => "go",
        "rb" => "ruby",
        "php" => "php",
        "html" | "htm" => "xml",
        "css" => "css",
        "scss" | "sass" => "scss",
        "less" => "less",
        "json" => "json",
        "xml" => "xml",
        "yaml" | "yml" => "yaml",
        "toml" | "ini" | "cfg" | "conf" => "ini",
        "sh" | "bash" | "zsh" => "bash",
        "sql" => "sql",
        "md" | "markdown" => "markdown",
        "kt" | "kts" => "kotlin",
        "swift" => "swift",
        "lua" => "lua",
        "r" => "r",
        "pl" | "pm" => "perl",
        "groovy" | "gradle" => "groovy",
        "vb" => "vbnet",
        "graphql" | "gql" => "graphql",
        "diff" | "patch" => "diff",
        "bat" | "cmd" => "dos",
        "ps1" => "powershell",
        "vue" => "xml",
        "txt" => "plaintext",
        _ => "plaintext",
    }
}

fn escape_html(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        match c {
            '&' => out.push_str("&amp;"),
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            '"' => out.push_str("&quot;"),
            '\'' => out.push_str("&#39;"),
            _ => out.push(c),
        }
    }
    out
}

const STYLE: &str = r#"
:root {
  --bg: #0d1117;
  --panel: #161b22;
  --text: #c9d1d9;
  --muted: #8b949e;
  --accent: #58a6ff;
  --border: #30363d;
}
* { box-sizing: border-box; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}
header {
  padding: 14px 20px;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
header a { color: var(--accent); text-decoration: none; font-weight: 600; }
header a:hover { text-decoration: underline; }
.path { color: var(--muted); font-size: 0.9em; word-break: break-all; }
.note {
  padding: 10px 20px;
  background: #21262d;
  border-bottom: 1px solid var(--border);
  color: var(--muted);
  font-size: 0.85em;
}
main { padding: 0; }
pre {
  margin: 0;
  padding: 16px 20px 40px 20px;
  overflow-x: auto;
  font-size: 13px;
  line-height: 1.5;
}
code { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; }
.container { max-width: 1000px; margin: 30px auto; padding: 0 20px 60px; }
h1 { font-size: 1.6em; }
.meta { color: var(--muted); font-size: 0.9em; margin-bottom: 20px; }
ul.tree { list-style: none; padding-left: 18px; margin: 6px 0; }
ul.tree > li { margin: 4px 0; }
.dir-name { font-weight: 600; color: var(--text); }
.file a { color: var(--accent); text-decoration: none; }
.file a:hover { text-decoration: underline; }
.badge {
  display: inline-block;
  font-size: 0.72em;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 1px 8px;
  margin-left: 6px;
}
.skipped { color: var(--muted); font-size: 0.85em; margin-top: 30px; }

/* Syntax highlighting: generated in Rust, no JavaScript involved. */
.c { color: #8b949e; font-style: italic; }
.s { color: #a5d6ff; }
.n { color: #79c0ff; }
.k { color: #ff7b72; font-weight: 600; }
"#;

fn render_file_html(rel_path: &str, label: &str, body_html: &str, note: Option<&str>, back_to_index: &str) -> String {
    let title = escape_html(rel_path);
    let note_html = match note {
        Some(n) => format!("<div class=\"note\">{}</div>\n", escape_html(n)),
        None => String::new(),
    };
    format!(
        r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{title}</title>
<style>{STYLE}</style>
</head>
<body>
<header>
  <a href="{back_to_index}index.html">&larr; Index</a>
  <span class="path">{title}</span>
  <span class="badge">{label}</span>
</header>
{note_html}<main>
<pre><code>{body_html}</code></pre>
</main>
</body>
</html>
"#
    )
}

fn render_index_html(tree_html: &str, n_ok: usize, n_binary: usize, n_truncated: usize) -> String {
    let mut extra = String::new();
    if n_binary > 0 {
        extra.push_str(&format!(
            "<p class=\"skipped\">{} binary files included with a hex-dump preview.</p>",
            n_binary
        ));
    }
    if n_truncated > 0 {
        extra.push_str(&format!(
            "<p class=\"skipped\">{} very large files shown as a truncated preview only.</p>",
            n_truncated
        ));
    }

    let folder_emoji = '\u{1F4C2}';
    format!(
        r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Source index</title>
<style>{STYLE}</style>
</head>
<body>
<div class="container">
<h1>{folder_emoji} Source index</h1>
<p class="meta">{n_ok} files included in total.</p>
{tree_html}
{extra}
</div>
</body>
</html>
"#
    )
}