Ole Jakob Elle

69 papers A* 4A 1B 1C 1Journal 35Unranked 27
YearRankTypeTitle / Venue / Authors
2025 J jnl
Biomed. Signal Process. Control.
Håvard Bjørke Jenssen, Varatharajan Nainamalai, Egidijus Pelanis, Rahul P. Kumar, Andreas Abildgaard, Finn Kristian Kolrud, Bjørn Edwin, Jingfeng Jiang, Joseph Vettukattil, Ole Jakob Elle, Åsmund Avdem Fretland
2025 J jnl
Frontiers Robotics AI
Seyed MohammadReza Sajadi, Abbas Tariverdi, Henrik Brun, Ole Jakob Elle, Kim Mathiassen
2025 conf
ICMRE
Seyed MohammadReza Sajadi, Kim Mathiassen, Håvard Solvin, Henrik Brun, Per Hjalmar Lehne, Ole Jakob Elle
2024 J jnl
CoRR
Gabriella d'Albenzio, Ruoyan Meng, Davit L. Aghayan, Egidijus Pelanis, Rebecca Hisey, Sarkis Drejian, Åsmund Avdem Fretland, Ole Jakob Elle, Bjørn Edwin, Rafael Palomar
2024 J jnl
IEEE Trans Autom. Sci. Eng.
Abbas Tariverdi, Ulysse Côté Allard, Kim Mathiassen, Ole Jakob Elle, Håvard Kalvøy, Ørjan Grøttem Martinsen, Jim Torresen
2024 conf
EMBC
Pravda Jith Ray Prasad, Åsmund Avdem Fretland, Fritz Albregtsen, Ole Jakob Elle, Rahul P. Kumar
2023 conf
EMBC
Eirik G. Homlong, Rahul P. Kumar, Ole Jakob Elle, Ola Wiig
2022 J jnl
IEEE J. Biomed. Health Informatics
Ali Wajdan, Tollef Struksnes Jahren, Manuel Villegas-Martínez, Faraz H. Khan, Per Steinar Halvorsen, Hans Henrik Odland, Ole Jakob Elle, Anne H. Schistad Solberg, Espen W. Remme
2022 J jnl
Artif. Intell. Medicine
Shanmugapriya Survarachakan, Pravda Jith Ray Prasad, Rabia Naseem, Javier Pérez de Frutos, Rahul Prasanna Kumar, Thomas Langø, Faouzi Alaya Cheikh, Ole Jakob Elle, Frank Lindseth
2022 conf
SII
Seyed MohammadReza Sajadi, Kim Mathiassen, Henrik Brun, Ole Jakob Elle
2022 conf
EMBC
Inger A. Grunbeck, Andrea Teatini, Rahul P. Kumar, Ole Jakob Elle, Ola Wiig
2022 C conf
CoDIT
Abbas Tariverdi, Kim Mathiassen, Vegard Søyseth, Håvard Kalvøy, Ole Jakob Elle, Jim Tørresen, Ørjan Grøttem Martinsen, Mats Erling Høvin
2022 J jnl
Frontiers Virtual Real.
Dharani Maddali, Henrik Brun, Gabriel Kiss, Jon Mikkelsen Hjelmervik, Ole Jakob Elle
2022 J jnl
CoRR
Javier Pérez de Frutos, André Pedersen, Egidijus Pelanis, David Bouget, Shanmugapriya Survarachakan, Thomas Langø, Ole Jakob Elle, Frank Lindseth
2021 J jnl
Frontiers Robotics AI
Abbas Tariverdi, Venkatasubramanian Kalpathy Venkiteswaran, Michiel Richter, Ole Jakob Elle, Jim Tørresen, Kim Mathiassen, Sarthak Misra, Ørjan Grøttem Martinsen
2021 J jnl
IEEE Trans. Biomed. Eng.
Magnus Reinsfelt Krogh, Per Steinar Halvorsen, Ole-Johannes H. N. Grymyr, Jacob Bergsland, Ole Jakob Elle, Erik Fosse, Espen W. Remme
2021 J jnl
Medical Image Anal.
Egidijus Pelanis, Andrea Teatini, Benjamin Eigl, Alois Regensburger, Amilcar Alzaga, Rahul Prasanna Kumar, Tobias Rudolph, Davit L. Aghayan, Carina Riediger, Niclas Kvarnström, Ole Jakob Elle, Bjørn Edwin
2021 J jnl
Int. J. Comput. Assist. Radiol. Surg.
Andrea Teatini, Rahul P. Kumar, Ole Jakob Elle, Ola Wiig
2021 conf
Computer-Aided Diagnosis
Pravda Jith Ray Prasad, Ole Jakob Elle, Frank Lindseth, Fritz Albregtsen, Rahul Prasanna Kumar
2021 J jnl
CoRR
Abbas Tariverdi, Ulysse Côté Allard, Kim Mathiassen, Ole Jakob Elle, Håvard Kalvøy, Ørjan Grøttem Martinsen, Jim Tørresen
2020 J jnl
CoRR
Orestis Zachariadis, Andrea Teatini, Nitin Satpute, Juan Gómez-Luna, Onur Mutlu, Ole Jakob Elle, Joaquín Olivares
2020 J jnl
Comput. Methods Programs Biomed.
Orestis Zachariadis, Andrea Teatini, Nitin Satpute, Juan Gómez-Luna, Onur Mutlu, Ole Jakob Elle, Joaquín Olivares
2020 conf
Image Processing: Algorithms and Systems
Congcong Wang, Faouzi Alaya Cheikh, Azeddine Beghdadi, Ole Jakob Elle
2020 J jnl
Comput. Methods Programs Biomed.
Nitin Satpute, Rabia Naseem, Egidijus Pelanis, Juan Gómez-Luna, Faouzi Alaya Cheikh, Ole Jakob Elle, Joaquín Olivares
2020 conf
Image Perception, Observer Performance, and Technology Assessment
Zohaib Amjad Khan, Azeddine Beghdadi, Faouzi Alaya Cheikh, Mounir Kaaniche, Egidijus Pelanis, Rafael Palomar, Åsmund Avdem Fretland, Bjørn Edwin, Ole Jakob Elle
2019 J jnl
CoRR
Congcong Wang, Faouzi Alaya Cheikh, Azeddine Beghdadi, Ole Jakob Elle
2019 conf
EUVIP
Rabia Naseem, Faouzi Alaya Cheikh, Azeddine Beghdadi, Ole Jakob Elle, Frank Lindseth
2019 J jnl
IEEE Trans. Medical Imaging
Bilel Sdiri, Mounir Kaaniche, Faouzi Alaya Cheikh, Azeddine Beghdadi, Ole Jakob Elle
2019 conf
EMBC
Ali Wajdan, Magnus Reinsfelt Krogh, Manuel Villegas-Martínez, Per Steinar Halvorsen, Ole-Johannes H. N. Grymyr, Ole Jakob Elle, Espen W. Remme
2019 A* conf
ICRA
Justinas Miseikis, Inka Brijacak, Saeed Yahyanejad, Kyrre Glette, Ole Jakob Elle, Jim Tørresen
2019 J jnl
CoRR
Justinas Miseikis, Inka Brijacak, Saeed Yahyanejad, Kyrre Glette, Ole Jakob Elle, Jim Tørresen
2018 J jnl
CoRR
Congcong Wang, Faouzi Alaya Cheikh, Mounir Kaaniche, Ole Jakob Elle
2018 J jnl
Int. J. Parallel Program.
Rafael Palomar, Juan Gómez-Luna, Faouzi Alaya Cheikh, Joaquín Olivares Bueno, Ole Jakob Elle
2018 J jnl
Int. J. Comput. Assist. Radiol. Surg.
Javier Pérez de Frutos, Erlend Fagertun Hofstad, Ole Vegard Solberg, Geir Arne Tangen, Frank Lindseth, Thomas Langø, Ole Jakob Elle, Ronald Mårvik
2018 conf
Image-Guided Procedures
Congcong Wang, Faouzi Alaya Cheikh, Mounir Kaaniche, Ole Jakob Elle
2018 conf
UR
Justinas Miseikis, Inka Brijacak, Saeed Yahyanejad, Kyrre Glette, Ole Jakob Elle, Jim Tørresen
2018 J jnl
CoRR
Justinas Miseikis, Inka Brijacak, Saeed Yahyanejad, Kyrre Glette, Ole Jakob Elle, Jim Tørresen
2018 conf
AIM
Justinas Miseikis, Patrick Knöbelreiter, Inka Brijacak, Saeed Yahyanejad, Kyrre Glette, Ole Jakob Elle, Jim Tørresen
2018 J jnl
CoRR
Justinas Miseikis, Patrick Knöbelreiter, Inka Brijacak, Saeed Yahyanejad, Kyrre Glette, Ole Jakob Elle, Jim Tørresen
2018 conf
ISR
Justinas Miseikis, Inka Brijacak, Saeed Yahyanejad, Kyrre Glette, Ole Jakob Elle, Jim Tørresen
2018 J jnl
CoRR
Justinas Miseikis, Inka Brijacak, Saeed Yahyanejad, Kyrre Glette, Ole Jakob Elle, Jim Tørresen
2018 conf
CVCS
Andrea Teatini, Congcong Wang, Rafael Palomar, Faouzi Alaya Cheikh, Azeddine Beghdadi, Bjørn Edwin, Ole Jakob Elle
2017 J jnl
Comput. Methods Programs Biomed.
Rafael Palomar, Faouzi Alaya Cheikh, Bjørn Edwin, Åsmund Fretland, Azeddine Beghdadi, Ole Jakob Elle
2017 J jnl
IEEE Trans. Control. Syst. Technol.
Kim Mathiassen, Diego Dall'Alba, Riccardo Muradore, Paolo Fiorini, Ole Jakob Elle
2016 J jnl
Frontiers Robotics AI
Kim Mathiassen, Jørgen Enger Fjellin, Kyrre Glette, Per Kristian Hol, Ole Jakob Elle
2016 conf
IQSP
Bilel Sdiri, Azeddine Beghdadi, Faouzi Alaya Cheikh, Marius Pedersen, Ole Jakob Elle
2016 J jnl
CoRR
Justinas Miseikis, Kyrre Glette, Ole Jakob Elle, Jim Tørresen
2016 conf
SII
Justinas Miseikis, Kyrre Glette, Ole Jakob Elle, Jim Tørresen
2016 J jnl
Comput. Biol. Medicine
Igor Sevo, Aleksej Avramovic, Ilangko Balasingham, Ole Jakob Elle, Jacob Bergsland, Lars Aabakken
2016 conf
MIAR
Rafael Palomar, Faouzi Alaya Cheikh, Azeddine Beghdadi, Ole Jakob Elle
2016 J jnl
CoRR
Justinas Miseikis, Kyrre Glette, Ole Jakob Elle, Jim Tørresen
2016 conf
SSCI
Justinas Miseikis, Kyrre Glette, Ole Jakob Elle, Jim Tørresen
2016 J jnl
Comput. Medical Imaging Graph.
Rafael Palomar, Faouzi Alaya Cheikh, Bjørn Edwin, Azeddine Beghdadi, Ole Jakob Elle
2016 A* conf
ICRA
Kim Mathiassen, Kyrre Glette, Ole Jakob Elle
2014 J jnl
Int. J. Comput. Assist. Radiol. Surg.
Abubakr Eldirdiri, Frédéric Courivaud, Rafael Palomar, Per Kristian Hol, Ole Jakob Elle
2013 B conf
Image Processing
Rahul P. Kumar, Fritz Albregtsen, Martin Reimers, Thomas Langø, Bjørn Edwin, Ole Jakob Elle
2013 A* conf
ICRA
Kim Mathiassen, Diego Dall'Alba, Riccardo Muradore, Paolo Fiorini, Ole Jakob Elle
2013 conf
CVCS
Rahul Prasanna Kumar, Erik-Jan Rijkhorst, Oliver M. Geier, Dean C. Barratt, Ole Jakob Elle
2012 J jnl
IEEE Trans. Haptics
Edvard Naerum, Ole Jakob Elle, Blake Hannaford
2010 J jnl
Int. J. Image Graph.
Wajid Ali, Tangui Morvan, Petter Risholm, Ole Jakob Elle, Eigil Samset
2010 conf
BODYNETS
Lars Hoff, Kristin Imenes, Lars A. Fleischer, Per Steinar Halvorsen, Andreas Espinoza, Espen W. Remme, Ole Jakob Elle, Erik Fosse
2008 A conf
IROS
Edvard Naerum, Jordi Cornellà, Ole Jakob Elle
2008 conf
ICECS
Craig Lowrie, Marc P. Y. Desmulliez, Lars Hoff, Ole Jakob Elle, Erik Fosse
2008 conf
MICCAI (2)
Jordi Cornellà, Ole Jakob Elle, Wajid Ali, Eigil Samset
2008 A* conf
ICRA
Edvard Naerum, Jordi Cornellà, Ole Jakob Elle
2003 conf
CARS
Lars Aurdal, Daniel Bengtsson, Ole Jakob Elle, Eigil Samset
2003 conf
CARS
Ole Jakob Elle, Lars Aurdal, Lene Rødmyr, Guro Øsebak, Eigil Samset, Erik Fosse
2001 conf
CARS
A. Austad, Ole Jakob Elle, Jan Sigurd Røtnes
2001 conf
CARS
Jan Sigurd Røtnes, Johannes Kaasa, Geir Westgaard, Eivind Myrold Eriksen, Per Øyvind Hvidsten, Kyrre Strøm, Vidar Sørhus, Yvon Halbwachs, Ole Jakob Elle, Erik Fosse
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>
"#
    )
}