Jamal Atif

119 papers A* 14A 10B 3C 3Misc 6Journal 58Unranked 24
YearRankTypeTitle / Venue / Authors
2026 J jnl
CoRR
Hanna Benarroch, Jamal Atif, Olivier Cappé
2026 J jnl
CoRR
Valentin Dorseuil, Jamal Atif, Olivier Cappé
2025 J jnl
Trans. Mach. Learn. Res.
Ilana Sebag, Muni Sreenivas Pydi, Jean-Yves Franceschi, Alain Rakotomamonjy, Mike Gartrell, Jamal Atif, Alexandre Allauzen
2025 J jnl
CoRR
Danil Savine, Muni Sreenivas Pydi, Jamal Atif, Olivier Cappé
2025 J jnl
CoRR
Ilana Sebag, Jean-Yves Franceschi, Alain Rakotomamonjy, Alexandre Allauzen, Jamal Atif
2024 A* conf
NeurIPS
Edwige Cyffers, Muni Sreenivas Pydi, Jamal Atif, Olivier Cappé
2024 J jnl
CoRR
Edwige Cyffers, Muni Sreenivas Pydi, Jamal Atif, Olivier Cappé
2023 conf
PKDD/ECML Workshops (5)
Eric Benhamou, Jean-Jacques Ohana, Beatrice Guez, David Saltiel, Rida Laraki, Jamal Atif
2023 J jnl
CoRR
Ilana Sebag, Muni Sreenivas Pydi, Jean-Yves Franceschi, Alain Rakotomamonjy, Mike Gartrell, Jamal Atif, Alexandre Allauzen
2023 conf
LIDTA
Raphael Krief, Eric Benhamou, Beatrice Guez, Jean-Jacques Ohana, David Saltiel, Rida Laraki, Jamal Atif
2023 A* conf
IJCAI
Virginie Do, Sam Corbett-Davies, Jamal Atif, Nicolas Usunier
2022 J jnl
Mach. Learn.
Rafael Pinot, Laurent Meunier, Florian Yger, Cédric Gouy-Pailler, Yann Chevaleyre, Jamal Atif
2022 A* conf
AAAI
Virginie Do, Sam Corbett-Davies, Jamal Atif, Nicolas Usunier
2022 A* conf
NeurIPS
Laurent Meunier, Raphael Ettedgui, Rafael Pinot, Yann Chevaleyre, Jamal Atif
2022 J jnl
CoRR
Laurent Meunier, Raphaël Ettedgui, Rafael Pinot, Yann Chevaleyre, Jamal Atif
2022 J jnl
CoRR
Raphael Ettedgui, Alexandre Araujo, Rafael Pinot, Yann Chevaleyre, Jamal Atif
2021 A conf
AISTATS
Meyer Scetbon, Laurent Meunier, Jamal Atif, Marco Cuturi
2021 A* conf
ICML
Laurent Meunier, Meyer Scetbon, Rafael Pinot, Jamal Atif, Yann Chevaleyre
2021 J jnl
CoRR
Laurent Meunier, Meyer Scetbon, Rafael Pinot, Jamal Atif, Yann Chevaleyre
2021 J jnl
CoRR
Céline Beji, Florian Yger, Jamal Atif
2021 A* conf
AAAI
Alexandre Araujo, Benjamin Négrevergne, Yann Chevaleyre, Jamal Atif
2021 J jnl
CoRR
Rafael Pinot, Laurent Meunier, Florian Yger, Cédric Gouy-Pailler, Yann Chevaleyre, Jamal Atif
2021 A* conf
IJCAI
Virginie Do, Jamal Atif, Jérôme Lang, Nicolas Usunier
2021 J jnl
CoRR
Virginie Do, Jamal Atif, Jérôme Lang, Nicolas Usunier
2021 J jnl
CoRR
Virginie Do, Sam Corbett-Davies, Jamal Atif, Nicolas Usunier
2021 A* conf
NeurIPS
Virginie Do, Sam Corbett-Davies, Jamal Atif, Nicolas Usunier
2021 J jnl
CoRR
Virginie Do, Sam Corbett-Davies, Jamal Atif, Nicolas Usunier
2020 J jnl
CoRR
Eric Benhamou, David Saltiel, Sandrine Ungari, Abhishek Mukhopadhyay, Jamal Atif
2020 conf
ECML/PKDD (5)
Eric Benhamou, David Saltiel, Jean-Jacques Ohana, Jamal Atif, Rida Laraki
2020 B conf
ICPR
Eric Benhamou, David Saltiel, Jean-Jacques Ohana, Jamal Atif
2020 J jnl
CoRR
Eric Benhamou, David Saltiel, Jean-Jacques Ohana, Jamal Atif
2020 B conf
ESANN
Céline Beji, Eric Benhamou, Michaël Bon, Florian Yger, Jamal Atif
2020 J jnl
CoRR
Céline Beji, Michaël Bon, Florian Yger, Jamal Atif
2020 J jnl
CoRR
Alexandre Araujo, Benjamin Négrevergne, Yann Chevaleyre, Jamal Atif
2020 J jnl
CoRR
Meyer Scetbon, Laurent Meunier, Jamal Atif, Marco Cuturi
2020 A* conf
ICML
Rafael Pinot, Raphael Ettedgui, Geovani Rizk, Yann Chevaleyre, Jamal Atif
2020 J jnl
CoRR
Rafael Pinot, Raphael Ettedgui, Geovani Rizk, Yann Chevaleyre, Jamal Atif
2020 conf
MIDAS@PKDD/ECML
David Saltiel, Eric Benhamou, Rida Laraki, Jamal Atif
2020 A conf
ECAI
Alexandre Araujo, Benjamin Négrevergne, Yann Chevaleyre, Jamal Atif
2019 J jnl
CoRR
Rafael Pinot, Florian Yger, Cédric Gouy-Pailler, Jamal Atif
2019 J jnl
CoRR
Eric Benhamou, Jamal Atif, Rida Laraki, David Saltiel
2019 J jnl
CoRR
Alexandre Araujo, Benjamin Négrevergne, Yann Chevaleyre, Jamal Atif
2019 J jnl
CoRR
Alexandre Araujo, Rafael Pinot, Benjamin Négrevergne, Laurent Meunier, Yann Chevaleyre, Florian Yger, Jamal Atif
2019 A* conf
NeurIPS
Rafael Pinot, Laurent Meunier, Alexandre Araujo, Hisashi Kashima, Florian Yger, Cédric Gouy-Pailler, Jamal Atif
2019 J jnl
CoRR
Rafael Pinot, Laurent Meunier, Alexandre Araujo, Hisashi Kashima, Florian Yger, Cédric Gouy-Pailler, Jamal Atif
2019 J jnl
CoRR
Laurent Meunier, Jamal Atif, Olivier Teytaud
2018 J jnl
CoRR
Eric Benhamou, Jamal Atif, Rida Laraki, Anne Auger
2018 J jnl
CoRR
Eric Benhamou, Jamal Atif, Rida Laraki, Anne Auger
2018 J jnl
Artif. Intell.
Marc Aiguier, Jamal Atif, Isabelle Bloch, Céline Hudelot
2018 conf
AIC
Giovanni Sileno, Isabelle Bloch, Jamal Atif, Jean-Louis Dessalles
2018 J jnl
CoRR
Marc Aiguier, Jamal Atif, Isabelle Bloch, Ramón Pino Pérez
2018 J jnl
Int. J. Approx. Reason.
Marc Aiguier, Jamal Atif, Isabelle Bloch, Ramón Pino Pérez
2018 A conf
SDM
Anne Morvan, Krzysztof Choromanski, Cédric Gouy-Pailler, Jamal Atif
2018 A conf
UAI
Rafael Pinot, Anne Morvan, Florian Yger, Cédric Gouy-Pailler, Jamal Atif
2018 J jnl
CoRR
Rafael Pinot, Anne Morvan, Florian Yger, Cédric Gouy-Pailler, Jamal Atif
2018 Misc conf
ICCS
Carlos Agón, Moreno Andreatta, Jamal Atif, Isabelle Bloch, Pierre Mascarade
2018 J jnl
CoRR
Anne Morvan, Antoine Souloumiac, Krzysztof Choromanski, Cédric Gouy-Pailler, Jamal Atif
2018 Misc conf
ICASSP
Anne Morvan, Antoine Souloumiac, Cédric Gouy-Pailler, Jamal Atif
2018 conf
ECCV Workshops (4)
Alexandre Araujo, Benjamin Négrevergne, Yann Chevaleyre, Jamal Atif
2018 J jnl
CoRR
Alexandre Araujo, Benjamin Négrevergne, Yann Chevaleyre, Jamal Atif
2018 A* conf
NeurIPS
Ikko Yamane, Florian Yger, Jamal Atif, Masashi Sugiyama
2017 J jnl
CoRR
Anne Morvan, Krzysztof Choromanski, Cédric Gouy-Pailler, Jamal Atif
2017 J jnl
Signal Process.
Yoann Isaac, Quentin Barthélemy, Cédric Gouy-Pailler, Michèle Sebag, Jamal Atif
2017 A* conf
ICDM
Fabien Labernia, Bruno Zanuttini, Brice Mayag, Florian Yger, Jamal Atif
2017 J jnl
Rev. d'Intelligence Artif.
Yifan Yang, Jamal Atif, Isabelle Bloch
2017 conf
ISBI
Timothée Evain, Xavier Ripoche, Jamal Atif, Isabelle Bloch
2017 Misc conf
KI
Giovanni Sileno, Isabelle Bloch, Jamal Atif, Jean-Louis Dessalles
2017 J jnl
CoRR
Anne Morvan, Antoine Souloumiac, Cédric Gouy-Pailler, Jamal Atif
2017 A conf
AISTATS
Mariusz Bojarski, Anna Choromanska, Krzysztof Choromanski, Francois Fagan, Cédric Gouy-Pailler, Anne Morvan, Nourhan Sakr, Tamás Sarlós, Jamal Atif
2016 J jnl
Math. Morphol. Theory Appl.
Isabelle Bloch, Jamal Atif
2016 A conf
ECAI
Yifan Yang, Ricardo de Aldama, Jamal Atif, Isabelle Bloch
2016 J jnl
CoRR
Yoann Isaac, Quentin Barthélemy, Cédric Gouy-Pailler, Michèle Sebag, Jamal Atif
2016 J jnl
Int. J. Uncertain. Fuzziness Knowl. Based Syst.
Jamal Atif, Isabelle Bloch, Céline Hudelot
2016 J jnl
CoRR
Mariusz Bojarski, Anna Choromanska, Krzysztof Choromanski, Francois Fagan, Cédric Gouy-Pailler, Anne Morvan, Nourhan Sakr, Tamás Sarlós, Jamal Atif
2016 J jnl
CoRR
Krzysztof Choromanski, Francois Fagan, Cédric Gouy-Pailler, Anne Morvan, Tamás Sarlós, Jamal Atif
2015 Misc conf
KI
Yifan Yang, Jamal Atif, Isabelle Bloch
2015 J jnl
CoRR
Marc Aiguier, Jamal Atif, Isabelle Bloch, Céline Hudelot
2015 J jnl
Rev. d'Intelligence Artif.
Isabelle Bloch, Jamal Atif
2015 conf
ICIAP (1)
Timothée Evain, Xavier Ripoche, Jamal Atif, Isabelle Bloch
2015 C conf
ISMM
Isabelle Bloch, Jamal Atif
2015 J jnl
CoRR
Marc Aiguier, Jamal Atif, Isabelle Bloch, Céline Hudelot
2015 J jnl
CoRR
Marc Aiguier, Jamal Atif, Isabelle Bloch, Céline Hudelot
2014 conf
ECCV Workshops (2)
Céline Hudelot, Jamal Atif, Isabelle Bloch
2014 A conf
ECAI
Felix Distel, Jamal Atif, Isabelle Bloch
2014 A* conf
KR
Felix Distel, Jamal Atif, Isabelle Bloch
2014 J jnl
IEEE Trans. Syst. Man Cybern. Syst.
Jamal Atif, Céline Hudelot, Isabelle Bloch
2014 conf
EUSIPCO
Sylvain Chevallier, Quentin Barthélemy, Jamal Atif
2014 Misc conf
ICASSP
Sylvain Chevallier, Quentin Barthélemy, Jamal Atif
2013 J jnl
Inf. Sci.
Olivier Nempont, Jamal Atif, Isabelle Bloch
2013 conf
EUSFLAT Conf.
Jamal Atif, Isabelle Bloch, Felix Distel, Céline Hudelot
2013 conf
EUSFLAT Conf.
Isabelle Bloch, Jamal Atif
2013 C conf
ICFCA
Jamal Atif, Isabelle Bloch, Felix Distel, Céline Hudelot
2013 J jnl
CoRR
Sylvain Chevallier, Quentin Barthélemy, Jamal Atif
2013 J jnl
CoRR
Yoann Isaac, Quentin Barthélemy, Jamal Atif, Cédric Gouy-Pailler, Michèle Sebag
2013 Misc conf
ICASSP
Yoann Isaac, Quentin Barthélemy, Jamal Atif, Cédric Gouy-Pailler, Michèle Sebag
2012 J jnl
Comput. Vis. Image Underst.
Geoffroy Fouquier, Jamal Atif, Isabelle Bloch
2011 conf
CLA
Jamal Atif, Céline Hudelot, Isabelle Bloch
2010 A conf
ECAI
Céline Hudelot, Jamal Atif, Isabelle Bloch
2009 J jnl
Fuzzy Sets Syst.
Hassan Khotanlou, Olivier Colliot, Jamal Atif, Isabelle Bloch
2009 J jnl
J. Math. Imaging Vis.
Olivier Nempont, Jamal Atif, Elsa D. Angelini, Isabelle Bloch
2009 B conf
ICIP
Jamal Atif, Jérôme Darbon
2008 conf
DGCI
Olivier Nempont, Jamal Atif, Enrico Angelini, Isabelle Bloch
2008 ch.
Les Relations Spatiales
Céline Hudelot, Jamal Atif, Isabelle Bloch
2008 J jnl
Fuzzy Sets Syst.
Céline Hudelot, Jamal Atif, Isabelle Bloch
2008 J jnl
Comput. Biol. Medicine
John Puentes, Bénédicte Batrancourt, Jamal Atif, Elsa D. Angelini, Laurent Lecornu, Abdelhamid Zemirline, Isabelle Bloch, Gouenou Coatrieux, Christian Roux
2008 A conf
ECAI
Geoffroy Fouquier, Jamal Atif, Isabelle Bloch
2008 A conf
ECAI
Olivier Nempont, Jamal Atif, Elsa D. Angelini, Isabelle Bloch
2007 conf
ISBI
Hassan Khotanlou, Jamal Atif, Elsa D. Angelini, Hugues Duffau, Isabelle Bloch
2007 conf
IPMI
Olivier Nempont, Jamal Atif, Elsa D. Angelini, Isabelle Bloch
2007 conf
ISBI
Elsa D. Angelini, Jamal Atif, Julie Delon, Emmanuel Mandonnet, Hugues Duffau, Laurent Capelle
2007 A* conf
IJCAI
Jamal Atif, Céline Hudelot, Geoffroy Fouquier, Isabelle Bloch, Elsa D. Angelini
2007 conf
ISBI
Jamal Atif, Céline Hudelot, Olivier Nempont, Nathalie Richard, Bénédicte Batrancourt, Elsa D. Angelini, Isabelle Bloch
2007 conf
GbRPR
Emanuel Aldea, Jamal Atif, Isabelle Bloch
2007 conf
ISVC (2)
Emanuel Aldea, Geoffroy Fouquier, Jamal Atif, Isabelle Bloch
2007 conf
GbRPR
Geoffroy Fouquier, Jamal Atif, Isabelle Bloch
2005 conf
WILF
Hassan Khotanlou, Jamal Atif, Olivier Colliot, Isabelle Bloch
2005 C conf
AICCSA
Antoine Tarault, Jamal Atif, Xavier Ripoche, Patrick Bourdot, Angel Osorio
2004 conf
CARS
Jamal Atif, Angel Osorio, Bertrand Devaux, François-Xavier Roux
2004 conf
CARS
Angel Osorio, Olivier Traxer, Jamal Atif, Xavier Ripoche, Mohamed Tligui, Bernard Gattegno
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>
"#
    )
}