Ola Spjuth

85 papers B 1C 1Journal 68Unranked 14
YearRankTypeTitle / Venue / Authors
2025 J jnl
CoRR
Srijit Seal, Dinh Long Huynh, Moudather Chelbi, Sara Khosravi, Ankur Kumar, Mattson Thieme, Isaac Wilks, Mark Davies, Jessica Mustali, Yannick Sun, Nick Edwards, Daniil Boiko, Andrei Tyrin, Douglas W. Selinger, Ayaan Parikh, Rahul Vijayan, Shoman Kasbekar, Dylan Reid, Andreas Bender, Ola Spjuth
2025 J jnl
npj Digit. Medicine
Akshai P. Sreenivasan, Aina Vaivade, Yassine Noui, Payam Emami Khoonsari, Joachim Burman, Ola Spjuth, Kim Kultima
2025 conf
CPAIOR (1)
Ramiz Gindullin, María Andreína Francisco Rodríguez, Brinton Seashore-Ludlow, Ola Spjuth
2025 J jnl
J. Cheminformatics
Srijit Seal, Maria-Anna Trapotsi, Manas Mahale, Vigneshwari Subramanian, Nigel Greene, Ola Spjuth, Andreas Bender
2024 J jnl
J. Cheminformatics
Staffan Arvidsson McShane, Ulf Norinder, Jonathan Alvarsson, Ernst Ahlberg, Lars Carlsson, Ola Spjuth
2024 J jnl
Expert Syst. Appl.
Tianru Zhang, Ankit Gupta, María Andreína Francisco Rodríguez, Ola Spjuth, Andreas Hellander, Salman Toor
2024 J jnl
J. Chem. Inf. Model.
Srijit Seal, Ola Spjuth, Layla Hosseini-Gerami, Miguel García-Ortegón, Shantanu Singh, Andreas Bender, Anne E. Carpenter
2023 J jnl
PLoS Comput. Biol.
Philip J. Harrison, Ankit Gupta, Jonne Rietdijk, Håkan Wieslander, Jordi Carreras Puigvert, Polina Georgiev, Carolina Wählby, Ola Spjuth, Ida-Maria Sintorn
2023 J jnl
J. Cheminformatics
Srijit Seal, Hongbin Yang, Maria-Anna Trapotsi, Satvik Singh, Jordi Carreras Puigvert, Ola Spjuth, Andreas Bender
2022 J jnl
J. Cheminformatics
Akshai P. Sreenivasan, Philip J. Harrison, Wesley Schaal, Damian J. Matuszewski, Kim Kultima, Ola Spjuth
2022 B conf
CCGRID
Morgan Ekmefjord, Addi Ait-Mlouk, Sadi Alawadi, Mattias Åkesson, Prashant Singh, Ola Spjuth, Salman Toor, Andreas Hellander
2022 J jnl
J. Cheminformatics
Nina Lukashina, Elena Kartysheva, Ola Spjuth, Elizaveta Virko, Aleksei Shpilman
2021 J jnl
F1000Research
Ola Spjuth, Marco Capuccini, Matteo Carone, Anders Larsson, Wesley Schaal, Jon Ander Novella, Oliver Stein, Morgan Ekmefjord, Paolo Di Tommaso, Evan W. Floden, Cédric Notredame, Pablo A. Moreno, Andreas Hellander, Payam Emami Khoonsari, Stephanie Herman, Kim Kultima, Samuel Lampa
2021 J jnl
J. Cheminformatics
Andrea Morger, Fredrik Svensson, Staffan Arvidsson McShane, Niharika Gauraha, Ulf Norinder, Ola Spjuth, Andrea Volkamer
2021 J jnl
IEEE J. Biomed. Health Informatics
Håkan Wieslander, Philip J. Harrison, Gabriel Skogberg, Sonya Jackson, Hakan Fridén, Johan Karlsson, Ola Spjuth, Carolina Wählby
2021 J jnl
J. Chem. Inf. Model.
Staffan Arvidsson McShane, Ernst Ahlberg, Tobias Noeske, Ola Spjuth
2021 J jnl
CoRR
Morgan Ekmefjord, Addi Ait-Mlouk, Sadi Alawadi, Mattias Åkesson, Desislava Stoyanova, Ola Spjuth, Salman Toor, Andreas Hellander
2021 C conf
ICPRAM
Niharika Gauraha, Ola Spjuth
2021 J jnl
J. Cheminformatics
Ulf Norinder, Ola Spjuth, Fredrik Svensson
2021 conf
COPA
Niharika Gauraha, Ola Spjuth
2021 J jnl
Bioinform.
Jon E. T. Jakobsson, Ola Spjuth, Malin C. Lagerström
2020 J jnl
J. Chem. Inf. Model.
Ulf Norinder, Ola Spjuth, Fredrik Svensson
2020 J jnl
J. Cheminformatics
Laeeq Ahmed, Hiba Alogheli, Staffan Arvidsson McShane, Jonathan Alvarsson, Arvid Berg, Anders Larsson, Wesley Schaal, Erwin Laure, Ola Spjuth
2020 conf
IEEE BigData
Oliver Stein, Ben Blamey, Johan Karlsson, Alan Sabirsh, Ola Spjuth, Andreas Hellander, Salman Toor
2020 J jnl
CoRR
Oliver Stein, Ben Blamey, Johan Karlsson, Alan Sabirsh, Ola Spjuth, Andreas Hellander, Salman Toor
2020 J jnl
J. Cheminformatics
Nalini Schaduangrat, Samuel Lampa, Saw Simeon, Matthew Paul Gleeson, Ola Spjuth, Chanin Nantasenamat
2020 J jnl
J. Chem. Inf. Model.
Ulf Norinder, Ola Spjuth, Fredrik Svensson
2019 conf
COPA
Ola Spjuth, Robin Carrión Brännström, Lars Carlsson, Niharika Gauraha
2019 J jnl
CoRR
Ola Spjuth, Robin Carrión Brännström, Lars Carlsson, Niharika Gauraha
2019 J jnl
Bioinform.
Jon Ander Novella, Payam Emami Khoonsari, Stephanie Herman, Daniel Whitenack, Marco Capuccini, Joachim Burman, Kim Kultima, Ola Spjuth
2019 J jnl
Bioinform.
Payam Emami Khoonsari, Pablo A. Moreno, Sven Bergmann, Joachim Burman, Marco Capuccini, Matteo Carone, Marta Cascante, Pedro de Atauri, Carles Foguet, Alejandra N. González-Beltrán, Thomas Hankemeier, Kenneth Haug, Sijin He, Stephanie Herman, David Johnson, Namrata Kale, Anders Larsson, Steffen Neumann, Kristian Peters, Luca Pireddu, Philippe Rocca-Serra, Pierrick Roger, Rico Rueedi, Christoph Ruttkies, Noureddin Sadawi, Reza M. Salek, Susanna-Assunta Sansone, Daniel Schober, Vitaly A. Selivanov, Etienne A. Thévenot, Michael van Vliet, Gianluigi Zanetti, Christoph Steinbeck, Kim Kultima, Ola Spjuth
2019 J jnl
PeerJ Comput. Sci.
Marco Capuccini, Anders Larsson, Matteo Carone, Jon Ander Novella, Noureddin Sadawi, Jianliang Gao, Salman Toor, Ola Spjuth
2019 J jnl
Comput. Sci. Eng.
Samuel Lampa, Martin Dahlö, Jonathan Alvarsson, Ola Spjuth
2019 conf
COPA
Niharika Gauraha, Fabian Söderdahl, Ola Spjuth
2018 J jnl
J. Cheminformatics
Maris Lapins, Staffan Arvidsson, Samuel Lampa, Arvid Berg, Wesley Schaal, Jonathan Alvarsson, Ola Spjuth
2018 J jnl
CoRR
Ola Spjuth, Lars Carlsson, Niharika Gauraha
2018 J jnl
CoRR
Niharika Gauraha, Lars Carlsson, Ola Spjuth
2018 J jnl
J. Chem. Inf. Model.
Fredrik Svensson, Natália Aniceto, Ulf Norinder, Isidro Cortes-Ciriano, Ola Spjuth, Lars Carlsson, Andreas Bender
2018 conf
COPA
Niharika Gauraha, Lars Carlsson, Ola Spjuth
2018 J jnl
J. Cheminformatics
Laeeq Ahmed, Valentin Georgiev, Marco Capuccini, Salman Zubair Toor, Wesley Schaal, Erwin Laure, Ola Spjuth
2018 J jnl
J. Cheminformatics
Alexander Kensert, Jonathan Alvarsson, Ulf Norinder, Ola Spjuth
2018 J jnl
CoRR
Marco Capuccini, Anders Larsson, Matteo Carone, Jon Ander Novella, Noureddin Sadawi, Jianliang Gao, Salman Zubair Toor, Ola Spjuth
2018 J jnl
CoRR
Marco Capuccini, Salman Zubair Toor, Staffan Arvidsson, Ola Spjuth
2018 J jnl
J. Cheminformatics
Ola Spjuth
2018 J jnl
CoRR
Niharika Gauraha, Ola Spjuth
2017 J jnl
J. Am. Medical Informatics Assoc.
Ola Spjuth, Andreas Karlsson, Mark Clements, Keith Humphreys, Emma Ivansson, Jim Dowling, Martin Eklund, Alexandra Jauhiainen, Kamila Czene, Henrik Gronberg, Pär Sparén, Fredrik Wiklund, Abbas Cheddad, þorgerður Pálsdóttir, Mattias Rantalainen, Linda Abrahamsson, Erwin Laure, Jan-Eric Litton, Juni Palmgren
2017 J jnl
J. Cheminformatics
Egon L. Willighagen, John W. Mayfield, Jonathan Alvarsson, Arvid Berg, Lars Carlsson, Nina Jeliazkova, Stefan Kuhn, Tomás Pluskal, Miquel Rojas-Chertó, Ola Spjuth, Gilleain M. Torrance, Chris T. A. Evelo, Rajarshi Guha, Christoph Steinbeck
2017 conf
ICCSW
Marco Capuccini, Anders Larsson, Salman Zubair Toor, Ola Spjuth
2017 J jnl
J. Cheminformatics
Marco Capuccini, Laeeq Ahmed, Wesley Schaal, Erwin Laure, Ola Spjuth
2017 conf
COPA
Staffan Arvidsson, Ola Spjuth, Lars Carlsson, Paolo Toccaceli
2017 J jnl
J. Biomed. Semant.
Samuel Lampa, Egon L. Willighagen, Pekka Kohonen, Ali King, Denny Vrandecic, Roland Grafstrom, Ola Spjuth
2017 conf
eScience
Salman Zubair Toor, Mathias Lindberg, Ingemar Falman, Andreas Vallin, Olof Mohill, Pontus Freyhult, Linus Nilsson, Martin Agback, Lars Viklund, Henric Zazzik, Ola Spjuth, Marco Capuccini, Joakim Moller, Donal Murtagh, Andreas Hellander
2017 J jnl
J. Cheminformatics
Egon L. Willighagen, John W. Mayfield, Jonathan Alvarsson, Arvid Berg, Lars Carlsson, Nina Jeliazkova, Stefan Kuhn, Tomás Pluskal, Miquel Rojas-Chertó, Ola Spjuth, Gilleain M. Torrance, Chris T. A. Evelo, Rajarshi Guha, Christoph Steinbeck
2017 J jnl
F1000Research
Merlijn N. van Rijswijk, Charlie Beirnaert, Christophe Caron, Marta Cascante, Victoria Dominguez Del Angel, Warwick B. Dunn, Timothy M. D. Ebbels, Franck Giacomoni, Alejandra N. González-Beltrán, Thomas Hankemeier, Kenneth Haug, Jose L. Izquierdo-García, Rafael C. Jiménez, Fabien Jourdan, Namrata Kale, Maria I. Klapa, Oliver Kohlbacher, Kairi Koort, Kim Kultima, Gildas Le Corguillé, Nicholas K. Moschonas, Steffen Neumann, Claire O'Donovan, Martin Reczko, Philippe Rocca-Serra, Antonio Rosato, Reza M. Salek, Susanna-Assunta Sansone, Venkata P. Satagopam, Daniel Schober, Ruth Shimmo, Rachel A. Spicer, Ola Spjuth, Etienne A. Thévenot, Mark R. Viant, Ralf J. M. Weber, Egon L. Willighagen, Gianluigi Zanetti, Christoph Steinbeck
2016 J jnl
J. Cheminformatics
Jonathan Alvarsson, Samuel Lampa, Wesley Schaal, Claes R. Andersson, Jarl E. S. Wikberg, Ola Spjuth
2016 J jnl
J. Cheminformatics
Samuel Lampa, Jonathan Alvarsson, Ola Spjuth
2016 J jnl
J. Cheminformatics
Ola Spjuth, Patrik Rydberg, Egon L. Willighagen, Chris T. A. Evelo, Nina Jeliazkova
2015 conf
BDC
Marco Capuccini, Lars Carlsson, Ulf Norinder, Ola Spjuth
2015 conf
SLDS
Ernst Ahlberg, Ola Spjuth, Catrin Hasselgren, Lars Carlsson
2015 J jnl
J. Chem. Inf. Model.
Behrooz Torabi Moghadam, Jonathan Alvarsson, Marcus Holm, Martin Eklund, Lars Carlsson, Ola Spjuth
2014 J jnl
J. Chem. Inf. Model.
Jonathan Alvarsson, Martin Eklund, Claes R. Andersson, Lars Carlsson, Ola Spjuth, Jarl E. S. Wikberg
2014 conf
DILS
Ola Spjuth, Jani Heikkinen, Jan-Eric Litton, Juni Palmgren, Maria Krestyaninova
2014 conf
eScience
Alexey Siretskiy, Ola Spjuth
2014 J jnl
J. Chem. Inf. Model.
Jonathan Alvarsson, Martin Eklund, Ola Engkvist, Ola Spjuth, Lars Carlsson, Jarl E. S. Wikberg, Tobias Noeske
2013 J jnl
J. Cheminformatics
Ola Spjuth, Arvid Berg, Samuel E. Adams, Egon L. Willighagen
2013 J jnl
Bioinform.
Wesley Schaal, Ulf Hammerling, Mats G. Gustafsson, Ola Spjuth
2013 J jnl
Bioinform.
Ola Spjuth, Valentin Georgiev, Lars Carlsson, Jonathan Alvarsson, Arvid Berg, Egon L. Willighagen, Jarl E. S. Wikberg, Martin Eklund
2013 J jnl
J. Cheminformatics
Egon L. Willighagen, Andra Waagmeester, Ola Spjuth, Peter Ansell, Antony J. Williams, Valery Tkachenko, Janna Hastings, Bin Chen, David J. Wild
2013 conf
CloudCom (2)
Laeeq Ahmed, Åke Edlund, Erwin Laure, Ola Spjuth
2013 J jnl
Bioinform.
Michal Rostkowski, Ola Spjuth, Patrik Rydberg
2011 J jnl
BMC Bioinform.
Jonathan Alvarsson, Claes R. Andersson, Ola Spjuth, Rolf Larsson, Jarl E. S. Wikberg
2011 J jnl
J. Chem. Inf. Model.
Ola Spjuth, Martin Eklund, Ernst Ahlberg Helgee, Scott Boyer, Lars Carlsson
2011 J jnl
J. Biomed. Semant.
Egon L. Willighagen, Jonathan Alvarsson, Annsofie Andersson, Martin Eklund, Samuel Lampa, Maris Lapins, Ola Spjuth, Jarl E. S. Wikberg
2011 J jnl
J. Cheminformatics
Noel M. O'Boyle, Rajarshi Guha, Egon L. Willighagen, Sam E. Adams, Jonathan Alvarsson, Jean-Claude Bradley, Igor V. Filippov, Robert M. Hanson, Marcus D. Hanwell, Geoffrey R. Hutchison, Craig A. James, Nina Jeliazkova, Andrew S. I. D. Lang, Karol M. Langner, David C. Lonie, Daniel M. Lowe, Jérôme Pansanel, Dmitry Pavlov, Ola Spjuth, Christoph Steinbeck, Adam L. Tenderholt, Kevin J. Theisen, Peter Murray-Rust
2011 J jnl
Bioinform.
Ola Spjuth, Martin Eklund, Maris Lapins, Muhammad Junaid, Jarl E. S. Wikberg
2010 J jnl
BMC Bioinform.
Martin Eklund, Ola Spjuth, Jarl E. S. Wikberg
2010 J jnl
J. Cheminformatics
Ola Spjuth, Egon L. Willighagen, Rajarshi Guha, Martin Eklund, Jarl E. S. Wikberg
2010 J jnl
BMC Bioinform.
Lars Carlsson, Ola Spjuth, Samuel E. Adams, Robert C. Glen, Scott Boyer
2009 J jnl
BMC Bioinform.
Ola Spjuth, Jonathan Alvarsson, Arvid Berg, Martin Eklund, Stefan Kuhn, Carl Mäsak, Gilleain M. Torrance, Johannes Wagener, Egon L. Willighagen, Christoph Steinbeck, Jarl E. S. Wikberg
2009
Ola Spjuth
2009 J jnl
BMC Bioinform.
Johannes Wagener, Ola Spjuth, Egon L. Willighagen, Jarl E. S. Wikberg
2008 J jnl
BMC Bioinform.
Maris Lapins, Martin Eklund, Ola Spjuth, Peteris Prusis, Jarl E. S. Wikberg
2008 J jnl
BMC Bioinform.
Martin Eklund, Ola Spjuth, Jarl E. S. Wikberg
2007 J jnl
BMC Bioinform.
Ola Spjuth, Tobias Helmus, Egon L. Willighagen, Stefan Kuhn, Martin Eklund, Johannes Wagener, Peter Murray-Rust, Christoph Steinbeck, Jarl E. S. Wikberg
2006 J jnl
Bioinform.
Adam Ameur, Vladimir Yankovski, Stefan Enroth, Ola Spjuth, Henryk Jan Komorowski
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>
"#
    )
}