Rainer Koschke

172 papers A* 7A 12B 6C 11Journal 52Unranked 70
YearRankTypeTitle / Venue / Authors
2026 conf
BIOSTEC (2)
Dennis Küster, Rathi Adarshi Rammohan, Yvo Muskulus, Romina Razeghi Oskouei, Eva Krumhuber, Tanja Schultz, Rainer Koschke
2025 conf
ICSA Companion
Leon Ehrhardt, Rainer Koschke
2025 conf
ICSA Companion
Leon Ehrhardt, Rainer Koschke
2025 A conf
ICSME
Falko Galperin, Michel Krause, Rainer Koschke
2025 J jnl
Softwaretechnik-Trends
Leon Ehrhardt, Rainer Koschke
2025 J jnl
Softwaretechnik-Trends
Michel Krause, Rainer Koschke
2025 conf
BIOSTEC (1)
Dennis Küster, Rathi Adarshi Rammohan, Hui Liu, Tanja Schultz, Rainer Koschke
2024 J jnl
Softwaretechnik-Trends
Rainer Koschke
2024 conf
BIOSTEC (1)
Abhinav Veldanda, Hui Liu, Rainer Koschke, Tanja Schultz, Dennis Küster
2024 J jnl
Softwaretechnik-Trends
Rainer Koschke, Dennis Küster
2024 conf
IDE@ICSE
Martin Schröer, Rainer Koschke
2024 J jnl
CoRR
Martin Schröer, Rainer Koschke
2022 conf
IWSC
Rainer Koschke, Marcel Steinbeck
2022 B conf
VISSOFT
Marcel Steinbeck, Rainer Koschke
2022 C conf
Software Engineering
Rainer Koschke, Marcel Steinbeck
2022 J jnl
IEEE Trans. Software Eng.
Abdul Razzaq, Anthony Ventresque, Rainer Koschke, Andrea De Lucia, Jim Buckley
2022 B conf
VISSOFT
Falko Galperin, Rainer Koschke, Marcel Steinbeck
2021 ch.
Code Clone Analysis
Rainer Koschke, Stefan Bellon
2021 A conf
SANER
Marcel Steinbeck, Rainer Koschke
2021 conf
ECSA (Companion)
Rainer Koschke, Marcel Steinbeck
2021 A conf
SANER
Martin Schröer, Rainer Koschke
2021 conf
IWSC
Rainer Koschke, Marcel Steinbeck
2021 A conf
SANER
Marcel Steinbeck, Rainer Koschke
2021 ch.
Code Clone Analysis
Muhammad Hammad, Hamid Abdul Basit, Stan Jarzabek, Rainer Koschke
2020 J jnl
Comput. Sci. Rev.
Muhammad Hammad, Hamid Abdul Basit, Stan Jarzabek, Rainer Koschke
2020 B conf
VISSOFT
Rainer Koschke, Marcel Steinbeck
2020 J jnl
Softwaretechnik-Trends
Rainer Koschke, Marcel Steinbeck
2020 A conf
SANER
Marcel Steinbeck, Rainer Koschke, Marc O. Rüdel
2020 J jnl
Empir. Softw. Eng.
Wasim Said, Jochen Quante, Rainer Koschke
2020 C conf
SCAM
Bernhard J. Berger, Rodrigue Wete Nguempnang, Karsten Sohr, Rainer Koschke
2020 J jnl
CoRR
Chanchal K. Roy, Minhaz F. Zibran, Rainer Koschke
2019 A conf
ICPC
Marcel Steinbeck, Rainer Koschke, Marc O. Rüdel
2019 A conf
ICPC
Wasim Said, Jochen Quante, Rainer Koschke
2019 conf
IWESEP
Vara Arammongkolvichai, Rainer Koschke, Chaiyong Ragkhitwetsagul, Morakot Choetkiertikul, Thanwadee Sunetnanta
2019 C conf
SCAM
Marcel Steinbeck, Rainer Koschke, Marc O. Rüdel
2019 C conf
SCAM
Bernhard J. Berger, Karsten Sohr, Rainer Koschke
2019 A conf
SANER
Wasim Said, Jochen Quante, Rainer Koschke
2018 J jnl
Softwaretechnik-Trends
Rainer Koschke
2018 B conf
VISSOFT
Marc-Oliver Rüdel, Johannes Ganser, Rainer Koschke
2018 J jnl
CoRR
Verena Käfer, Stefan Wagner, Rainer Koschke
2018 conf
IWSC
Verena Käfer, Stefan Wagner, Rainer Koschke
2018 conf
IWSC
Rainer Koschke, Moritz Weinig
2018 conf
ECSA (Companion)
Rainer Koschke
2018 conf
MODELSWARD (Revised Selected Papers)
Wasim Said, Jochen Quante, Rainer Koschke
2018 A conf
ICSME
Wasim Said, Jochen Quante, Rainer Koschke
2018 A conf
ICSME
Wasim Said, Jochen Quante, Rainer Koschke
2018 C conf
MODELSWARD
Wasim Said, Jochen Quante, Rainer Koschke
2018 C conf
SCAM
Rainer Koschke, Urs-Bjorn Schmidt, Bernhard J. Berger
2016 conf
ESSoS
Bernhard J. Berger, Karsten Sohr, Rainer Koschke
2016 conf
IWSC
Rainer Koschke, Saman Bazrafshan
2016 J jnl
Softw. Qual. J.
Nils Göde, Yoshiki Higo, Rainer Koschke
2015 A ed.
ICSME
Rainer Koschke, Jens Krinke, Martin P. Robillard
2015 B conf
VISSOFT
Hamid Abdul Basit, Muhammad Hammad, Rainer Koschke
2015 C conf
SCAM
Dierk Lüdemann, Rainer Koschke
2015 conf
Software Engineering & Management
Walid Maalej, Rebecca Tiarks, Tobias Röhm, Rainer Koschke
2015 J jnl
J. Softw. Evol. Process.
Rainer Koschke, Juergen Rilling
2015 conf
IWSC
Hamid Abdul Basit, Muhammad Hammad, Stan Jarzabek, Rainer Koschke
2014 C conf
SCAM
Saman Bazrafshan, Rainer Koschke
2014 C conf
Software Engineering
Tobias Roehm, Rebecca Tiarks, Rainer Koschke, Walid Maalej
2014 conf
CSMR-WCRE
Rainer Koschke, Nils Göde, Yoshiki Higo
2014 J jnl
J. Softw. Evol. Process.
Rainer Koschke
2014 J jnl
ACM Trans. Softw. Eng. Methodol.
Walid Maalej, Rebecca Tiarks, Tobias Roehm, Rainer Koschke
2014 J jnl
Electron. Commun. Eur. Assoc. Softw. Sci. Technol.
Rainer Koschke, Nils Göde, Yoshiki Higo
2014 J jnl
Electron. Commun. Eur. Assoc. Softw. Sci. Technol.
Rainer Koschke, Ole Jan Lars Riemann
2014 J jnl
Sci. Comput. Program.
Katsuro Inoue, Rainer Koschke, Jens Krinke
2014 conf
CSMR-WCRE
Chanchal K. Roy, Minhaz F. Zibran, Rainer Koschke
2013 A* conf
ICSE
Rainer Koschke, Elmar Jürgens, Juergen Rilling
2013 conf
ICSM
Saman Bazrafshan, Rainer Koschke
2013 conf
CSMR
Bernhard J. Berger, Karsten Sohr, Rainer Koschke
2013 J jnl
J. Softw. Evol. Process.
Rainer Koschke
2013 ed.
IWSC
Rainer Koschke, Elmar Jürgens, Juergen Rilling
2013 J jnl
J. Softw. Evol. Process.
Nils Göde, Rainer Koschke
2013 C conf
Software Engineering (Workshops)
Klaus Schmid, Rainer Koschke, Christian Kröher, Dierk Lüdemann
2012 conf
IWSC
James R. Cordy, Katsuro Inoue, Rainer Koschke
2012 A* conf
ICSE
Tobias Roehm, Rebecca Tiarks, Rainer Koschke, Walid Maalej
2012 conf
CSMR
Rainer Koschke
2012 ed.
IWSC
James R. Cordy, Katsuro Inoue, Rainer Koschke, Jens Krinke, Chanchal K. Roy
2012 A conf
ICPC
Bernhard Katzmarski, Rainer Koschke
2012 conf
WCRE
Armijn Hemel, Rainer Koschke
2012 J jnl
Dagstuhl Reports
Ira D. Baxter, Michael Conradt, James R. Cordy, Rainer Koschke
2011 J jnl
Softw. Qual. J.
Rebecca Tiarks, Rainer Koschke, Raimar Falke
2011 conf
WCRE
Saman Bazrafshan, Rainer Koschke, Nils Göde
2011 A* conf
ICSE
James R. Cordy, Katsuro Inoue, Stanislaw Jarzabek, Rainer Koschke
2011 A* conf
ICSE
Nils Göde, Rainer Koschke
2011 J jnl
Softw. Qual. J.
Rainer Koschke, Andrian Marcus, Gerald C. Gannod
2011 conf
WCRE
Bernhard Scholz, Rainer Koschke
2011 conf
CSMR
Thilo Mende, Rainer Koschke, Jan Peleska
2011 ed.
IWSC
James R. Cordy, Katsuro Inoue, Stanislaw Jarzabek, Rainer Koschke
2010 conf
CSMR
Thilo Mende, Rainer Koschke
2010 J jnl
Softwaretechnik-Trends
Thilo Mende, Rainer Koschke
2010 conf
ICSE (2)
Katsuro Inoue, Stanislaw Jarzabek, James R. Cordy, Rainer Koschke
2010 conf
CSMR
Rainer Koschke
2010 ed.
IWSC
Katsuro Inoue, Stanislaw Jarzabek, Rainer Koschke, James R. Cordy
2009 J jnl
IEEE Trans. Software Eng.
Bas Cornelissen, Andy Zaidman, Arie van Deursen, Leon Moonen, Rainer Koschke
2009 C conf
SCAM
Rebecca Tiarks, Rainer Koschke, Raimar Falke
2009 J jnl
J. Softw. Maintenance Res. Pract.
Thilo Mende, Rainer Koschke, Felix Beckwermert
2009 J jnl
Sci. Comput. Program.
Chanchal Kumar Roy, James R. Cordy, Rainer Koschke
2009 conf
CSMR
Thilo Mende, Rainer Koschke, Marek Leszak
2009 J jnl
Softw. Qual. J.
Rainer Koschke, Pierre Frenzel, Andreas P. J. Breu, Karsten Angstmann
2009 conf
CSMR
Nils Göde, Rainer Koschke
2009 conf
PROMISE
Thilo Mende, Rainer Koschke
2009 conf
CSMR
Rainer Koschke, Stanislaw Jarzabek, James R. Cordy, Katsuro Inoue
2008 conf
ISSSE
Rainer Koschke
2008 J jnl
J. Syst. Softw.
Jochen Quante, Rainer Koschke
2008 J jnl
Empir. Softw. Eng.
Raimar Falke, Pierre Frenzel, Rainer Koschke
2008 conf
Workshop Software Reengineering
Jan Harder, Rainer Koschke
2008 J jnl
J. Softw. Maintenance Res. Pract.
Jim Buckley, Andrew P. LeGear, Chris Exton, Ross Cadogan, Trevor Johnston, Bill Looby, Rainer Koschke
2008 ch.
Software Evolution
Rainer Koschke
2008 ed.
SOFTVIS
Rainer Koschke, Christopher D. Hundhausen, Alexandru C. Telea
2008 conf
GI Jahrestagung (2)
Bernhard J. Berger, Rainer Koschke
2008 conf
CSMR
Thilo Mende, Felix Beckwermert, Rainer Koschke, Gerald Meier
2008 conf
Workshop Software Reengineering
Rainer Koschke
2007 ed.
GI Jahrestagung (1)
Rainer Koschke, Otthein Herzog, Karl-Heinz Rödiger, Marc Ronthaler
2007 ed.
GI Jahrestagung (2)
Rainer Koschke, Otthein Herzog, Karl-Heinz Rödiger, Marc Ronthaler
2007 J jnl
Inf. Softw. Technol.
Andreas Christl, Rainer Koschke, Margaret-Anne D. Storey
2007 J jnl
IEEE Trans. Software Eng.
Stefan Bellon, Rainer Koschke, Giuliano Antoniol, Jens Krinke, Ettore Merlo
2007 ed.
Duplication, Redundancy, and Similarity in Software
Rainer Koschke, Ettore Merlo, Andrew Walenstein
2007 conf
WCRE
Jochen Quante, Rainer Koschke
2007 conf
WCRE
Pierre Frenzel, Rainer Koschke, Andreas P. J. Breu, Karsten Angstmann
2007 J jnl
IEEE Trans. Software Eng.
David W. Binkley, Rainer Koschke, Spiros Mancoridis
2006 conf
Duplication, Redundancy, and Similarity in Software
Rainer Koschke, Andrew Walenstein, Ettore Merlo
2006 conf
Duplication, Redundancy, and Similarity in Software
Andrew Walenstein, Rainer Koschke, Ettore Merlo
2006 conf
WCRE
Rainer Koschke, Raimar Falke, Pierre Frenzel
2006 conf
CSMR
Jochen Quante, Rainer Koschke
2006 J jnl
Softwaretechnik-Trends
Jochen Quante, Rainer Koschke
2006 J jnl
Autom. Softw. Eng.
Rick Kazman, Arie van Deursen, Rainer Koschke
2006 J jnl
Softwaretechnik-Trends
Rainer Koschke
2006 J jnl
Softwaretechnik-Trends
Matthias Hamburg, Timea Illes, Stefan Jungmayr, Rainer Koschke
2006 J jnl
Sci. Comput. Program.
Rainer Koschke, Gerardo Canfora, Jörg Czeranski
2006 J jnl
J. Syst. Softw.
Tom Dean, Mark Harman, Rainer Koschke, Michael L. Van de Vanter
2006 conf
Duplication, Redundancy, and Similarity in Software
Cory Kapser, Paul Anderson, Michael W. Godfrey, Rainer Koschke, Matthias Rieger, Filip Van Rysselberghe, Peter Weißgerber
2006 conf
Duplication, Redundancy, and Similarity in Software
Rainer Koschke
2005 conf
WCRE
Andreas Christl, Rainer Koschke, Margaret-Anne D. Storey
2005 A* conf
ASE
Rainer Koschke, Jochen Quante
2005 J jnl
Inform. Forsch. Entwickl.
Rainer Koschke
2005 J jnl
J. Syst. Softw.
Thomas Eisenbarth, Rainer Koschke, Gunther Vogel
2005 B conf
VISSOFT
Raimar Falke, Raimund Klein, Rainer Koschke, Jochen Quante
2005 conf
WICSA
Rainer Koschke
2004 J jnl
IEEE Trans. Software Eng.
Dragan Bojic, Thomas Eisenbarth, Rainer Koschke, Daniel Simon, Dusan M. Velasevic
2004 conf
WICSA
Arie van Deursen, Christine Hofmeister, Rainer Koschke, Leon Moonen, Claudio Riva
2004 J jnl
ACM SIGSOFT Softw. Eng. Notes
Andrew Walenstein, Arun Lakhotia, Rainer Koschke
2003 conf
IWPC
Arie van Deursen, Jean-Marie Favre, Rainer Koschke, Juergen Rilling
2003 J jnl
Softwaretechnik-Trends
Thomas Eisenbarth, Rainer Koschke, Gunther Vogel
2003 conf
WCRE
Rainer Koschke, Daniel Simon
2003 J jnl
IEEE Trans. Software Eng.
Thomas Eisenbarth, Rainer Koschke, Daniel Simon
2003 conf
WCRE
Arun Lakhotia, Andrew Walenstein, Rainer Koschke
2003 J jnl
J. Softw. Maintenance Res. Pract.
Rainer Koschke
2002 conf
ICSM
Rainer Koschke
2002 conf
ICSM
Thomas Eisenbarth, Rainer Koschke, Daniel Simon
2002 conf
WCRE
Thomas Eisenbarth, Rainer Koschke, Gunther Vogel
2001 conf
ICSM
Thomas Eisenbarth, Rainer Koschke, Daniel Simon
2001 conf
CSMR
Thomas Eisenbarth, Rainer Koschke, Daniel Simon
2001 conf
IWPC
Thomas Eisenbarth, Rainer Koschke, Daniel Simon
2001 ed.
WCRE
Elizabeth Burd, Peter Aiken, Rainer Koschke
2001 conf
Software Visualization
Rainer Koschke
2001 conf
WCRE
Rudolf Ferenc, Susan Elliott Sim, Richard C. Holt, Rainer Koschke, Tibor Gyimóthy
2001 J jnl
ACM SIGSOFT Softw. Eng. Notes
Susan Elliott Sim, Rainer Koschke
2000 conf
IWPC
Rainer Koschke, Thomas Eisenbarth
2000 J jnl
Sci. Comput. Program.
Jean-Francois Girard, Rainer Koschke
2000 conf
WCRE
Jörg Czeranski, Thomas Eisenbarth, Holger M. Kienle, Rainer Koschke, Daniel Simon
2000 J jnl
Softwaretechnik-Trends
Rainer Koschke
2000
Rainer Koschke
2000 conf
WCRE
Jörg Czeranski, Thomas Eisenbarth, Holger M. Kienle, Rainer Koschke, Erhard Plödereder, Daniel Simon, Yan Zhang V, Jean-Francois Girard, Martin Würthner
2000 conf
WCRE
Gerardo Canfora, Jörg Czeranski, Rainer Koschke
2000 A* conf
ICSE
Susan Elliott Sim, Richard C. Holt, Rainer Koschke
1999 J jnl
Autom. Softw. Eng.
Jean-Francois Girard, Rainer Koschke, Georg Schied
1999 conf
WCRE
Rainer Koschke
1998 conf
WCRE
Rainer Koschke, Jean-Francois Girard
1997 A* conf
ASE
Jean-Francois Girard, Rainer Koschke, Georg Schied
1997 conf
WCRE
Jean-Francois Girard, Rainer Koschke, Georg Schied
1997 conf
ICSM
Jean-Francois Girard, Rainer Koschke
1996 ch.
Softwarewartung und Reengineering
Rainer Koschke, Erhard Plödereder
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>
"#
    )
}