Walter Murray Wonham

99 papers A* 1A 1B 2C 6Journal 71Unranked 17
YearRankTypeTitle / Venue / Authors
2019 J jnl
IEEE Trans. Autom. Control.
Liyong Lin, Tomás Masopust, Walter Murray Wonham, Rong Su
2019 J jnl
Unmanned Syst.
Ke Dong, Quan Quan, Walter Murray Wonham
2018 conf
CDC
Rong Su, Walter Murray Wonham
2018 J jnl
CoRR
Liyong Lin, Tomás Masopust, Walter Murray Wonham, Rong Su
2018 J jnl
IEEE Access
Deguang Wang, Liyong Lin, ZhiWu Li, Walter Murray Wonham
2018 conf
CDC
Sadegh Rahnamoon, Walter Murray Wonham
2018 J jnl
Autom.
Liyong Lin, Alin Stefanescu, Weilin Wang, Rong Su, Walter Murray Wonham
2018 J jnl
Autom.
Rong Su, Walter Murray Wonham
2017 conf
CCE
Matin Macktoobian, Walter Murray Wonham
2017 J jnl
IEEE Trans. Autom. Control.
Kai Cai, Renyuan Zhang, Walter Murray Wonham
2017 J jnl
CoRR
Quan Quan, Zhiyao Zhao, Liyong Lin, Peng Wang, Walter Murray Wonham, Kai-Yuan Cai
2017 J jnl
IEEE Trans. Syst. Man Cybern. Syst.
Xi Wang, ZhiWu Li, Walter Murray Wonham
2017 J jnl
IEEE Trans. Autom. Control.
Liyong Lin, Simon Ware, Rong Su, Walter Murray Wonham
2017 C conf
ACC
Liyong Lin, Simon Ware, Rong Su, Walter Murray Wonham
2017 J jnl
Autom.
Renyuan Zhang, Kai Cai, Walter Murray Wonham
2016 conf
WODES
Kai Cai, Walter Murray Wonham
2016 J jnl
CoRR
Kai Cai, Renyuan Zhang, Walter Murray Wonham
2016 J jnl
Discret. Event Dyn. Syst.
Renyuan Zhang, Kai Cai, Yongmei Gan, Walter Murray Wonham
2016 J jnl
CoRR
Kai Cai, Renyuan Zhang, Walter Murray Wonham
2015 J jnl
Discret. Event Dyn. Syst.
Kai Cai, Walter Murray Wonham
2015 C conf
ACC
Kai Cai, Renyuan Zhang, Walter Murray Wonham
2015 J jnl
IEEE Trans. Autom. Control.
Kai Cai, Renyuan Zhang, Walter Murray Wonham
2015 ch.
Encyclopedia of Systems and Control
Walter Murray Wonham
2014 conf
CDC
Renyuan Zhang, Kai Cai, Walter Murray Wonham
2014 conf
WODES
Kai Cai, Renyuan Zhang, Walter Murray Wonham
2014 J jnl
IEEE Trans. Autom. Control.
Kai Cai, Walter Murray Wonham
2013 J jnl
CoRR
Renyuan Zhang, Kai Cai, Yongmei Gan, Walter Murray Wonham
2013 J jnl
Int. J. Control
Wujie Chao, Yongmei Gan, Zhaoan Wang, Walter Murray Wonham
2013 conf
CDC
Kai Cai, Renyuan Zhang, Walter Murray Wonham
2013 J jnl
CoRR
Kai Cai, Renyuan Zhang, Walter Murray Wonham
2013 J jnl
CoRR
Renyuan Zhang, Kai Cai, Yongmei Gan, Zhaoan Wang, Walter Murray Wonham
2013 C conf
ACC
Kai Cai, Renyuan Zhang, Walter Murray Wonham
2013 J jnl
Autom.
Renyuan Zhang, Kai Cai, Yongmei Gan, Zhaoan Wang, Walter Murray Wonham
2013 J jnl
CoRR
Kai Cai, Walter Murray Wonham
2012 J jnl
CoRR
Renyuan Zhang, Kai Cai, Yongmei Gan, Zhaoan Wang, Walter Murray Wonham
2012 conf
WODES
Kai Cai, Walter Murray Wonham
2012 conf
CDC
Kai Cai, Walter Murray Wonham
2010 J jnl
Discret. Event Dyn. Syst.
Lei Feng, Walter Murray Wonham
2010 J jnl
IEEE Trans. Autom. Control.
Kai Cai, Walter Murray Wonham
2009 conf
CDC
Kai Cai, Walter Murray Wonham
2008 J jnl
IEEE Trans. Autom. Control.
Lei Feng, Walter Murray Wonham
2007 J jnl
Formal Methods Syst. Des.
Lei Feng, Walter Murray Wonham, P. S. Thiagarajan
2007 conf
CDC
Lei Feng, Walter Murray Wonham
2007 J jnl
Syst. Control. Lett.
Ali Saadatpoor, Walter Murray Wonham
2006 J jnl
Discret. Event Dyn. Syst.
Rong Su, Walter Murray Wonham
2006 J jnl
IEEE Trans. Autom. Control.
Chuan Ma, Walter Murray Wonham
2006 conf
ICINCO-SPSMC
A. Afzalian, Ali Saadatpoor, Walter Murray Wonham
2005 conf
ISIC
Chuan Ma, Walter Murray Wonham
2005 J jnl
IEEE Trans. Autom. Control.
Peyman Gohari, Walter Murray Wonham
2005 J jnl
IEEE Trans. Autom. Control.
Ryan J. Leduc, Bertil A. Brandin, Mark Lawford, Walter Murray Wonham
2005 J jnl
IEEE Trans. Autom. Control.
Shahin Hashtrudi-Zad, Raymond H. Kwong, Walter Murray Wonham
2005 J jnl
IEEE Trans. Autom. Control.
Rong Su, Walter Murray Wonham
2005 J jnl
IEEE Trans. Autom. Control.
Ryan J. Leduc, Bertil A. Brandin, Mark Lawford, Walter Murray Wonham
2005 J jnl
IEEE Trans. Autom. Control.
Ryan J. Leduc, Mark Lawford, Walter Murray Wonham
2005 J jnl
Discret. Event Dyn. Syst.
Max H. Queiroz, José E. R. Cury, Walter Murray Wonham
2005 J jnl
Syst. Control. Lett.
Shahin Hashtrudi-Zad, Mohammad Moosaei, Walter Murray Wonham
2005 J jnl
IEEE Trans. Autom. Control.
Sean E. Bourdon, Mark Lawford, Walter Murray Wonham
2004 J jnl
IEEE Trans. Autom. Control.
Murat Uzam, Walter Murray Wonham
2004 J jnl
IEEE Trans. Autom. Control.
Walter Murray Wonham
2004 J jnl
Discret. Event Dyn. Syst.
Kai C. Wong, Walter Murray Wonham
2004 J jnl
Discret. Event Dyn. Syst.
Rong Su, Walter Murray Wonham
2003 C conf
ACC
Sherif Abdelwahed, Walter Murray Wonham
2003 J jnl
IEEE Trans. Autom. Control.
Shahin Hashtrudi-Zad, Raymond H. Kwong, Walter Murray Wonham
2003 B conf
SMC
Sherif Abdelwahed, Walter Murray Wonham
2003 C conf
ACC
Rajinderjeet Minhas, Walter Murray Wonham
2003 J jnl
IEEE Trans. Autom. Control.
Peyman Gohari, Walter Murray Wonham
2002 conf
ISIC
Weimin Wu, Hongye Su, Jian Chu, Walter Murray Wonham
2002 J jnl
Real Time Syst.
Peter C. Y. Chen, Walter Murray Wonham
2002 J jnl
Discret. Event Dyn. Syst.
Xi-Ren Cao, Guy Cohen, Alessandro Giua, Walter Murray Wonham, Jan H. van Schuppen
2001 conf
CDC
Ryan J. Leduc, Bertil A. Brandin, Walter Murray Wonham, Mark Lawford
2000 J jnl
IEEE Trans. Syst. Man Cybern. Part B
Peyman Gohari, Walter Murray Wonham
1998 J jnl
Discret. Event Dyn. Syst.
Kai C. Wong, Walter Murray Wonham
1996 J jnl
IEEE Trans. Autom. Control.
P. Kozák, Walter Murray Wonham
1996 J jnl
Discret. Event Dyn. Syst.
Kai C. Wong, Walter Murray Wonham
1996 J jnl
Discret. Event Dyn. Syst.
Kai C. Wong, Walter Murray Wonham
1996 C conf
FORTE
Mark Lawford, Jonathan S. Ostroff, Walter Murray Wonham
1995 J jnl
IEEE Trans. Autom. Control.
Yong Li, Walter Murray Wonham
1995 J jnl
IEEE Trans. Autom. Control.
Mark Lawford, Walter Murray Wonham
1995 J jnl
IEEE Trans. Autom. Control.
P. Kozák, Walter Murray Wonham
1995 J jnl
IEEE Trans. Autom. Control.
Feng Lin, Walter Murray Wonham
1994 J jnl
IEEE Trans. Autom. Control.
Bertil A. Brandin, Walter Murray Wonham
1993 J jnl
IEEE Trans. Autom. Control.
Yong Li, Walter Murray Wonham
1993 conf
ICRA (1)
Bertil A. Brandin, Walter Murray Wonham, Beno Benhabib
1992 A* conf
ICRA
Bertil A. Brandin, Walter Murray Wonham, Beno Benhabib
1991 B conf
CSL
John G. Thistle, Walter Murray Wonham
1990 conf
PSTV
Karen Rudie, Walter Murray Wonham
1989 J jnl
Proc. IEEE
Peter J. Ramadge, Walter Murray Wonham
1988 J jnl
Inf. Sci.
Feng Lin, Walter Murray Wonham
1988 J jnl
Inf. Sci.
Yang Li, Walter Murray Wonham
1988 J jnl
Inf. Sci.
Feng Lin, Walter Murray Wonham
1987 A conf
RTSS
Jonathan S. Ostroff, Walter Murray Wonham
1984 J jnl
Math. Syst. Theory
J. S. A. Hepburn, Walter Murray Wonham
1979 J jnl
Autom.
Walter Murray Wonham
1976 J jnl
Autom.
O. A. Sebakhy, Walter Murray Wonham
1976 J jnl
Autom.
Bruce A. Francis, Walter Murray Wonham
1976 J jnl
IEEE Trans. Syst. Man Cybern.
Walter Murray Wonham
1972 J jnl
Inf. Control.
S. P. Bhattacharyya, J. B. Pearson, Walter Murray Wonham
1969 J jnl
Autom.
Walter Murray Wonham
1960 J jnl
IRE Trans. Inf. Theory
Walter Murray Wonham
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>
"#
    )
}