├── rustfmt.toml ├── .gitattributes ├── src ├── lib.rs ├── errors │ └── mod.rs ├── tablegen │ └── mod.rs └── main.rs ├── tests ├── garbage ├── test-s5-n.html ├── test-s5.html ├── test.csv ├── test.tsv ├── test-s2-n.html ├── test-s1-n.html ├── test-r.html ├── test-n.html ├── test-s0-n.html ├── test-default.html ├── test-c-t.html ├── test-attrs.html └── e2e.rs ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── Cargo.toml ├── justfile ├── LICENSE ├── README.md └── Cargo.lock /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /Cargo.lock -diff 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod errors; 2 | pub mod tablegen; 3 | -------------------------------------------------------------------------------- /tests/garbage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbohdan/csv2html/HEAD/tests/garbage -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /attic 2 | /build/ 3 | /dist/ 4 | /.venv* 5 | *.egg* 6 | *.pyc 7 | *.sublime-* 8 | 9 | /target/ 10 | -------------------------------------------------------------------------------- /tests/test-s5-n.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
358bazOndřej MrázekEtiam placerat venenatis erat, sed dapibus libero pellentesque non.2017-07-21
359quuxLeah HermanInteger efficitur ullamcorper libero, vitae dignissim libero feugiat ut.2017-07-22
5 | -------------------------------------------------------------------------------- /tests/test-s5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
#itemnamecommentdate
358bazOndřej MrázekEtiam placerat venenatis erat, sed dapibus libero pellentesque non.2017-07-21
359quuxLeah HermanInteger efficitur ullamcorper libero, vitae dignissim libero feugiat ut.2017-07-22
6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | os: 10 | - macos-latest 11 | - ubuntu-latest 12 | - windows-latest 13 | steps: 14 | - name: 'Disable `autocrlf` in Git' 15 | run: git config --global core.autocrlf false 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | - name: Set up just task runner 19 | uses: extractions/setup-just@v2 20 | - name: Run tests 21 | run: | 22 | just test 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "csv2html" 3 | version = "3.1.1" 4 | authors = ["D. Bohdan "] 5 | edition = "2021" 6 | license = "BSD-3-Clause" 7 | readme = "README.md" 8 | repository = "https://github.com/dbohdan/csv2html" 9 | homepage = "https://github.com/dbohdan/csv2html" 10 | description = "Convert CSV files to HTML tables" 11 | keywords = ["csv"] 12 | categories = ["command-line-utilities"] 13 | 14 | [dependencies] 15 | clap = { version = "4.5.2", features = ["cargo"] } 16 | csv = "1.3" 17 | exitcode = "1.1.2" 18 | snafu = "0.8.2" 19 | 20 | [dev-dependencies] 21 | regex = "1" 22 | -------------------------------------------------------------------------------- /tests/test.csv: -------------------------------------------------------------------------------- 1 | #,item,name,comment,date 2 | 354,foo,Peggy C. Valdez,"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",2017-07-20 3 | 355,foo,Mildred A. Perkins,"In eget odio interdum, pharetra orci eu, blandit dolor.",2017-07-20 4 | 356,bar,Richard Porath,Quisque vitae mollis justo.,2017-07-20 5 | 357,bar,Roger P. Lyle,"Suspendisse aliquam nibh lacus, eget tincidunt orci imperdiet in. Duis sed finibus odio. Sed luctus purus at iaculis efficitur.",2017-07-21 6 | 358,baz,Ondřej Mrázek,"Etiam placerat venenatis erat, sed dapibus libero pellentesque non.",2017-07-21 7 | 359,quux,Leah Herman,"Integer efficitur ullamcorper libero, vitae dignissim libero feugiat ut.",2017-07-22 8 | -------------------------------------------------------------------------------- /tests/test.tsv: -------------------------------------------------------------------------------- 1 | # item name comment date 2 | 354 foo Peggy C. Valdez "Lorem ipsum dolor sit amet, consectetur adipiscing elit." 2017-07-20 3 | 355 foo Mildred A. Perkins "In eget odio interdum, pharetra orci eu, blandit dolor." 2017-07-20 4 | 356 bar Richard Porath Quisque vitae mollis justo. 2017-07-20 5 | 357 bar Roger P. Lyle "Suspendisse aliquam nibh lacus, eget tincidunt orci imperdiet in. Duis sed finibus odio. Sed luctus purus at iaculis efficitur." 2017-07-21 6 | 358 baz Ondřej Mrázek "Etiam placerat venenatis erat, sed dapibus libero pellentesque non." 2017-07-21 7 | 359 quux Leah Herman "Integer efficitur ullamcorper libero, vitae dignissim libero feugiat ut." 2017-07-22 8 | -------------------------------------------------------------------------------- /tests/test-s2-n.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
355fooMildred A. PerkinsIn eget odio interdum, pharetra orci eu, blandit dolor.2017-07-20
356barRichard PorathQuisque vitae mollis justo.2017-07-20
357barRoger P. LyleSuspendisse aliquam nibh lacus, eget tincidunt orci imperdiet in. Duis sed finibus odio. Sed luctus purus at iaculis efficitur.2017-07-21
358bazOndřej MrázekEtiam placerat venenatis erat, sed dapibus libero pellentesque non.2017-07-21
359quuxLeah HermanInteger efficitur ullamcorper libero, vitae dignissim libero feugiat ut.2017-07-22
8 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] 2 | 3 | export CSV2HTML_COMMAND := 'target/debug/csv2html' 4 | 5 | default: test 6 | 7 | debug: 8 | cargo build 9 | 10 | [unix] 11 | release: release-linux release-windows 12 | 13 | [unix] 14 | release-linux: 15 | cargo build --release --target x86_64-unknown-linux-musl 16 | cp target/x86_64-unknown-linux-musl/release/csv2html csv2html-linux-x86_64 17 | strip csv2html-linux-x86_64 18 | 19 | [unix] 20 | release-windows: 21 | cargo build --release --target i686-pc-windows-gnu 22 | cp target/i686-pc-windows-gnu/release/csv2html.exe csv2html-win32.exe 23 | strip csv2html-win32.exe 24 | 25 | test: debug test-unit test-e2e 26 | 27 | test-e2e: 28 | cargo test -- --ignored 29 | 30 | test-unit: 31 | cargo test 32 | -------------------------------------------------------------------------------- /tests/test-s1-n.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
354fooPeggy C. ValdezLorem ipsum dolor sit amet, consectetur adipiscing elit.2017-07-20
355fooMildred A. PerkinsIn eget odio interdum, pharetra orci eu, blandit dolor.2017-07-20
356barRichard PorathQuisque vitae mollis justo.2017-07-20
357barRoger P. LyleSuspendisse aliquam nibh lacus, eget tincidunt orci imperdiet in. Duis sed finibus odio. Sed luctus purus at iaculis efficitur.2017-07-21
358bazOndřej MrázekEtiam placerat venenatis erat, sed dapibus libero pellentesque non.2017-07-21
359quuxLeah HermanInteger efficitur ullamcorper libero, vitae dignissim libero feugiat ut.2017-07-22
9 | -------------------------------------------------------------------------------- /tests/test-r.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
#itemnamecommentdate
1fooPeggy C. ValdezLorem ipsum dolor sit amet, consectetur adipiscing elit.2017-07-20
2fooMildred A. PerkinsIn eget odio interdum, pharetra orci eu, blandit dolor.2017-07-20
3barRichard PorathQuisque vitae mollis justo.2017-07-20
4barRoger P. LyleSuspendisse aliquam nibh lacus, eget tincidunt orci imperdiet in. Duis sed finibus odio. Sed luctus purus at iaculis efficitur.2017-07-21
5bazOndřej MrázekEtiam placerat venenatis erat, sed dapibus libero pellentesque non.2017-07-21
6quuxLeah HermanInteger efficitur ullamcorper libero, vitae dignissim libero feugiat ut.2017-07-22
10 | -------------------------------------------------------------------------------- /tests/test-n.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
#itemnamecommentdate
354fooPeggy C. ValdezLorem ipsum dolor sit amet, consectetur adipiscing elit.2017-07-20
355fooMildred A. PerkinsIn eget odio interdum, pharetra orci eu, blandit dolor.2017-07-20
356barRichard PorathQuisque vitae mollis justo.2017-07-20
357barRoger P. LyleSuspendisse aliquam nibh lacus, eget tincidunt orci imperdiet in. Duis sed finibus odio. Sed luctus purus at iaculis efficitur.2017-07-21
358bazOndřej MrázekEtiam placerat venenatis erat, sed dapibus libero pellentesque non.2017-07-21
359quuxLeah HermanInteger efficitur ullamcorper libero, vitae dignissim libero feugiat ut.2017-07-22
10 | -------------------------------------------------------------------------------- /tests/test-s0-n.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
#itemnamecommentdate
354fooPeggy C. ValdezLorem ipsum dolor sit amet, consectetur adipiscing elit.2017-07-20
355fooMildred A. PerkinsIn eget odio interdum, pharetra orci eu, blandit dolor.2017-07-20
356barRichard PorathQuisque vitae mollis justo.2017-07-20
357barRoger P. LyleSuspendisse aliquam nibh lacus, eget tincidunt orci imperdiet in. Duis sed finibus odio. Sed luctus purus at iaculis efficitur.2017-07-21
358bazOndřej MrázekEtiam placerat venenatis erat, sed dapibus libero pellentesque non.2017-07-21
359quuxLeah HermanInteger efficitur ullamcorper libero, vitae dignissim libero feugiat ut.2017-07-22
10 | -------------------------------------------------------------------------------- /tests/test-default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
#itemnamecommentdate
354fooPeggy C. ValdezLorem ipsum dolor sit amet, consectetur adipiscing elit.2017-07-20
355fooMildred A. PerkinsIn eget odio interdum, pharetra orci eu, blandit dolor.2017-07-20
356barRichard PorathQuisque vitae mollis justo.2017-07-20
357barRoger P. LyleSuspendisse aliquam nibh lacus, eget tincidunt orci imperdiet in. Duis sed finibus odio. Sed luctus purus at iaculis efficitur.2017-07-21
358bazOndřej MrázekEtiam placerat venenatis erat, sed dapibus libero pellentesque non.2017-07-21
359quuxLeah HermanInteger efficitur ullamcorper libero, vitae dignissim libero feugiat ut.2017-07-22
10 | -------------------------------------------------------------------------------- /tests/test-c-t.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Foo & Bar 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
#itemnamecommentdate
354fooPeggy C. ValdezLorem ipsum dolor sit amet, consectetur adipiscing elit.2017-07-20
355fooMildred A. PerkinsIn eget odio interdum, pharetra orci eu, blandit dolor.2017-07-20
356barRichard PorathQuisque vitae mollis justo.2017-07-20
357barRoger P. LyleSuspendisse aliquam nibh lacus, eget tincidunt orci imperdiet in. Duis sed finibus odio. Sed luctus purus at iaculis efficitur.2017-07-21
358bazOndřej MrázekEtiam placerat venenatis erat, sed dapibus libero pellentesque non.2017-07-21
359quuxLeah HermanInteger efficitur ullamcorper libero, vitae dignissim libero feugiat ut.2017-07-22
14 | 15 | 16 | -------------------------------------------------------------------------------- /src/errors/mod.rs: -------------------------------------------------------------------------------- 1 | // csv2html 2 | // Copyright (c) 2013, 2014, 2017, 2020 D. Bohdan. 3 | // License: BSD (3-clause). See the file LICENSE. 4 | 5 | use snafu::Snafu; 6 | 7 | #[derive(Debug, Snafu)] 8 | #[snafu(visibility(pub))] 9 | pub enum Error { 10 | #[snafu(display("Invalid start row argument: {}", source))] 11 | CLIStart { source: std::num::ParseIntError }, 12 | 13 | #[snafu(display("Invalid delimiter: \"{}\"", delimiter))] 14 | CLIDelimiter { delimiter: String }, 15 | 16 | #[snafu(display( 17 | "Can not open the input file \"{}\": {}", 18 | filename, 19 | source 20 | ))] 21 | OpenInput { 22 | filename: String, 23 | source: std::io::Error, 24 | }, 25 | 26 | #[snafu(display( 27 | "Can not open the output file \"{}\": {}", 28 | filename, 29 | source 30 | ))] 31 | OpenOutput { 32 | filename: String, 33 | source: std::io::Error, 34 | }, 35 | 36 | #[snafu(display("Can not parse the CSV header: {}", source))] 37 | ParseHeader { source: csv::Error }, 38 | 39 | #[snafu(display("Can not parse a CSV row: {}", source))] 40 | ParseRow { source: csv::Error }, 41 | 42 | #[snafu(display("Can not write to the output file: {}", source))] 43 | WriteOutput { source: std::io::Error }, 44 | } 45 | 46 | pub type Result = std::result::Result; 47 | -------------------------------------------------------------------------------- /tests/test-attrs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
#itemnamecommentdate
354fooPeggy C. ValdezLorem ipsum dolor sit amet, consectetur adipiscing elit.2017-07-20
355fooMildred A. PerkinsIn eget odio interdum, pharetra orci eu, blandit dolor.2017-07-20
356barRichard PorathQuisque vitae mollis justo.2017-07-20
357barRoger P. LyleSuspendisse aliquam nibh lacus, eget tincidunt orci imperdiet in. Duis sed finibus odio. Sed luctus purus at iaculis efficitur.2017-07-21
358bazOndřej MrázekEtiam placerat venenatis erat, sed dapibus libero pellentesque non.2017-07-21
359quuxLeah HermanInteger efficitur ullamcorper libero, vitae dignissim libero feugiat ut.2017-07-22
10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014, 2017, 2020-2021, 2023-2025 D. Bohdan. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of csv2html nor the names of its contributors may be 15 | used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /src/tablegen/mod.rs: -------------------------------------------------------------------------------- 1 | // csv2html 2 | // Copyright (c) 2013, 2014, 2017, 2020 D. Bohdan. 3 | // License: BSD (3-clause). See the file LICENSE. 4 | 5 | pub fn escape(s: &str) -> String { 6 | s.replace("&", "&") 7 | .replace("<", "<") 8 | .replace(">", ">") 9 | .replace("\"", """) 10 | } 11 | 12 | fn tag_with_attrs(tag: &str, attrs: &str) -> String { 13 | if attrs == "" { 14 | format!("<{}>", tag) 15 | } else { 16 | format!("<{} {}>", tag, attrs) 17 | } 18 | } 19 | 20 | pub fn start(complete_doc: bool, title: &str, table_attrs: &str) -> String { 21 | let mut s = String::new(); 22 | 23 | if complete_doc { 24 | s.push_str(&format!( 25 | "\n\n{}\n\n", 26 | escape(title), 27 | )); 28 | } 29 | 30 | s.push_str(&tag_with_attrs("table", table_attrs)); 31 | s.push('\n'); 32 | 33 | s 34 | } 35 | 36 | pub fn end(complete_doc: bool) -> String { 37 | let mut s = "\n".to_string(); 38 | 39 | if complete_doc { 40 | s.push_str("\n\n"); 41 | } 42 | 43 | s 44 | } 45 | 46 | pub fn row( 47 | cols: &[&str], 48 | header: bool, 49 | row_attrs: &str, 50 | col_attrs: &str, 51 | ) -> String { 52 | let col_tag = if header { "th" } else { "td" }; 53 | 54 | let mut s = String::new(); 55 | 56 | s.push_str(&tag_with_attrs("tr", row_attrs)); 57 | 58 | for col in cols { 59 | s.push_str(&format!( 60 | "{}{}", 61 | &tag_with_attrs(col_tag, col_attrs), 62 | &escape(col), 63 | &col_tag 64 | )); 65 | } 66 | 67 | s.push_str("\n"); 68 | 69 | s 70 | } 71 | 72 | #[cfg(test)] 73 | mod test { 74 | use super::*; 75 | 76 | #[test] 77 | fn test_start_1() { 78 | assert_eq!(start(false, "<\"Greetings!\">", "x=y"), "\n"); 79 | } 80 | 81 | #[test] 82 | fn test_start_2() { 83 | assert_eq!( 84 | start(true, "<\"Greetings!\">", ""), 85 | "\n\n<"\ 86 | Greetings!">\n\n
\n" 87 | ); 88 | } 89 | 90 | #[test] 91 | fn test_end() { 92 | assert_eq!(end(true), "
\n\n\n"); 93 | } 94 | 95 | #[test] 96 | fn test_row_1() { 97 | assert_eq!( 98 | row(&vec!["foo", "bar", "baz"], false, "", ""), 99 | "foobarbaz\n" 100 | ) 101 | } 102 | 103 | #[test] 104 | fn test_row_2() { 105 | assert_eq!( 106 | row(&vec!["one", "two"], true, "x=1", "y=2"), 107 | "onetwo\n" 108 | ) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # csv2html 2 | 3 | This command-line utility converts [CSV files](http://en.wikipedia.org/wiki/Comma-separated_values) to HTML tables and complete HTML documents. 4 | It can use the first row of the CSV file as the [header](https://developer.mozilla.org/en/docs/Web/HTML/Element/th) of the table, and does so by default. 5 | 6 | The original Python version of csv2html is preserved in the branch [`python`](https://github.com/dbohdan/csv2html/tree/python). 7 | 8 | 9 | ## Installation 10 | 11 | Prebuilt Linux and Windows binaries are available. 12 | They are attached to releases on the ["Releases"](https://github.com/dbohdan/csv2html/releases) page. 13 | 14 | csv2html requires Rust 1.74 or later to build. 15 | 16 | ### Installing with Cargo 17 | 18 | ```sh 19 | cargo install csv2html 20 | ``` 21 | 22 | ### Building on Debian and Ubuntu 23 | 24 | Follow the instructions to build a static Linux binary of csv2html from the source code on recent Debian and Ubuntu. 25 | 26 | 1\. Install [Rustup](https://rustup.rs/). 27 | Through Rustup, add the stable musl libc target for your CPU. 28 | 29 | ```sh 30 | rustup target add x86_64-unknown-linux-musl 31 | ``` 32 | 33 | 2\. Install the build and test dependencies. 34 | 35 | ```sh 36 | sudo apt install build-essential musl-tools 37 | cargo install just 38 | ``` 39 | 40 | 3\. Clone this repository. 41 | Build the binary. 42 | 43 | ```sh 44 | git clone https://github.com/dbohdan/csv2html 45 | cd csv2html 46 | just test 47 | just release-linux 48 | ``` 49 | 50 | ### Cross-compiling for Windows 51 | 52 | Follow the instructions to build a 32-bit Windows binary of csv2html on recent Debian and Ubuntu. 53 | 54 | 1\. Install [Rustup](https://rustup.rs/). 55 | Through Rustup, add the i686 GNU ABI Windows target. 56 | 57 | ```sh 58 | rustup target add i686-pc-windows-gnu 59 | ``` 60 | 61 | 2\. Install the build dependencies. 62 | 63 | ```sh 64 | sudo apt install build-essential mingw-w64 65 | cargo install just 66 | ``` 67 | 68 | 3\. Configure Cargo for cross-compilation. 69 | Add the following in `~/.cargo/config`. 70 | 71 | ```toml 72 | [target.i686-pc-windows-gnu] 73 | linker = "/usr/bin/i686-w64-mingw32-gcc" 74 | ``` 75 | 76 | 4\. Clone this repository. 77 | Build the binary. 78 | 79 | ```sh 80 | git clone https://github.com/dbohdan/csv2html 81 | cd csv2html 82 | just release-windows 83 | ``` 84 | 85 | ## Command-line arguments 86 | 87 | ```none 88 | Convert CSV files to HTML tables 89 | 90 | Usage: csv2html [OPTIONS] [input] 91 | 92 | Arguments: 93 | [input] Input file 94 | 95 | Options: 96 | -o, --output Output file 97 | -t, --title HTML document title 98 | -d, --delimiter <DELIM> Field delimiter character for CSV (',' by default) 99 | -s, --start <N> Skip the first N-1 rows; start at row N 100 | -r, --renumber Replace the first column with row numbers 101 | -n, --no-header Do not use the first row of the input as the header 102 | -c, --complete-document Output a complete HTML document instead of only a 103 | table 104 | --table <ATTRS> HTML attributes for the tag <table> (e.g., --table 105 | 'foo="bar" baz' results in the output <table foo="bar" baz>...</table>); it is 106 | up to the user to ensure the result is valid HTML 107 | --tr <ATTRS> Attributes for <tr> 108 | --th <ATTRS> Attributes for <th> 109 | --td <ATTRS> Attributes for <td> 110 | -h, --help Print help 111 | -V, --version Print version 112 | ``` 113 | 114 | ## Use examples 115 | 116 | This command reads data from `test/test.csv` and writes an HTML table to `test.html`: 117 | 118 | ```sh 119 | csv2html -o test.html tests/test.csv 120 | ``` 121 | 122 | The following command takes semicolon-delimited data from `pub.csv`, starting with row 267. 123 | It replaces the first column of the table with the row number starting at 1 (except in the header row, which is not changed). 124 | The output is redirected to the file `pub.html`. 125 | 126 | ```sh 127 | csv2html pub.csv -d \; -r -s 267 > pub.html 128 | ``` 129 | 130 | The same as above, but the output is a full HTML document instead of just the markup for the table: 131 | 132 | ```sh 133 | csv2html pub.csv -d \; -r -s 267 -c > pub.html 134 | ``` 135 | 136 | If the input file is tab-delimited, use `\t` as the deliminter argument. 137 | 138 | ```sh 139 | # POSIX. 140 | csv2html --delimiter '\t' tests/test.tsv 141 | ``` 142 | 143 | ```batch 144 | rem Windows. 145 | csv2html-win32.exe --delimiter \t tests/test.tsv 146 | ``` 147 | 148 | `\t` is the only [backslash escape sequence](https://en.wikipedia.org/wiki/Escape_sequences_in_C) that is implemented. 149 | 150 | ## License 151 | 152 | Three-clause ("new" or "revised") BSD. 153 | See the file `LICENSE`. 154 | -------------------------------------------------------------------------------- /tests/e2e.rs: -------------------------------------------------------------------------------- 1 | // End-to-end tests for csv2html. 2 | // Copyright (c) 2013-2014, 2017, 2020, 2021, 2024-2025 D. Bohdan. 3 | // License: BSD (3-clause). See the file LICENSE. 4 | 5 | use std::{ 6 | convert::AsRef, 7 | env, 8 | ffi::OsStr, 9 | fs, 10 | io::{Result, Write}, 11 | iter::IntoIterator, 12 | process::{Command, Stdio}, 13 | }; 14 | use {exitcode, regex::Regex}; 15 | 16 | #[derive(Debug, Eq, PartialEq)] 17 | struct Output { 18 | code: i32, 19 | stderr: String, 20 | stdout: String, 21 | } 22 | 23 | fn csv2html_cmd() -> String { 24 | env::var("CSV2HTML_COMMAND") 25 | .expect("Environment variable CSV2HTML_COMMAND should be set") 26 | } 27 | 28 | fn csv2html<I, S>(args: I) -> Result<Output> 29 | where 30 | I: IntoIterator<Item = S>, 31 | S: AsRef<OsStr>, 32 | { 33 | let output = Command::new(csv2html_cmd()).args(args).output()?; 34 | 35 | Ok(Output { 36 | code: output.status.code().unwrap(), 37 | stderr: String::from_utf8(output.stderr).unwrap(), 38 | stdout: String::from_utf8(output.stdout).unwrap(), 39 | }) 40 | } 41 | 42 | fn read_file<S>(filename: S) -> Result<String> 43 | where 44 | S: AsRef<str> + std::fmt::Display, 45 | { 46 | fs::read_to_string(format!("tests/{}", filename)) 47 | } 48 | 49 | #[test] 50 | #[ignore] 51 | fn help_message() { 52 | let re = Regex::new(r"Convert CSV files to HTML tables").unwrap(); 53 | 54 | let output = csv2html(&["-h"]).unwrap(); 55 | assert!(re.is_match(&output.stdout)); 56 | assert_eq!(output.code, exitcode::OK); 57 | } 58 | 59 | #[test] 60 | #[ignore] 61 | fn version() { 62 | let re = Regex::new(r"\d+\.\d+\.\d+\s*$").unwrap(); 63 | 64 | let output = csv2html(&["--version"]).unwrap(); 65 | assert!(re.is_match(&output.stdout)); 66 | assert_eq!(output.code, exitcode::OK); 67 | } 68 | 69 | #[test] 70 | #[ignore] 71 | fn default() { 72 | let output = csv2html(&["tests/test.csv"]).unwrap(); 73 | let reference = read_file("test-default.html").unwrap(); 74 | 75 | assert_eq!(output.stdout, reference); 76 | assert_eq!(output.code, exitcode::OK); 77 | } 78 | 79 | #[test] 80 | #[ignore] 81 | fn tab_escape() { 82 | let output = csv2html(&["--delimiter", "\\t", "tests/test.tsv"]).unwrap(); 83 | let reference = read_file("test-default.html").unwrap(); 84 | 85 | assert_eq!(output.stdout, reference); 86 | assert_eq!(output.code, exitcode::OK); 87 | } 88 | 89 | #[test] 90 | #[ignore] 91 | fn tab_literal() { 92 | let output = csv2html(&["--delimiter", "\t", "tests/test.tsv"]).unwrap(); 93 | let reference = read_file("test-default.html").unwrap(); 94 | 95 | assert_eq!(output.stdout, reference); 96 | assert_eq!(output.code, exitcode::OK); 97 | } 98 | 99 | #[test] 100 | #[ignore] 101 | fn stdin() { 102 | let input = read_file("test.csv").unwrap(); 103 | let reference = read_file("test-default.html").unwrap(); 104 | 105 | let mut proc = Command::new(csv2html_cmd()) 106 | .args(&["-"]) 107 | .stdin(Stdio::piped()) 108 | .stdout(Stdio::piped()) 109 | .spawn() 110 | .unwrap(); 111 | 112 | let _ = proc.stdin.take().unwrap().write_all(input.as_bytes()); 113 | let output = proc.wait_with_output().unwrap(); 114 | 115 | assert_eq!(String::from_utf8(output.stdout).unwrap(), reference); 116 | assert_eq!(output.status.code().unwrap(), exitcode::OK); 117 | } 118 | 119 | #[test] 120 | #[ignore] 121 | fn complete_doc_and_title() { 122 | let output = csv2html(&[ 123 | "--title", 124 | "Foo & Bar", 125 | "--complete-document", 126 | "tests/test.csv", 127 | ]) 128 | .unwrap(); 129 | let reference = read_file("test-c-t.html").unwrap(); 130 | 131 | assert_eq!(output.stdout, reference); 132 | assert_eq!(output.code, exitcode::OK); 133 | } 134 | 135 | #[test] 136 | #[ignore] 137 | fn renum() { 138 | let output = csv2html(&["--renumber", "tests/test.csv"]).unwrap(); 139 | let reference = read_file("test-r.html").unwrap(); 140 | 141 | assert_eq!(output.stdout, reference); 142 | assert_eq!(output.code, exitcode::OK); 143 | } 144 | 145 | #[test] 146 | #[ignore] 147 | fn no_header() { 148 | let output = csv2html(&["-n", "tests/test.csv"]).unwrap(); 149 | let reference = read_file("test-n.html").unwrap(); 150 | 151 | assert_eq!(output.stdout, reference); 152 | assert_eq!(output.code, exitcode::OK); 153 | } 154 | 155 | #[test] 156 | #[ignore] 157 | fn start_5() { 158 | let output = csv2html(&["--start", "5", "tests/test.csv"]).unwrap(); 159 | let reference = read_file("test-s5.html").unwrap(); 160 | 161 | assert_eq!(output.stdout, reference); 162 | assert_eq!(output.code, exitcode::OK); 163 | } 164 | 165 | #[test] 166 | #[ignore] 167 | fn start_0_and_no_header() { 168 | let output = 169 | csv2html(&["--start", "0", "--no-header", "tests/test.csv"]).unwrap(); 170 | let reference = read_file("test-s0-n.html").unwrap(); 171 | 172 | assert_eq!(output.stdout, reference); 173 | assert_eq!(output.code, exitcode::OK); 174 | } 175 | 176 | #[test] 177 | #[ignore] 178 | fn start_1_and_no_header() { 179 | let output = 180 | csv2html(&["--start", "1", "--no-header", "tests/test.csv"]).unwrap(); 181 | let reference = read_file("test-s1-n.html").unwrap(); 182 | 183 | assert_eq!(output.stdout, reference); 184 | assert_eq!(output.code, exitcode::OK); 185 | } 186 | 187 | #[test] 188 | #[ignore] 189 | fn start_2_and_no_header() { 190 | let output = 191 | csv2html(&["--start", "2", "--no-header", "tests/test.csv"]).unwrap(); 192 | let reference = read_file("test-s2-n.html").unwrap(); 193 | 194 | assert_eq!(output.stdout, reference); 195 | assert_eq!(output.code, exitcode::OK); 196 | } 197 | 198 | #[test] 199 | #[ignore] 200 | fn start_5_and_no_header() { 201 | let output = 202 | csv2html(&["--start", "5", "--no-header", "tests/test.csv"]).unwrap(); 203 | let reference = read_file("test-s5-n.html").unwrap(); 204 | 205 | assert_eq!(output.stdout, reference); 206 | assert_eq!(output.code, exitcode::OK); 207 | } 208 | 209 | #[test] 210 | #[ignore] 211 | fn attrs() { 212 | let output = csv2html(&[ 213 | "--table", 214 | "class=\"foo\" id=\"bar\"", 215 | "--tr", 216 | "class=\"row\"", 217 | "--th", 218 | "class=\"hcell\"", 219 | "--td", 220 | "class=\"cell\"", 221 | "tests/test.csv", 222 | ]) 223 | .unwrap(); 224 | let reference = read_file("test-attrs.html").unwrap(); 225 | 226 | assert_eq!(output.stdout, reference); 227 | assert_eq!(output.code, exitcode::OK); 228 | } 229 | 230 | #[test] 231 | #[ignore] 232 | fn no_file() { 233 | let re = Regex::new(r".*Can not open the input file.*").unwrap(); 234 | 235 | let output = csv2html(&["tests/does-not-exist.csv"]).unwrap(); 236 | assert!(re.is_match(&output.stderr)); 237 | assert_eq!(output.code, exitcode::IOERR); 238 | } 239 | 240 | #[test] 241 | #[ignore] 242 | fn garbage_file() { 243 | let re = Regex::new(r".*Can not parse.*").unwrap(); 244 | 245 | let output = csv2html(&["tests/garbage"]).unwrap(); 246 | assert!(re.is_match(&output.stderr)); 247 | assert_eq!(output.code, exitcode::DATAERR); 248 | } 249 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // csv2html 2 | // Copyright (c) 2013-2014, 2017, 2020, 2024 D. Bohdan. 3 | // License: BSD (3-clause). See the file LICENSE. 4 | 5 | #![recursion_limit = "1024"] 6 | 7 | use std::{ 8 | fs::File, 9 | io::{BufRead, BufReader, BufWriter, Write}, 10 | process::exit, 11 | }; 12 | 13 | use clap::{ 14 | crate_description, crate_name, crate_version, Arg, ArgAction, Command, 15 | }; 16 | use csv::ReaderBuilder; 17 | use exitcode; 18 | use snafu::{ensure, ResultExt}; 19 | 20 | use csv2html::{errors, tablegen}; 21 | 22 | #[derive(Debug)] 23 | struct Opts { 24 | input: String, 25 | output: String, 26 | title: String, 27 | delimiter: u8, 28 | start: usize, 29 | renumber: bool, 30 | header: bool, 31 | complete_document: bool, 32 | table_attrs: String, 33 | th_attrs: String, 34 | tr_attrs: String, 35 | td_attrs: String, 36 | } 37 | 38 | fn cli() -> errors::Result<Opts> { 39 | let matches = Command::new(crate_name!()) 40 | .version(crate_version!()) 41 | .about(crate_description!()) 42 | .arg( 43 | Arg::new("input") 44 | .help("Input file") 45 | .default_value("-") 46 | .hide_default_value(true), 47 | ) 48 | .arg( 49 | Arg::new("output") 50 | .short('o') 51 | .long("output") 52 | .value_name("OUTPUT") 53 | .help("Output file") 54 | .default_value("-") 55 | .hide_default_value(true), 56 | ) 57 | .arg( 58 | Arg::new("title") 59 | .short('t') 60 | .long("title") 61 | .value_name("TITLE") 62 | .help("HTML document title") 63 | .default_value("") 64 | .hide_default_value(true), 65 | ) 66 | .arg( 67 | Arg::new("delimiter") 68 | .short('d') 69 | .long("delimiter") 70 | .value_name("DELIM") 71 | .help("Field delimiter character for CSV (',' by default)") 72 | .default_value(",") 73 | .hide_default_value(true), 74 | ) 75 | .arg( 76 | Arg::new("start") 77 | .short('s') 78 | .long("start") 79 | .value_name("N") 80 | .help("Skip the first N-1 rows; start at row N") 81 | .default_value("0") 82 | .hide_default_value(true), 83 | ) 84 | .arg( 85 | Arg::new("renumber") 86 | .short('r') 87 | .long("renumber") 88 | .help("Replace the first column with row numbers") 89 | .action(ArgAction::SetTrue), 90 | ) 91 | .arg( 92 | Arg::new("no-header") 93 | .short('n') 94 | .long("no-header") 95 | .help("Do not use the first row of the input as the header") 96 | .action(ArgAction::SetTrue), 97 | ) 98 | .arg( 99 | Arg::new("complete-document") 100 | .short('c') 101 | .long("complete-document") 102 | .help("Output a complete HTML document instead of only a table") 103 | .action(ArgAction::SetTrue), 104 | ) 105 | .arg( 106 | Arg::new("table") 107 | .long("table") 108 | .value_name("ATTRS") 109 | .help( 110 | "HTML attributes for the tag <table> (e.g., --table \ 111 | 'foo=\"bar\" baz' results in the output <table \ 112 | foo=\"bar\" baz>...</table>); it is up to the \ 113 | user to ensure the result is valid HTML", 114 | ) 115 | .default_value("") 116 | .hide_default_value(true), 117 | ) 118 | .arg( 119 | Arg::new("tr") 120 | .long("tr") 121 | .value_name("ATTRS") 122 | .help("Attributes for <tr>") 123 | .default_value("") 124 | .hide_default_value(true), 125 | ) 126 | .arg( 127 | Arg::new("th") 128 | .long("th") 129 | .value_name("ATTRS") 130 | .help("Attributes for <th>") 131 | .default_value("") 132 | .hide_default_value(true), 133 | ) 134 | .arg( 135 | Arg::new("td") 136 | .long("td") 137 | .value_name("ATTRS") 138 | .help("Attributes for <td>") 139 | .default_value("") 140 | .hide_default_value(true), 141 | ) 142 | .get_matches(); 143 | 144 | let start_s = matches.get_one::<String>("start").unwrap().to_string(); 145 | 146 | let start = start_s.parse::<usize>().context(errors::CLIStartSnafu {})?; 147 | 148 | let delimiter_s = matches.get_one::<String>("delimiter").unwrap(); 149 | let tab_escape = "\\t"; 150 | 151 | ensure!( 152 | delimiter_s.len() == 1 || delimiter_s == tab_escape, 153 | errors::CLIDelimiterSnafu { 154 | delimiter: delimiter_s 155 | } 156 | ); 157 | 158 | let delimiter = if delimiter_s == tab_escape { 159 | "\t" 160 | } else { 161 | delimiter_s 162 | } 163 | .bytes() 164 | .nth(0) 165 | .unwrap(); 166 | 167 | Ok(Opts { 168 | input: matches.get_one::<String>("input").unwrap().to_string(), 169 | output: matches.get_one::<String>("output").unwrap().to_string(), 170 | start: start, 171 | delimiter: delimiter, 172 | title: matches.get_one::<String>("title").unwrap().to_string(), 173 | renumber: matches.get_flag("renumber"), 174 | header: !matches.get_flag("no-header"), 175 | complete_document: matches.get_flag("complete-document"), 176 | table_attrs: matches.get_one::<String>("table").unwrap().to_string(), 177 | tr_attrs: matches.get_one::<String>("tr").unwrap().to_string(), 178 | th_attrs: matches.get_one::<String>("th").unwrap().to_string(), 179 | td_attrs: matches.get_one::<String>("td").unwrap().to_string(), 180 | }) 181 | } 182 | 183 | fn app() -> errors::Result<()> { 184 | let opts = cli()?; 185 | 186 | let input: Box<dyn BufRead> = if &opts.input == "-" { 187 | Box::new(BufReader::new(std::io::stdin())) 188 | } else { 189 | Box::new(BufReader::new(File::open(&opts.input).context( 190 | errors::OpenInputSnafu { 191 | filename: &opts.input, 192 | }, 193 | )?)) 194 | }; 195 | 196 | let mut output: Box<dyn Write> = if &opts.output == "-" { 197 | Box::new(BufWriter::new(std::io::stdout())) 198 | } else { 199 | Box::new(BufWriter::new(File::create(&opts.output).context( 200 | errors::OpenOutputSnafu { 201 | filename: &opts.output, 202 | }, 203 | )?)) 204 | }; 205 | 206 | write!( 207 | output, 208 | "{}", 209 | tablegen::start(opts.complete_document, &opts.title, &opts.table_attrs) 210 | ) 211 | .context(errors::WriteOutputSnafu {})?; 212 | 213 | let mut csv_reader = ReaderBuilder::new() 214 | .flexible(true) 215 | .has_headers(opts.header) 216 | .delimiter(opts.delimiter) 217 | .from_reader(input); 218 | 219 | if opts.header { 220 | let headers = csv_reader 221 | .headers() 222 | .context(errors::ParseHeaderSnafu {})? 223 | .iter() 224 | .collect::<Vec<_>>(); 225 | 226 | write!( 227 | output, 228 | "{}", 229 | tablegen::row(&headers, true, &opts.tr_attrs, &opts.th_attrs) 230 | ) 231 | .context(errors::WriteOutputSnafu {})?; 232 | } 233 | 234 | let mut i: u64 = 1; 235 | let mut skip = opts.start; 236 | if skip > 0 && opts.header { 237 | skip -= 1; 238 | } 239 | 240 | for result in csv_reader.records().skip(skip) { 241 | let record = result.context(errors::ParseRowSnafu {})?; 242 | let mut row = record.iter().collect::<Vec<_>>(); 243 | 244 | let i_s = i.to_string(); 245 | 246 | if opts.renumber { 247 | row[0] = &i_s; 248 | } 249 | 250 | write!( 251 | output, 252 | "{}", 253 | tablegen::row(&row, false, &opts.tr_attrs, &opts.td_attrs) 254 | ) 255 | .context(errors::WriteOutputSnafu {})?; 256 | 257 | i += 1; 258 | } 259 | 260 | write!(output, "{}", tablegen::end(opts.complete_document)) 261 | .context(errors::WriteOutputSnafu {})?; 262 | 263 | Ok(()) 264 | } 265 | 266 | fn main() { 267 | match app() { 268 | Ok(_) => exit(exitcode::OK), 269 | Err(ref err) => match err { 270 | errors::Error::OpenInput { 271 | filename: _, 272 | source: _, 273 | } 274 | | errors::Error::OpenOutput { 275 | filename: _, 276 | source: _, 277 | } 278 | | errors::Error::WriteOutput { source: _ } => { 279 | eprintln!("{}", err); 280 | exit(exitcode::IOERR); 281 | } 282 | errors::Error::ParseHeader { source: _ } 283 | | errors::Error::ParseRow { source: _ } => { 284 | eprintln!("{}", err); 285 | exit(exitcode::DATAERR); 286 | } 287 | _ => { 288 | eprintln!("{}", err); 289 | exit(exitcode::SOFTWARE); 290 | } 291 | }, 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.14" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.7" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.4" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.0.3" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" 49 | dependencies = [ 50 | "windows-sys", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.3" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 58 | dependencies = [ 59 | "anstyle", 60 | "windows-sys", 61 | ] 62 | 63 | [[package]] 64 | name = "clap" 65 | version = "4.5.4" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" 68 | dependencies = [ 69 | "clap_builder", 70 | ] 71 | 72 | [[package]] 73 | name = "clap_builder" 74 | version = "4.5.2" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" 77 | dependencies = [ 78 | "anstream", 79 | "anstyle", 80 | "clap_lex", 81 | "strsim", 82 | ] 83 | 84 | [[package]] 85 | name = "clap_lex" 86 | version = "0.7.0" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 89 | 90 | [[package]] 91 | name = "colorchoice" 92 | version = "1.0.1" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 95 | 96 | [[package]] 97 | name = "csv" 98 | version = "1.3.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" 101 | dependencies = [ 102 | "csv-core", 103 | "itoa", 104 | "ryu", 105 | "serde", 106 | ] 107 | 108 | [[package]] 109 | name = "csv-core" 110 | version = "0.1.11" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" 113 | dependencies = [ 114 | "memchr", 115 | ] 116 | 117 | [[package]] 118 | name = "csv2html" 119 | version = "3.1.1" 120 | dependencies = [ 121 | "clap", 122 | "csv", 123 | "exitcode", 124 | "regex", 125 | "snafu", 126 | ] 127 | 128 | [[package]] 129 | name = "exitcode" 130 | version = "1.1.2" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" 133 | 134 | [[package]] 135 | name = "heck" 136 | version = "0.4.1" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 139 | 140 | [[package]] 141 | name = "is_terminal_polyfill" 142 | version = "1.70.0" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 145 | 146 | [[package]] 147 | name = "itoa" 148 | version = "1.0.11" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 151 | 152 | [[package]] 153 | name = "memchr" 154 | version = "2.7.2" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 157 | 158 | [[package]] 159 | name = "proc-macro2" 160 | version = "1.0.83" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" 163 | dependencies = [ 164 | "unicode-ident", 165 | ] 166 | 167 | [[package]] 168 | name = "quote" 169 | version = "1.0.36" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 172 | dependencies = [ 173 | "proc-macro2", 174 | ] 175 | 176 | [[package]] 177 | name = "regex" 178 | version = "1.10.4" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" 181 | dependencies = [ 182 | "aho-corasick", 183 | "memchr", 184 | "regex-automata", 185 | "regex-syntax", 186 | ] 187 | 188 | [[package]] 189 | name = "regex-automata" 190 | version = "0.4.6" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 193 | dependencies = [ 194 | "aho-corasick", 195 | "memchr", 196 | "regex-syntax", 197 | ] 198 | 199 | [[package]] 200 | name = "regex-syntax" 201 | version = "0.8.3" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 204 | 205 | [[package]] 206 | name = "ryu" 207 | version = "1.0.18" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 210 | 211 | [[package]] 212 | name = "serde" 213 | version = "1.0.202" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" 216 | dependencies = [ 217 | "serde_derive", 218 | ] 219 | 220 | [[package]] 221 | name = "serde_derive" 222 | version = "1.0.202" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" 225 | dependencies = [ 226 | "proc-macro2", 227 | "quote", 228 | "syn", 229 | ] 230 | 231 | [[package]] 232 | name = "snafu" 233 | version = "0.8.2" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "75976f4748ab44f6e5332102be424e7c2dc18daeaf7e725f2040c3ebb133512e" 236 | dependencies = [ 237 | "snafu-derive", 238 | ] 239 | 240 | [[package]] 241 | name = "snafu-derive" 242 | version = "0.8.2" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "b4b19911debfb8c2fb1107bc6cb2d61868aaf53a988449213959bb1b5b1ed95f" 245 | dependencies = [ 246 | "heck", 247 | "proc-macro2", 248 | "quote", 249 | "syn", 250 | ] 251 | 252 | [[package]] 253 | name = "strsim" 254 | version = "0.11.1" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 257 | 258 | [[package]] 259 | name = "syn" 260 | version = "2.0.66" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" 263 | dependencies = [ 264 | "proc-macro2", 265 | "quote", 266 | "unicode-ident", 267 | ] 268 | 269 | [[package]] 270 | name = "unicode-ident" 271 | version = "1.0.12" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 274 | 275 | [[package]] 276 | name = "utf8parse" 277 | version = "0.2.1" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 280 | 281 | [[package]] 282 | name = "windows-sys" 283 | version = "0.52.0" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 286 | dependencies = [ 287 | "windows-targets", 288 | ] 289 | 290 | [[package]] 291 | name = "windows-targets" 292 | version = "0.52.5" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 295 | dependencies = [ 296 | "windows_aarch64_gnullvm", 297 | "windows_aarch64_msvc", 298 | "windows_i686_gnu", 299 | "windows_i686_gnullvm", 300 | "windows_i686_msvc", 301 | "windows_x86_64_gnu", 302 | "windows_x86_64_gnullvm", 303 | "windows_x86_64_msvc", 304 | ] 305 | 306 | [[package]] 307 | name = "windows_aarch64_gnullvm" 308 | version = "0.52.5" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 311 | 312 | [[package]] 313 | name = "windows_aarch64_msvc" 314 | version = "0.52.5" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 317 | 318 | [[package]] 319 | name = "windows_i686_gnu" 320 | version = "0.52.5" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 323 | 324 | [[package]] 325 | name = "windows_i686_gnullvm" 326 | version = "0.52.5" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 329 | 330 | [[package]] 331 | name = "windows_i686_msvc" 332 | version = "0.52.5" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 335 | 336 | [[package]] 337 | name = "windows_x86_64_gnu" 338 | version = "0.52.5" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 341 | 342 | [[package]] 343 | name = "windows_x86_64_gnullvm" 344 | version = "0.52.5" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 347 | 348 | [[package]] 349 | name = "windows_x86_64_msvc" 350 | version = "0.52.5" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 353 | --------------------------------------------------------------------------------