├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .pre-commit-hooks.yaml ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── NEWS.md ├── README.md ├── build.rs ├── completion ├── README.md ├── _tex-fmt ├── _tex-fmt.ps1 ├── tex-fmt.bash ├── tex-fmt.elv └── tex-fmt.fish ├── default.nix ├── extra ├── binary.sh ├── card.py ├── latex.sh ├── logo.py ├── logo.svg ├── perf.sh └── prof.sh ├── flake.lock ├── flake.nix ├── justfile ├── man ├── README.md └── tex-fmt.1 ├── notes.org ├── overlay.nix ├── shell.nix ├── src ├── args.rs ├── bin.rs ├── cli.rs ├── command.rs ├── comments.rs ├── config.rs ├── format.rs ├── ignore.rs ├── indent.rs ├── lib.rs ├── logging.rs ├── read.rs ├── regexes.rs ├── subs.rs ├── tests.rs ├── verbatim.rs ├── wasm.rs ├── wrap.rs └── write.rs ├── tests ├── brackets │ ├── source │ │ └── brackets.tex │ └── target │ │ └── brackets.tex ├── comments │ ├── source │ │ └── comments.tex │ └── target │ │ └── comments.tex ├── cv │ ├── source │ │ ├── cv.tex │ │ └── wgu-cv.cls │ └── target │ │ ├── cv.tex │ │ └── wgu-cv.cls ├── empty │ ├── source │ │ └── empty.tex │ └── target │ │ └── empty.tex ├── environments │ ├── source │ │ ├── document.tex │ │ └── environment_lines.tex │ └── target │ │ ├── document.tex │ │ └── environment_lines.tex ├── higher_categories_thesis │ ├── source │ │ ├── cam-thesis.cls │ │ ├── higher_categories_thesis.bib │ │ ├── higher_categories_thesis.tex │ │ └── quiver.sty │ └── target │ │ ├── cam-thesis.cls │ │ ├── higher_categories_thesis.bib │ │ ├── higher_categories_thesis.tex │ │ └── quiver.sty ├── ignore │ ├── source │ │ └── ignore.tex │ └── target │ │ └── ignore.tex ├── linear_map_chinese │ ├── source │ │ └── linear_map_chinese.tex │ ├── target │ │ └── linear_map_chinese.tex │ └── tex-fmt.toml ├── lists │ ├── source │ │ └── lists.tex │ ├── target │ │ └── lists.tex │ └── tex-fmt.toml ├── masters_dissertation │ ├── source │ │ ├── masters_dissertation.tex │ │ ├── ociamthesis.cls │ │ └── tikz-network.sty │ └── target │ │ ├── masters_dissertation.tex │ │ ├── ociamthesis.cls │ │ └── tikz-network.sty ├── no_indent_envs │ ├── source │ │ └── no_indent_envs.tex │ ├── target │ │ └── no_indent_envs.tex │ └── tex-fmt.toml ├── phd_dissertation │ ├── source │ │ ├── phd_dissertation.tex │ │ ├── phd_dissertation_refs.bib │ │ └── puthesis.cls │ └── target │ │ ├── phd_dissertation.tex │ │ ├── phd_dissertation_refs.bib │ │ └── puthesis.cls ├── readme │ ├── source │ │ └── readme.tex │ └── target │ │ └── readme.tex ├── sections │ ├── source │ │ └── sections.tex │ └── target │ │ └── sections.tex ├── short_document │ ├── source │ │ └── short_document.tex │ └── target │ │ └── short_document.tex ├── tabsize │ ├── cli.txt │ ├── source │ │ └── tabsize.tex │ ├── target │ │ └── tabsize.tex │ └── tex-fmt.toml ├── unicode │ ├── source │ │ └── unicode.tex │ └── target │ │ └── unicode.tex ├── verb │ ├── source │ │ └── verb.tex │ └── target │ │ └── verb.tex ├── verbatim │ ├── source │ │ └── verbatim.tex │ ├── target │ │ └── verbatim.tex │ └── tex-fmt.toml ├── wrap │ ├── source │ │ ├── heavy_wrap.tex │ │ └── wrap.tex │ └── target │ │ ├── heavy_wrap.tex │ │ └── wrap.tex └── wrap_chars │ ├── source │ ├── wrap_chars.tex │ └── wrap_chars_chinese.tex │ ├── target │ ├── wrap_chars.tex │ └── wrap_chars_chinese.tex │ └── tex-fmt.toml ├── tex-fmt.toml └── web ├── index.html └── index.js /.gitattributes: -------------------------------------------------------------------------------- 1 | tests/** linguist-vendored 2 | completion/** linguist-vendored 3 | man/** linguist-vendored 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | - develop 7 | push: 8 | branches: 9 | - main 10 | - develop 11 | workflow_dispatch: 12 | jobs: 13 | test: 14 | name: Cargo test (${{ matrix.os }}) 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: [windows-latest, macos-latest, ubuntu-latest] 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: dtolnay/rust-toolchain@stable 23 | - uses: Swatinem/rust-cache@v2 24 | - name: Test 25 | run: cargo test 26 | format: 27 | name: Cargo format (${{ matrix.os }}) 28 | runs-on: ${{ matrix.os }} 29 | strategy: 30 | fail-fast: true 31 | matrix: 32 | os: [windows-latest, macos-latest, ubuntu-latest] 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: dtolnay/rust-toolchain@stable 36 | - uses: Swatinem/rust-cache@v2 37 | - name: Format 38 | run: cargo fmt --check 39 | cross: 40 | name: Cargo cross build (${{ matrix.target }}) 41 | runs-on: ubuntu-latest 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | target: 46 | - aarch64-unknown-linux-gnu 47 | - x86_64-unknown-linux-musl 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: dtolnay/rust-toolchain@stable 51 | - uses: Swatinem/rust-cache@v2 52 | - run: cargo install cross 53 | - name: Build 54 | run: cross build --target ${{ matrix.target }} 55 | wasm: 56 | name: Cargo wasm build 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@v4 60 | - uses: dtolnay/rust-toolchain@stable 61 | with: 62 | targets: wasm32-unknown-unknown 63 | - uses: Swatinem/rust-cache@v2 64 | - uses: jetli/wasm-bindgen-action@v0.2.0 65 | with: 66 | version: '0.2.100' 67 | - name: Build wasm 68 | run: cargo build -r --lib --target wasm32-unknown-unknown 69 | - name: Bind wasm 70 | run: | 71 | wasm-bindgen --target web --out-dir web/pkg \ 72 | target/wasm32-unknown-unknown/release/tex_fmt.wasm 73 | - name: Optimize wasm 74 | uses: NiklasEi/wasm-opt-action@v2 75 | with: 76 | options: -Oz 77 | file: web/pkg/tex_fmt_bg.wasm 78 | output: web/pkg/tex_fmt_bg.wasm 79 | - name: Upload wasm 80 | if: github.ref == 'refs/heads/main' 81 | uses: actions/upload-artifact@v4 82 | with: 83 | name: pkg 84 | path: web/pkg/ 85 | pages: 86 | if: github.ref == 'refs/heads/main' 87 | name: Deploy to GitHub Pages 88 | runs-on: ubuntu-latest 89 | needs: wasm 90 | steps: 91 | - uses: actions/checkout@v3 92 | - name: Download WASM and JS artifacts 93 | uses: actions/download-artifact@v4 94 | with: 95 | name: pkg 96 | path: web/pkg 97 | - uses: peaceiris/actions-gh-pages@v3 98 | with: 99 | github_token: ${{ secrets.GITHUB_TOKEN }} 100 | publish_dir: web 101 | nix: 102 | name: Nix build 103 | runs-on: ubuntu-latest 104 | steps: 105 | - uses: actions/checkout@v4 106 | - uses: cachix/install-nix-action@v25 107 | with: 108 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 109 | nix_path: nixpkgs=channel:nixos-unstable 110 | - uses: DeterminateSystems/magic-nix-cache-action@main 111 | - run: nix build 112 | - run: nix flake check --all-systems 113 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: "Publish" 2 | on: 3 | release: 4 | types: [published] 5 | workflow_dispatch: 6 | jobs: 7 | build: 8 | name: Build (${{ matrix.archive }}) 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | include: 13 | - os: windows-latest 14 | target: x86_64-pc-windows-msvc 15 | program: cargo 16 | archive: tex-fmt-x86_64-windows.zip 17 | - os: windows-latest 18 | target: i686-pc-windows-msvc 19 | program: cargo 20 | archive: tex-fmt-i686-windows.zip 21 | - os: windows-latest 22 | target: aarch64-pc-windows-msvc 23 | program: cargo 24 | archive: tex-fmt-aarch64-windows.zip 25 | - os: macos-latest 26 | target: x86_64-apple-darwin 27 | program: cargo 28 | archive: tex-fmt-x86_64-macos.tar.gz 29 | - os: macos-latest 30 | target: aarch64-apple-darwin 31 | program: cargo 32 | archive: tex-fmt-aarch64-macos.tar.gz 33 | - os: ubuntu-latest 34 | target: x86_64-unknown-linux-gnu 35 | program: cargo 36 | archive: tex-fmt-x86_64-linux.tar.gz 37 | - os: ubuntu-latest 38 | target: aarch64-unknown-linux-gnu 39 | program: cross 40 | archive: tex-fmt-aarch64-linux.tar.gz 41 | - os: ubuntu-latest 42 | target: armv7-unknown-linux-gnueabihf 43 | program: cross 44 | archive: tex-fmt-armv7hf-linux.tar.gz 45 | - os: ubuntu-latest 46 | target: x86_64-unknown-linux-musl 47 | program: cargo 48 | archive: tex-fmt-x86_64-alpine.tar.gz 49 | steps: 50 | - uses: actions/checkout@v4 51 | - uses: dtolnay/rust-toolchain@stable 52 | with: 53 | targets: ${{ matrix.target }} 54 | - name: Install cross 55 | if: ${{ matrix.program == 'cross' }} 56 | run: cargo install cross 57 | - name: Build 58 | run: | 59 | ${{ matrix.program }} build --target ${{ matrix.target }} --all-features --release --locked 60 | - name: Compress (windows) 61 | if: ${{ contains(matrix.os, 'windows') }} 62 | run: | 63 | ${{ format('Compress-Archive target/{0}/release/tex-fmt.exe {1}', 64 | matrix.target, matrix.archive) }} 65 | - name: Compress (macos) 66 | if: ${{ contains(matrix.os, 'macos') }} 67 | run: | 68 | ${{ format('gtar -czvf {1} -C target/{0}/release tex-fmt', 69 | matrix.target, matrix.archive) }} 70 | - name: Compress (linux) 71 | if: ${{ contains(matrix.os, 'ubuntu') }} 72 | run: | 73 | ${{ format('tar -czvf {1} -C target/{0}/release tex-fmt', 74 | matrix.target, matrix.archive) }} 75 | - name: Upload binary archive 76 | uses: actions/upload-artifact@v4 77 | with: 78 | name: ${{ matrix.target }} 79 | path: ${{ matrix.archive }} 80 | github: 81 | name: GitHub archive upload 82 | needs: [build] 83 | runs-on: ubuntu-latest 84 | steps: 85 | - uses: actions/checkout@v4 86 | - uses: actions/download-artifact@v4 87 | - name: Publish binaries 88 | run: | 89 | gh release upload ${{ github.ref_name }} $(find . -iname tex-fmt*.zip) 90 | gh release upload ${{ github.ref_name }} $(find . -iname tex-fmt*.tar.gz) 91 | env: 92 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /debug/ 2 | /target/ 3 | **/*.rs.bk 4 | *.pdb 5 | /result 6 | *.log 7 | flamegraph.svg 8 | perf.data* 9 | *.csv 10 | *.pdf 11 | *.png 12 | cachegrind.out 13 | /web/pkg 14 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: tex-fmt 2 | name: Format LaTeX files 3 | language: rust 4 | entry: tex-fmt 5 | args: [--fail-on-change] 6 | files: \.(tex|bib|cls|sty)$ 7 | stages: [pre-commit, pre-merge-commit, pre-push, manual] -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tex-fmt" 3 | version = "0.5.4" 4 | authors = ["William George Underwood"] 5 | license = "MIT" 6 | repository = "https://github.com/WGUNDERWOOD/tex-fmt" 7 | edition = "2021" 8 | description = "LaTeX formatter written in Rust" 9 | keywords = ["latex", "formatter"] 10 | categories = ["command-line-utilities", "development-tools"] 11 | exclude = ["tests/*", "extra/*", "*.nix", ".github/*", "completion/*", "man/*"] 12 | 13 | [dependencies] 14 | clap = { version = "4.5.37", features = ["cargo"] } 15 | clap_complete = "4.5.48" 16 | clap_mangen = "0.2.26" 17 | colored = "2.2.0" 18 | dirs = "5.0.1" 19 | env_logger = "0.11.8" 20 | js-sys = "0.3.77" 21 | log = "0.4.27" 22 | merge = "0.1.0" 23 | regex = "1.11.1" 24 | similar = "2.7.0" 25 | toml = "0.8.22" 26 | wasm-bindgen = "0.2.100" 27 | web-time = "1.1.0" 28 | 29 | [features] 30 | shellinstall = [] 31 | 32 | [build-dependencies] 33 | clap = { version = "4.5.37", features = ["cargo"] } 34 | clap_complete = "4.5.48" 35 | clap_mangen = "0.2.26" 36 | 37 | [profile.release] 38 | codegen-units = 1 39 | 40 | [lib] 41 | name = "tex_fmt" 42 | path = "src/lib.rs" 43 | crate-type = ["cdylib", "rlib"] 44 | 45 | [[bin]] 46 | name = "tex-fmt" 47 | path = "src/bin.rs" 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 William George Underwood 4 | 5 | Permission is hereby granted, free of charge, to any 6 | person obtaining a copy of this software and associated 7 | documentation files (the "Software"), to deal in the 8 | Software without restriction, including without 9 | limitation the rights to use, copy, modify, merge, 10 | publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software 12 | is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice 16 | shall be included in all copies or substantial portions 17 | of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 20 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 21 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 22 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 23 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 26 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | DEALINGS IN THE SOFTWARE. 28 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # v0.5.4 2 | 3 | ## Allow custom non-indented environments 4 | 5 | - Pass `no-indent-envs = ["mydocument"]` in `tex-fmt.toml`. 6 | - Then no indentation is applied inside `\begin{mydocument}...\end{mydocument}`. 7 | - This is the default behaviour for `\begin{document}...\end{document}`. 8 | 9 | ## Allow custom verbatim environments 10 | 11 | - Pass `verbatims = ["myverbatim"]` in `tex-fmt.toml`. 12 | - All formatting is then skipped inside `\begin{myverbatim}...\end{myverbatim}`. 13 | 14 | ## Improve formatting when using `\verb|...|` 15 | 16 | - Lines are not broken inside `\verb|...|`. 17 | - Environments inside `\verb|...|` do not trigger new lines. 18 | - No indenting applied to lines containing `\verb|...|`. 19 | 20 | ## Minor changes 21 | 22 | - Fix output bug in profiling script at `extra/prof.sh`. 23 | - Add link to [docs.rs](https://docs.rs/tex-fmt/latest/tex_fmt/) page in README. 24 | - Add logo to [docs.rs](https://docs.rs/tex-fmt/latest/tex_fmt/) page. 25 | - Add better tests for config file options and CLI arguments. 26 | - Improve documentation of config file options and CLI arguments in README. 27 | 28 | # v0.5.3 29 | 30 | - Add `--fail-on-change` flag. 31 | - Deploy web app for using tex-fmt in a browser. 32 | - Simplify testing structure. 33 | - Switch nix flake input to nixpkgs-unstable. 34 | - Update README with GitHub Action, mason.nvim, Debian, bibtex-tidy, pre-commit. 35 | 36 | # v0.5.2 37 | 38 | - Fix critical bug with config files missing the `lists` field. 39 | - Trim trailing newlines. 40 | 41 | # v0.5.1 42 | 43 | - Custom list environments can be passed using the `lists` option in the config file. 44 | - Allow `verbosity = "info"` in the config file. 45 | - Fixed a bug with configuration values being incorrectly reset. 46 | 47 | # v0.5.0 48 | 49 | Version v0.5.0 is a major release, including breaking changes and substantial new features. 50 | 51 | ## Changes to existing CLI options 52 | - The option to disable line wrapping has been changed from `--keep` to `--nowrap`. 53 | - The option to set the number of characters used per indentation level has been changed from `--tab` to `--tabsize`. 54 | - The option to set the maximum line length for wrapping has been changed from `--wrap` to `--wraplen`. 55 | - See below for information on the new `--config`, `--noconfig`, `--man`, `--completion`, and `--args` flags. 56 | 57 | ## Configuration file support 58 | Configuring tex-fmt can now be achieved using a configuration file as well as CLI arguments. The configuration file can be read from a user-specified path with `--config `, from the current working directory, from the root of the current git repository, or from the user's configuration directory, in order of decreasing priority. Arguments passed on the command line will always override those specified in configuration files. Configuration files can be disabled by passing `--noconfig`. 59 | 60 | ## Man pages 61 | Man pages can be generated using the `--man` flag. Pre-built man pages are also available for download from the GitHub repository. 62 | 63 | ## Shell completion 64 | Completion files for popular shells, including bash, fish, zsh, elvish and PowerShell, can be generated using the `--completion ` flag. Pre-built completion scripts are also available for download from the GitHub repository. 65 | 66 | ## Minor changes 67 | - Arguments passed to tex-fmt can be inspected by passing `--args` 68 | - Fixed bug with `\itemsep` matching the `\item` pattern 69 | - Added last non-indented line number to "Indent did not return to zero" error messages 70 | - Removed LTO optimization to improve compile time with minimal effect on run time 71 | - If duplicate file names are provided, they are now removed before formatting 72 | - Added LLF to the list of existing tools 73 | - Changed order of options in help dialogs 74 | 75 | # v0.4.7 76 | 77 | - Fix bug with `--stdin` adding newlines at EOF 78 | - Fix logic for ignoring verbatim environments 79 | - Ensure sectioning commands begin on new lines 80 | - Various performance improvements 81 | - Add NEWS.md for release notes 82 | - Ensure all test files successfully compile to PDFs 83 | - Better documentation of options in README.md 84 | 85 | # v0.4.6 86 | 87 | - Added `--wrap` flag to choose line length for wrapping 88 | - Significant changes to central formatting logic to reduce memory allocations 89 | - Treat comment environments as verbatim 90 | - Improved performance with finding comments in source code 91 | 92 | # v0.4.5 93 | 94 | - Added `--usetabs` to use tabs instead of spaces for indentation 95 | - Fixed a bug with unicode graphemes and comment handling 96 | - Main function now returns `std::process::ExitCode` for a cleaner exit 97 | - Reduced memory allocation in comment handling logic 98 | - Reduced memory allocation when indenting lines 99 | - Caching of pattern matches reduces number of regex searches 100 | 101 | # v0.4.4 102 | 103 | - Added `--tab` flag for variable tab size [default: 2] 104 | - Fixed bug with incorrect line numbers being printed 105 | - Fixed bug with quadratic complexity of comment checking 106 | - Added Arch User Repository support 107 | - Added VS Code support 108 | - Improved performance by moving environment checking inside main loop 109 | - Upgraded Cargo dependencies 110 | - Included LTO optimization on the release build 111 | 112 | # v0.4.3 113 | 114 | - Switch output text coloring to the `colored` crate. 115 | - Add `--stdin` flag to read input from stdin (and output to stdout). 116 | 117 | # v0.4.2 118 | 119 | - Added `--quiet` flag to suppress warning messages 120 | - Allow `tex-fmt main` for `tex-fmt main.tex` 121 | - Internal documentation 122 | - Improved performance 123 | - Added more Clippy lints 124 | 125 | # v0.4.1 126 | 127 | - Added binary archives to GitHub release 128 | 129 | # v0.4.0 130 | 131 | ## Breaking change 132 | The logic for line wrapping has been changed. Previously, for lines longer than 133 | 80 characters, we would break the line at suitable points into chunks of no 134 | more than 80 characters. Then another round of indenting was applied, and this 135 | would often push the length back over 80 characters. A subsequent round of 136 | wrapping was therefore required, and often led to the creation of very short 137 | lines (#6). 138 | 139 | The new approach is to take lines longer than 80 characters and remove the 140 | first segment up to 70 characters, pushing the resulting two lines back onto 141 | the queue. When indenting is then reapplied, the lines typically do not go over 142 | 80 characters unless the indentation is very deep. However, some lines may now 143 | be truncated to 70 characters rather than 80. 144 | 145 | ## Other updates 146 | 147 | - Added a `--keep` flag to disable line wrapping (#10) 148 | - Improved the central algorithm to avoid multiple passes and improve run-time 149 | performance (#7) 150 | - Only write the file to disk if the formatting returns a different string, to 151 | avoid unnecessary editing of modification times 152 | 153 | # v0.3.1 154 | 155 | - Updated README 156 | - Added project logo 157 | 158 | # v0.3.0 159 | 160 | - Added a `--check` flag to check if file is correctly formatted 161 | - Fixed bug with line wrapping giving up early 162 | - Shell scripts verified with shellcheck 163 | - Refactored variable names 164 | - Some performance improvements 165 | 166 | # v0.2.2 167 | 168 | Bump version number 169 | 170 | # v0.2.1 171 | 172 | Bump version number 173 | 174 | # v0.2.0 175 | 176 | Bump version number 177 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | use clap_complete::{generate_to, Shell}; 3 | use std::env::var_os; 4 | use std::fs::create_dir; 5 | use std::io::Error; 6 | use std::path::Path; 7 | 8 | include!("src/command.rs"); 9 | 10 | fn main() -> Result<(), Error> { 11 | println!("cargo::rerun-if-changed=src/"); 12 | println!("cargo::rerun-if-changed=build.rs"); 13 | println!("cargo::rerun-if-changed=Cargo.toml"); 14 | if std::env::var("CARGO_FEATURE_SHELLINSTALL").is_ok() { 15 | println!("cargo::warning=shellinstall"); 16 | build_completion()?; 17 | build_man()?; 18 | } 19 | Ok(()) 20 | } 21 | 22 | fn build_completion() -> Result<(), Error> { 23 | let outdir = match var_os("CARGO_MANIFEST_DIR") { 24 | None => return Ok(()), 25 | Some(outdir) => Path::new(&outdir).join("completion/"), 26 | }; 27 | 28 | if !outdir.exists() { 29 | create_dir(&outdir).unwrap(); 30 | } 31 | 32 | let mut command = get_cli_command(); 33 | for &shell in Shell::value_variants() { 34 | generate_to(shell, &mut command, "tex-fmt", &outdir)?; 35 | } 36 | Ok(()) 37 | } 38 | 39 | fn build_man() -> Result<(), Error> { 40 | let outdir = match var_os("CARGO_MANIFEST_DIR") { 41 | None => return Ok(()), 42 | Some(outdir) => Path::new(&outdir).join("man/"), 43 | }; 44 | 45 | if !outdir.exists() { 46 | create_dir(&outdir).unwrap(); 47 | } 48 | 49 | let command = get_cli_command(); 50 | let man = clap_mangen::Man::new(command); 51 | let mut buffer: Vec = Default::default(); 52 | man.render(&mut buffer)?; 53 | std::fs::write(outdir.join("tex-fmt.1"), buffer)?; 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /completion/README.md: -------------------------------------------------------------------------------- 1 | # Shell completion for tex-fmt 2 | 3 | Shell completion scripts can be generated at run-time using the 4 | `--completion ` flag, as detailed below. Completion scripts 5 | generated at compile-time are also available for download in 6 | [this directory]( 7 | https://github.com/WGUNDERWOOD/tex-fmt/tree/main/completion/), 8 | but they may not be up-to-date with your tex-fmt installation. 9 | 10 | For **bash**: 11 | 12 | ```shell 13 | dir="$XDG_CONFIG_HOME/bash_completion" 14 | mkdir -p "$dir" 15 | tex-fmt --completion bash > "$dir/tex-fmt.bash" 16 | ``` 17 | 18 | For **fish**: 19 | 20 | ```shell 21 | dir="$XDG_CONFIG_HOME/fish/completions" 22 | mkdir -p "$dir" 23 | tex-fmt --completion fish > "$dir/tex-fmt.fish" 24 | ``` 25 | 26 | For **zsh**: 27 | 28 | ```shell 29 | dir="$HOME/.zsh-complete" 30 | mkdir -p "$dir" 31 | tex-fmt --completion zsh > "$dir/_tex-fmt" 32 | ``` 33 | 34 | For **elvish**: 35 | 36 | ```shell 37 | dir="$HOME/.elvish/lib" 38 | mkdir -p "$dir" 39 | tex-fmt --completion elvish > "$dir/tex-fmt.elv" 40 | use tex-fmt 41 | ``` 42 | 43 | For **PowerShell**, create the completions: 44 | 45 | ```shell 46 | tex-fmt --completion powershell > _tex-fmt.ps1 47 | ``` 48 | 49 | Then add `. _tex-fmt.ps1` to your PowerShell profile. 50 | If the `_tex-fmt.ps1` file is not on your `PATH`, do 51 | `. /path/to/_tex-fmt.ps1` instead. 52 | -------------------------------------------------------------------------------- /completion/_tex-fmt: -------------------------------------------------------------------------------- 1 | #compdef tex-fmt 2 | 3 | autoload -U is-at-least 4 | 5 | _tex-fmt() { 6 | typeset -A opt_args 7 | typeset -a _arguments_options 8 | local ret=1 9 | 10 | if is-at-least 5.2; then 11 | _arguments_options=(-s -S -C) 12 | else 13 | _arguments_options=(-s -C) 14 | fi 15 | 16 | local context curcontext="$curcontext" state line 17 | _arguments "${_arguments_options[@]}" : \ 18 | '-l+[Line length for wrapping \[default\: 80\]]:N:_default' \ 19 | '--wraplen=[Line length for wrapping \[default\: 80\]]:N:_default' \ 20 | '-t+[Number of characters to use as tab size \[default\: 2\]]:N:_default' \ 21 | '--tabsize=[Number of characters to use as tab size \[default\: 2\]]:N:_default' \ 22 | '--config=[Path to config file]:PATH:_files' \ 23 | '--completion=[Generate shell completion script]:SHELL:(bash elvish fish powershell zsh)' \ 24 | '-c[Check formatting, do not modify files]' \ 25 | '--check[Check formatting, do not modify files]' \ 26 | '-p[Print to stdout, do not modify files]' \ 27 | '--print[Print to stdout, do not modify files]' \ 28 | '-f[Format files and return non-zero exit code if files are modified]' \ 29 | '--fail-on-change[Format files and return non-zero exit code if files are modified]' \ 30 | '-n[Do not wrap long lines]' \ 31 | '--nowrap[Do not wrap long lines]' \ 32 | '--usetabs[Use tabs instead of spaces for indentation]' \ 33 | '-s[Process stdin as a single file, output to stdout]' \ 34 | '--stdin[Process stdin as a single file, output to stdout]' \ 35 | '--noconfig[Do not read any config file]' \ 36 | '-v[Show info messages]' \ 37 | '--verbose[Show info messages]' \ 38 | '-q[Hide warning messages]' \ 39 | '--quiet[Hide warning messages]' \ 40 | '--trace[Show trace messages]' \ 41 | '--man[Generate man page]' \ 42 | '--args[Print arguments passed to tex-fmt and exit]' \ 43 | '-h[Print help]' \ 44 | '--help[Print help]' \ 45 | '-V[Print version]' \ 46 | '--version[Print version]' \ 47 | '::files -- List of files to be formatted:_default' \ 48 | && ret=0 49 | } 50 | 51 | (( $+functions[_tex-fmt_commands] )) || 52 | _tex-fmt_commands() { 53 | local commands; commands=() 54 | _describe -t commands 'tex-fmt commands' commands "$@" 55 | } 56 | 57 | if [ "$funcstack[1]" = "_tex-fmt" ]; then 58 | _tex-fmt "$@" 59 | else 60 | compdef _tex-fmt tex-fmt 61 | fi 62 | -------------------------------------------------------------------------------- /completion/_tex-fmt.ps1: -------------------------------------------------------------------------------- 1 | 2 | using namespace System.Management.Automation 3 | using namespace System.Management.Automation.Language 4 | 5 | Register-ArgumentCompleter -Native -CommandName 'tex-fmt' -ScriptBlock { 6 | param($wordToComplete, $commandAst, $cursorPosition) 7 | 8 | $commandElements = $commandAst.CommandElements 9 | $command = @( 10 | 'tex-fmt' 11 | for ($i = 1; $i -lt $commandElements.Count; $i++) { 12 | $element = $commandElements[$i] 13 | if ($element -isnot [StringConstantExpressionAst] -or 14 | $element.StringConstantType -ne [StringConstantType]::BareWord -or 15 | $element.Value.StartsWith('-') -or 16 | $element.Value -eq $wordToComplete) { 17 | break 18 | } 19 | $element.Value 20 | }) -join ';' 21 | 22 | $completions = @(switch ($command) { 23 | 'tex-fmt' { 24 | [CompletionResult]::new('-l', '-l', [CompletionResultType]::ParameterName, 'Line length for wrapping [default: 80]') 25 | [CompletionResult]::new('--wraplen', '--wraplen', [CompletionResultType]::ParameterName, 'Line length for wrapping [default: 80]') 26 | [CompletionResult]::new('-t', '-t', [CompletionResultType]::ParameterName, 'Number of characters to use as tab size [default: 2]') 27 | [CompletionResult]::new('--tabsize', '--tabsize', [CompletionResultType]::ParameterName, 'Number of characters to use as tab size [default: 2]') 28 | [CompletionResult]::new('--config', '--config', [CompletionResultType]::ParameterName, 'Path to config file') 29 | [CompletionResult]::new('--completion', '--completion', [CompletionResultType]::ParameterName, 'Generate shell completion script') 30 | [CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'Check formatting, do not modify files') 31 | [CompletionResult]::new('--check', '--check', [CompletionResultType]::ParameterName, 'Check formatting, do not modify files') 32 | [CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Print to stdout, do not modify files') 33 | [CompletionResult]::new('--print', '--print', [CompletionResultType]::ParameterName, 'Print to stdout, do not modify files') 34 | [CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'Format files and return non-zero exit code if files are modified') 35 | [CompletionResult]::new('--fail-on-change', '--fail-on-change', [CompletionResultType]::ParameterName, 'Format files and return non-zero exit code if files are modified') 36 | [CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'Do not wrap long lines') 37 | [CompletionResult]::new('--nowrap', '--nowrap', [CompletionResultType]::ParameterName, 'Do not wrap long lines') 38 | [CompletionResult]::new('--usetabs', '--usetabs', [CompletionResultType]::ParameterName, 'Use tabs instead of spaces for indentation') 39 | [CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'Process stdin as a single file, output to stdout') 40 | [CompletionResult]::new('--stdin', '--stdin', [CompletionResultType]::ParameterName, 'Process stdin as a single file, output to stdout') 41 | [CompletionResult]::new('--noconfig', '--noconfig', [CompletionResultType]::ParameterName, 'Do not read any config file') 42 | [CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Show info messages') 43 | [CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Show info messages') 44 | [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Hide warning messages') 45 | [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Hide warning messages') 46 | [CompletionResult]::new('--trace', '--trace', [CompletionResultType]::ParameterName, 'Show trace messages') 47 | [CompletionResult]::new('--man', '--man', [CompletionResultType]::ParameterName, 'Generate man page') 48 | [CompletionResult]::new('--args', '--args', [CompletionResultType]::ParameterName, 'Print arguments passed to tex-fmt and exit') 49 | [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') 50 | [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') 51 | [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') 52 | [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') 53 | break 54 | } 55 | }) 56 | 57 | $completions.Where{ $_.CompletionText -like "$wordToComplete*" } | 58 | Sort-Object -Property ListItemText 59 | } 60 | -------------------------------------------------------------------------------- /completion/tex-fmt.bash: -------------------------------------------------------------------------------- 1 | _tex-fmt() { 2 | local i cur prev opts cmd 3 | COMPREPLY=() 4 | cur="${COMP_WORDS[COMP_CWORD]}" 5 | prev="${COMP_WORDS[COMP_CWORD-1]}" 6 | cmd="" 7 | opts="" 8 | 9 | for i in ${COMP_WORDS[@]} 10 | do 11 | case "${cmd},${i}" in 12 | ",$1") 13 | cmd="tex__fmt" 14 | ;; 15 | *) 16 | ;; 17 | esac 18 | done 19 | 20 | case "${cmd}" in 21 | tex__fmt) 22 | opts="-c -p -f -n -l -t -s -v -q -h -V --check --print --fail-on-change --nowrap --wraplen --tabsize --usetabs --stdin --config --noconfig --verbose --quiet --trace --completion --man --args --help --version [files]..." 23 | if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then 24 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 25 | return 0 26 | fi 27 | case "${prev}" in 28 | --wraplen) 29 | COMPREPLY=($(compgen -f "${cur}")) 30 | return 0 31 | ;; 32 | -l) 33 | COMPREPLY=($(compgen -f "${cur}")) 34 | return 0 35 | ;; 36 | --tabsize) 37 | COMPREPLY=($(compgen -f "${cur}")) 38 | return 0 39 | ;; 40 | -t) 41 | COMPREPLY=($(compgen -f "${cur}")) 42 | return 0 43 | ;; 44 | --config) 45 | COMPREPLY=($(compgen -f "${cur}")) 46 | return 0 47 | ;; 48 | --completion) 49 | COMPREPLY=($(compgen -W "bash elvish fish powershell zsh" -- "${cur}")) 50 | return 0 51 | ;; 52 | *) 53 | COMPREPLY=() 54 | ;; 55 | esac 56 | COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 57 | return 0 58 | ;; 59 | esac 60 | } 61 | 62 | if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then 63 | complete -F _tex-fmt -o nosort -o bashdefault -o default tex-fmt 64 | else 65 | complete -F _tex-fmt -o bashdefault -o default tex-fmt 66 | fi 67 | -------------------------------------------------------------------------------- /completion/tex-fmt.elv: -------------------------------------------------------------------------------- 1 | 2 | use builtin; 3 | use str; 4 | 5 | set edit:completion:arg-completer[tex-fmt] = {|@words| 6 | fn spaces {|n| 7 | builtin:repeat $n ' ' | str:join '' 8 | } 9 | fn cand {|text desc| 10 | edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc 11 | } 12 | var command = 'tex-fmt' 13 | for word $words[1..-1] { 14 | if (str:has-prefix $word '-') { 15 | break 16 | } 17 | set command = $command';'$word 18 | } 19 | var completions = [ 20 | &'tex-fmt'= { 21 | cand -l 'Line length for wrapping [default: 80]' 22 | cand --wraplen 'Line length for wrapping [default: 80]' 23 | cand -t 'Number of characters to use as tab size [default: 2]' 24 | cand --tabsize 'Number of characters to use as tab size [default: 2]' 25 | cand --config 'Path to config file' 26 | cand --completion 'Generate shell completion script' 27 | cand -c 'Check formatting, do not modify files' 28 | cand --check 'Check formatting, do not modify files' 29 | cand -p 'Print to stdout, do not modify files' 30 | cand --print 'Print to stdout, do not modify files' 31 | cand -f 'Format files and return non-zero exit code if files are modified' 32 | cand --fail-on-change 'Format files and return non-zero exit code if files are modified' 33 | cand -n 'Do not wrap long lines' 34 | cand --nowrap 'Do not wrap long lines' 35 | cand --usetabs 'Use tabs instead of spaces for indentation' 36 | cand -s 'Process stdin as a single file, output to stdout' 37 | cand --stdin 'Process stdin as a single file, output to stdout' 38 | cand --noconfig 'Do not read any config file' 39 | cand -v 'Show info messages' 40 | cand --verbose 'Show info messages' 41 | cand -q 'Hide warning messages' 42 | cand --quiet 'Hide warning messages' 43 | cand --trace 'Show trace messages' 44 | cand --man 'Generate man page' 45 | cand --args 'Print arguments passed to tex-fmt and exit' 46 | cand -h 'Print help' 47 | cand --help 'Print help' 48 | cand -V 'Print version' 49 | cand --version 'Print version' 50 | } 51 | ] 52 | $completions[$command] 53 | } 54 | -------------------------------------------------------------------------------- /completion/tex-fmt.fish: -------------------------------------------------------------------------------- 1 | complete -c tex-fmt -s l -l wraplen -d 'Line length for wrapping [default: 80]' -r 2 | complete -c tex-fmt -s t -l tabsize -d 'Number of characters to use as tab size [default: 2]' -r 3 | complete -c tex-fmt -l config -d 'Path to config file' -r -F 4 | complete -c tex-fmt -l completion -d 'Generate shell completion script' -r -f -a "bash\t'' 5 | elvish\t'' 6 | fish\t'' 7 | powershell\t'' 8 | zsh\t''" 9 | complete -c tex-fmt -s c -l check -d 'Check formatting, do not modify files' 10 | complete -c tex-fmt -s p -l print -d 'Print to stdout, do not modify files' 11 | complete -c tex-fmt -s f -l fail-on-change -d 'Format files and return non-zero exit code if files are modified' 12 | complete -c tex-fmt -s n -l nowrap -d 'Do not wrap long lines' 13 | complete -c tex-fmt -l usetabs -d 'Use tabs instead of spaces for indentation' 14 | complete -c tex-fmt -s s -l stdin -d 'Process stdin as a single file, output to stdout' 15 | complete -c tex-fmt -l noconfig -d 'Do not read any config file' 16 | complete -c tex-fmt -s v -l verbose -d 'Show info messages' 17 | complete -c tex-fmt -s q -l quiet -d 'Hide warning messages' 18 | complete -c tex-fmt -l trace -d 'Show trace messages' 19 | complete -c tex-fmt -l man -d 'Generate man page' 20 | complete -c tex-fmt -l args -d 'Print arguments passed to tex-fmt and exit' 21 | complete -c tex-fmt -s h -l help -d 'Print help' 22 | complete -c tex-fmt -s V -l version -d 'Print version' 23 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | {pkgs ? import {}}: let 2 | manifest = (pkgs.lib.importTOML ./Cargo.toml).package; 3 | in 4 | pkgs.rustPlatform.buildRustPackage rec { 5 | pname = manifest.name; 6 | version = manifest.version; 7 | cargoLock.lockFile = ./Cargo.lock; 8 | src = pkgs.lib.cleanSource ./.; 9 | } 10 | -------------------------------------------------------------------------------- /extra/binary.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "Testing binary" 3 | 4 | # tempdir for unmodified test files 5 | DIR_ORIG="$(mktemp -d)" 6 | cp -r ../tests/* "$DIR_ORIG" 7 | 8 | # tempdir for formatted test files 9 | DIR_TEST="$(mktemp -d)" 10 | cp -r ../tests/* "$DIR_TEST" 11 | 12 | # build binary 13 | cargo build --release 14 | BIN=$(realpath "../target/release/tex-fmt") 15 | 16 | # run tex-fmt in DIR_TEST 17 | for TESTDIR in "$DIR_TEST"/*; do 18 | FLAGS="-q" 19 | if [ -f "$TESTDIR/tex-fmt.toml" ]; then 20 | FLAGS="$FLAGS --config $TESTDIR/tex-fmt.toml" 21 | else 22 | FLAGS="$FLAGS --noconfig" 23 | fi 24 | if [ -f "$TESTDIR/cli.txt" ]; then 25 | FLAGS+=" $(paste -sd' ' "$TESTDIR/cli.txt")" 26 | fi 27 | (cd "$TESTDIR" && eval "$BIN $FLAGS" "$TESTDIR/source"/*) 28 | (cd "$TESTDIR" && eval "$BIN $FLAGS" "$TESTDIR/target"/*) 29 | done 30 | 31 | # check tex-fmt agrees with target files 32 | for TESTDIR in "$DIR_TEST"/*; do 33 | DIRNAME=$(basename "$TESTDIR") 34 | for file in "$TESTDIR/source"/*; do 35 | f=$(basename "$file") 36 | diff "$DIR_ORIG/$DIRNAME/target/$f" "$TESTDIR/target/$f" | diff-so-fancy 37 | diff "$DIR_ORIG/$DIRNAME/target/$f" "$TESTDIR/source/$f" | diff-so-fancy 38 | done 39 | done 40 | 41 | # if both config and cli exist, run tex-fmt again in DIR_TEST 42 | for TESTDIR in "$DIR_TEST"/*; do 43 | DIRNAME=$(basename "$TESTDIR") 44 | if [ -f "$TESTDIR/tex-fmt.toml" ] && [ -f "$TESTDIR/cli.txt" ]; then 45 | FLAGS="-q --noconfig" 46 | FLAGS+=" $(paste -sd' ' "$TESTDIR/cli.txt")" 47 | cp -r "$DIR_ORIG/$DIRNAME"/* "$TESTDIR" 48 | (cd "$TESTDIR" && eval "$BIN $FLAGS" "$TESTDIR/source"/*) 49 | (cd "$TESTDIR" && eval "$BIN $FLAGS" "$TESTDIR/target"/*) 50 | fi 51 | done 52 | 53 | # check tex-fmt agrees with target files 54 | for TESTDIR in "$DIR_TEST"/*; do 55 | DIRNAME=$(basename "$TESTDIR") 56 | for file in "$TESTDIR/source"/*; do 57 | f=$(basename "$file") 58 | diff "$DIR_ORIG/$DIRNAME/target/$f" "$TESTDIR/target/$f" | diff-so-fancy 59 | diff "$DIR_ORIG/$DIRNAME/target/$f" "$TESTDIR/source/$f" | diff-so-fancy 60 | done 61 | done 62 | -------------------------------------------------------------------------------- /extra/card.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | import matplotlib.pyplot as plt 3 | import matplotlib.font_manager as fm 4 | 5 | # start plot 6 | (fig, ax) = plt.subplots(figsize=(10, 5)) 7 | plt.xticks([]) 8 | plt.yticks([]) 9 | for side in ["bottom", "top", "left", "right"]: 10 | ax.spines[side].set_color("#FFFFFF00") 11 | 12 | # colors 13 | col_dark = "#191924" 14 | col_yellow = "#eed858" 15 | col_light = "#faf7e5" 16 | 17 | outer_col = col_yellow 18 | inner_col = col_light 19 | text_col = col_dark 20 | 21 | # outer box 22 | w = 200 23 | h = 100 24 | xs_outer = [w/2, w, w, 0, 0, w/2] 25 | ys_outer = [0, 0, h, h, 0, 0] 26 | plt.fill(xs_outer, ys_outer, c=outer_col, lw=1, zorder=1) 27 | 28 | # inner box 29 | dw = 23 30 | dh = 20 31 | xs_inner = [w/2, w-dw, w-dw, dw, dw, w/2] 32 | ys_inner = [dh, dh, h-dh, h-dh, dh, dh] 33 | plt.plot(xs_inner, ys_inner, c=inner_col, lw=30, zorder=2) 34 | plt.fill(xs_inner, ys_inner, c=inner_col, lw=0) 35 | 36 | # logo 37 | img = Image.open("logo.png").resize((900, 900)) 38 | fig.figimage(img, 2210, 540) 39 | 40 | # text 41 | fontfamily = "Roboto Slab" 42 | fonts = fm.findSystemFonts(fontpaths=None, fontext='ttf') 43 | [fm.fontManager.addfont(f) for f in fonts if fontfamily.split()[0] in f] 44 | 45 | fontsize = 16 46 | plt.text(31, 50, "An extremely fast La\nformatter written in Rust.", fontsize=fontsize, ha="left", va="center", fontweight="light", c=text_col, fontfamily=fontfamily, fontstyle="normal") 47 | 48 | plt.text(92.6, 53.53, "T", fontsize=fontsize, ha="left", va="center", fontweight="light", c=text_col, fontfamily=fontfamily, fontstyle="normal") 49 | 50 | plt.text(96.55, 53.53, "eX", fontsize=fontsize, ha="left", va="center", fontweight="light", c=text_col, fontfamily=fontfamily, fontstyle="normal") 51 | 52 | # save 53 | plt.savefig("card.svg", dpi=400, transparent=True) 54 | plt.close("all") 55 | -------------------------------------------------------------------------------- /extra/latex.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "Checking latex PDFs agree" 3 | DIR="$(mktemp -d)" 4 | cp -r ../tests/* "$DIR" 5 | echo "$DIR" 6 | cd "$DIR" || exit 7 | 8 | # empty file cannot be compiled 9 | rm -r ./empty/ 10 | 11 | echo 12 | 13 | for TESTDIR in "$DIR"/*; do 14 | for file in "$TESTDIR/source"/*.tex; do 15 | f=$(basename "$file" .tex) 16 | echo "Running latex for $f.tex" 17 | (cd "$TESTDIR/source" && latexmk -pdflua "$f.tex") 18 | (cd "$TESTDIR/target" && latexmk -pdflua "$f.tex") 19 | (cd "$TESTDIR/source" && pdftotext -q "$f.pdf") 20 | (cd "$TESTDIR/target" && pdftotext -q "$f.pdf") 21 | done 22 | done 23 | 24 | echo 25 | 26 | for TESTDIR in "$DIR"/*; do 27 | for file in "$TESTDIR/source"/*.tex; do 28 | f=$(basename "$file" .tex) 29 | echo "Checking PDF for $f.tex" 30 | diff -u "$TESTDIR/source/$f.txt" "$TESTDIR/target/$f.txt" | diff-so-fancy 31 | done 32 | done 33 | 34 | echo "$DIR" 35 | -------------------------------------------------------------------------------- /extra/logo.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib.font_manager as fm 3 | 4 | # start plot 5 | (fig, ax) = plt.subplots(figsize=(5, 5)) 6 | plt.xticks([]) 7 | plt.yticks([]) 8 | for side in ["bottom", "top", "left", "right"]: 9 | ax.spines[side].set_color("#FFFFFF00") 10 | 11 | # colors 12 | col_dark = "#191924" 13 | col_orange = "#e5652e" 14 | col_yellow = "#eed858" 15 | col_light = "#faf7e5" 16 | 17 | outer_col = col_orange 18 | inner_col = col_dark 19 | text_col = col_light 20 | line_col = col_yellow 21 | 22 | # outer box 23 | lw = 24 24 | xs_outer = [0.5, 1, 1, 0, 0, 0.5] 25 | ys_outer = [0, 0, 1, 1, 0, 0] 26 | plt.plot(xs_outer, ys_outer, c=outer_col, lw=lw, zorder=0) 27 | plt.fill(xs_outer, ys_outer, c=outer_col, lw=0) 28 | 29 | # inner box 30 | eps = 0.05 31 | xs_inner = [0.5, 1-eps, 1-eps, eps, eps, 0.5] 32 | ys_inner = [eps, eps, 1-eps, 1-eps, eps, eps] 33 | plt.plot(xs_inner, ys_inner, c=inner_col, lw=0.6*lw, zorder=2) 34 | plt.fill(xs_inner, ys_inner, c=inner_col, lw=0) 35 | 36 | # line 37 | eps = 0.125 38 | plt.plot([0.5, eps, 1-eps, 0.5], [0.485] * 4, 39 | lw=5, c=col_yellow) 40 | 41 | # text 42 | fontfamily = "Bungee" 43 | fonts = fm.findSystemFonts(fontpaths=None, fontext='ttf') 44 | [fm.fontManager.addfont(f) for f in fonts if fontfamily.split()[0] in f] 45 | 46 | fontsize = 100 47 | plt.text(0.5, 0.72, "TEX", fontsize=fontsize, ha="center", va="center", fontweight="light", c=text_col, fontfamily=fontfamily, fontstyle="normal") 48 | 49 | fontsize = 96 50 | plt.text(0.496, 0.25, "FMT", fontsize=fontsize, ha="center", va="center", fontweight="light", c=text_col, fontfamily=fontfamily, fontstyle="normal") 51 | 52 | # save 53 | plt.savefig("logo.svg", dpi=1000, transparent=True) 54 | plt.close("all") 55 | -------------------------------------------------------------------------------- /extra/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 1980-01-01T00:00:00+00:00 10 | image/svg+xml 11 | 12 | 13 | Matplotlib v3.8.4, https://matplotlib.org/ 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 31 | 32 | 33 | 34 | 41 | 42 | 43 | 50 | 51 | 52 | 59 | 60 | 61 | 68 | 69 | 70 | 71 | 72 | 79 | 80 | 81 | 86 | 87 | 88 | 91 | 92 | 93 | 96 | 97 | 98 | 101 | 102 | 103 | 106 | 107 | 108 | 109 | 110 | 111 | 134 | 165 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 267 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | -------------------------------------------------------------------------------- /extra/perf.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "Getting performance metrics" 3 | DIR="$(mktemp -d)" 4 | cp -r ../tests/* "$DIR" 5 | cargo build --release 6 | 7 | calc(){ awk "BEGIN { print ""$*"" }"; } 8 | 9 | echo 10 | echo -n "Test files: $(find "$DIR"/*/source/* "$DIR"/*/target/* | wc -l) files," 11 | echo -n " $(wc -l --total=only "$DIR"/*/source/* "$DIR"/*/target/*) lines, " 12 | du -hs "$DIR" | cut -f 1 13 | echo 14 | 15 | # tex-fmt 16 | TEXFMTFILE="hyperfine-tex-fmt.csv" 17 | hyperfine --warmup 10 \ 18 | --min-runs 20 \ 19 | --export-csv $TEXFMTFILE \ 20 | --command-name "tex-fmt" \ 21 | --prepare "cp -r ../tests/* $DIR" \ 22 | "../target/release/tex-fmt $DIR/*/source/* $DIR/*/target/*" 23 | 24 | # latexindent 25 | LATEXINDENTFILE="hyperfine-latexindent.csv" 26 | hyperfine --warmup 0 \ 27 | --export-csv $LATEXINDENTFILE \ 28 | --runs 1 \ 29 | --command-name "latexindent" \ 30 | --prepare "cp -r ../tests/* $DIR" \ 31 | "latexindent $DIR/*/source/* $DIR/*/target/*" 32 | 33 | # latexindent -m 34 | LATEXINDENTMFILE="hyperfine-latexindent-m.csv" 35 | hyperfine --warmup 0 \ 36 | --export-csv $LATEXINDENTMFILE \ 37 | --runs 1 \ 38 | --command-name "latexindent -m" \ 39 | --prepare "cp -r ../tests/* $DIR" \ 40 | "latexindent -m $DIR/*/source/* $DIR/*/target/*" 41 | 42 | # print results 43 | TEXFMT=$(cat $TEXFMTFILE | tail -n 1 | cut -d "," -f 2) 44 | echo "tex-fmt: ${TEXFMT}s" 45 | 46 | LATEXINDENT=$(cat $LATEXINDENTFILE | tail -n 1 | cut -d "," -f 2) 47 | LATEXINDENTTIMES=$(calc "$LATEXINDENT"/"$TEXFMT") 48 | echo "latexindent: ${LATEXINDENT}s, x$LATEXINDENTTIMES" 49 | 50 | LATEXINDENTM=$(cat $LATEXINDENTMFILE | tail -n 1 | cut -d "," -f 2) 51 | LATEXINDENTMTIMES=$(calc "$LATEXINDENTM"/"$TEXFMT") 52 | echo "latexindent -m: ${LATEXINDENTM}s, x$LATEXINDENTMTIMES" 53 | -------------------------------------------------------------------------------- /extra/prof.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "Making flamegraph profile" 3 | DIR="$(mktemp -d)" 4 | cp -r ../tests/* "$DIR" 5 | CARGO_PROFILE_RELEASE_DEBUG=true cargo build --release 6 | BIN="../target/release/tex-fmt" 7 | 8 | mv "$DIR"/*/source/* "$DIR" 9 | rm "$DIR"/*/target/* 10 | find "$DIR" -name "*.toml" -delete 11 | find "$DIR" -name "*.txt" -delete 12 | find "$DIR"/* -empty -type d -delete 13 | find "$DIR" -empty -type d -delete 14 | 15 | echo -n "Test files: $(find "$DIR" | wc -l) files, " 16 | echo -n "$(wc -l --total=only "$DIR"/*) lines, " 17 | du -hs "$DIR" | cut -f 1 18 | echo 19 | 20 | flamegraph -F 10000 -- "$BIN" "$DIR"/* 21 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1746206129, 24 | "narHash": "sha256-JA4DynBKhY7t4DdJZTuomRLAiXFDUgCGGwxgt+XGiik=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "9a7caecf30a0494c88b7daeeed29244cd9a52e7d", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixpkgs-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "LaTeX formatter written in Rust"; 3 | inputs = { 4 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | }; 7 | outputs = { 8 | self, 9 | nixpkgs, 10 | flake-utils, 11 | }: 12 | flake-utils.lib.eachDefaultSystem ( 13 | system: let 14 | pkgs = import nixpkgs {inherit system;}; 15 | in { 16 | packages = { 17 | default = pkgs.callPackage ./default.nix {inherit pkgs;}; 18 | }; 19 | devShells = { 20 | default = pkgs.callPackage ./shell.nix {inherit pkgs;}; 21 | }; 22 | } 23 | ) 24 | // { 25 | overlays.default = import ./overlay.nix; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | default: test doc clippy format shellcheck shellinstall wasm 2 | 3 | all: default prof binary logo perf latex 4 | 5 | alias b := build 6 | alias d := doc 7 | alias t := test 8 | alias l := latex 9 | alias c := clippy 10 | alias f := format 11 | 12 | build: 13 | @cargo build -r 14 | 15 | test: 16 | @cargo test --no-fail-fast 17 | 18 | doc: 19 | @cargo doc 20 | 21 | shellinstall: 22 | @cargo build -r --features shellinstall 23 | 24 | testignored: 25 | @cargo test -- --ignored 26 | 27 | clippy: 28 | @cargo clippy -r && cargo shear 29 | 30 | format: 31 | @cargo fmt 32 | @alejandra -q . 33 | 34 | latex: 35 | @cd extra && bash latex.sh 36 | 37 | wasm: 38 | @mkdir -p web/pkg 39 | @cargo build -r --lib --target wasm32-unknown-unknown 40 | @wasm-bindgen --target web --out-dir web/pkg \ 41 | target/wasm32-unknown-unknown/release/tex_fmt.wasm 42 | @cd web/pkg && wasm-opt -Oz -o tex_fmt_bg.wasm tex_fmt_bg.wasm 43 | 44 | perf: 45 | @cd extra && bash perf.sh 46 | 47 | prof: 48 | @cd extra && bash prof.sh 49 | 50 | binary: 51 | @cd extra && bash binary.sh 52 | 53 | upgrade: 54 | @cargo upgrade && cargo update 55 | 56 | shellcheck: 57 | @shellcheck extra/*.sh 58 | 59 | nix: 60 | @nix flake update 61 | 62 | todo: 63 | @rg -g '!justfile' todo 64 | 65 | logo: 66 | @cd extra && python logo.py 67 | @cd extra && magick -background none logo.svg -resize 5000x5000 logo.png 68 | @cd extra && python card.py 69 | @cd extra && magick -background none card.svg -resize 1280x640\! card.png 70 | @cd extra && inkscape -w 2560 -h 1280 card.svg -o card.png 71 | @cd extra && rm -f logo.png card.svg 72 | -------------------------------------------------------------------------------- /man/README.md: -------------------------------------------------------------------------------- 1 | # Man page generation for tex-fmt 2 | 3 | A man page can be generated at run-time using the 4 | `--man` flag, as follows. 5 | 6 | ```shell 7 | mkdir -p man/man1 8 | tex-fmt --man > man/man1/tex-fmt.1 9 | MANPATH="$PWD/man" man tex-fmt 10 | ``` 11 | 12 | It is also available for download in 13 | [this directory]( 14 | https://github.com/WGUNDERWOOD/tex-fmt/tree/main/man/), 15 | but may not be up-to-date with your tex-fmt installation. 16 | -------------------------------------------------------------------------------- /man/tex-fmt.1: -------------------------------------------------------------------------------- 1 | .ie \n(.g .ds Aq \(aq 2 | .el .ds Aq ' 3 | .TH tex-fmt 1 "tex-fmt 0.5.4" 4 | .SH NAME 5 | tex\-fmt \- LaTeX formatter written in Rust 6 | .SH SYNOPSIS 7 | \fBtex\-fmt\fR [\fB\-c\fR|\fB\-\-check\fR] [\fB\-p\fR|\fB\-\-print\fR] [\fB\-f\fR|\fB\-\-fail\-on\-change\fR] [\fB\-n\fR|\fB\-\-nowrap\fR] [\fB\-l\fR|\fB\-\-wraplen\fR] [\fB\-t\fR|\fB\-\-tabsize\fR] [\fB\-\-usetabs\fR] [\fB\-s\fR|\fB\-\-stdin\fR] [\fB\-\-config\fR] [\fB\-\-noconfig\fR] [\fB\-v\fR|\fB\-\-verbose\fR] [\fB\-q\fR|\fB\-\-quiet\fR] [\fB\-\-trace\fR] [\fB\-\-completion\fR] [\fB\-\-man\fR] [\fB\-\-args\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIfiles\fR] 8 | .SH DESCRIPTION 9 | LaTeX formatter written in Rust 10 | .SH OPTIONS 11 | .TP 12 | \fB\-c\fR, \fB\-\-check\fR 13 | Check formatting, do not modify files 14 | .TP 15 | \fB\-p\fR, \fB\-\-print\fR 16 | Print to stdout, do not modify files 17 | .TP 18 | \fB\-f\fR, \fB\-\-fail\-on\-change\fR 19 | Format files and return non\-zero exit code if files are modified 20 | .TP 21 | \fB\-n\fR, \fB\-\-nowrap\fR 22 | Do not wrap long lines 23 | .TP 24 | \fB\-l\fR, \fB\-\-wraplen\fR=\fIN\fR 25 | Line length for wrapping [default: 80] 26 | .TP 27 | \fB\-t\fR, \fB\-\-tabsize\fR=\fIN\fR 28 | Number of characters to use as tab size [default: 2] 29 | .TP 30 | \fB\-\-usetabs\fR 31 | Use tabs instead of spaces for indentation 32 | .TP 33 | \fB\-s\fR, \fB\-\-stdin\fR 34 | Process stdin as a single file, output to stdout 35 | .TP 36 | \fB\-\-config\fR=\fIPATH\fR 37 | Path to config file 38 | .TP 39 | \fB\-\-noconfig\fR 40 | Do not read any config file 41 | .TP 42 | \fB\-v\fR, \fB\-\-verbose\fR 43 | Show info messages 44 | .TP 45 | \fB\-q\fR, \fB\-\-quiet\fR 46 | Hide warning messages 47 | .TP 48 | \fB\-\-trace\fR 49 | Show trace messages 50 | .TP 51 | \fB\-\-completion\fR=\fISHELL\fR 52 | Generate shell completion script 53 | .br 54 | 55 | .br 56 | [\fIpossible values: \fRbash, elvish, fish, powershell, zsh] 57 | .TP 58 | \fB\-\-man\fR 59 | Generate man page 60 | .TP 61 | \fB\-\-args\fR 62 | Print arguments passed to tex\-fmt and exit 63 | .TP 64 | \fB\-h\fR, \fB\-\-help\fR 65 | Print help 66 | .TP 67 | \fB\-V\fR, \fB\-\-version\fR 68 | Print version 69 | .TP 70 | [\fIfiles\fR] 71 | List of files to be formatted 72 | .SH VERSION 73 | v0.5.4 74 | .SH AUTHORS 75 | William George Underwood, wg.underwood13@gmail.com 76 | -------------------------------------------------------------------------------- /notes.org: -------------------------------------------------------------------------------- 1 | #+title: tex-fmt 2 | * Tasks 3 | * Release process 4 | ** Update release notes in NEWS.md 5 | *** git log --oneline --no-merges vX.X.X..main 6 | ** Update version number in Cargo.toml 7 | ** Update version number for pre-commit in README 8 | ** Update Nix flake and lock 9 | *** just nix 10 | *** nix develop 11 | ** Update Rust version 12 | *** just upgrade 13 | ** Run tests 14 | *** just 15 | *** just perf 16 | *** Update performance results in README.md 17 | ** Push to GitHub and check action tests pass 18 | ** Create a git tag 19 | *** git tag vY.Y.Y 20 | *** git push --tags 21 | *** Can delete remote tags with git push --delete origin vY.Y.Y 22 | ** Publish to crates.io with cargo publish 23 | *** Pass --allow-dirty if notes.org has changed 24 | ** Publish GitHub release with notes from NEWS.md 25 | *** No need to add a title 26 | *** GitHub binaries published automatically with actions 27 | ** Publish in nixpkgs 28 | *** Go to nixpkgs fork directory 29 | *** git checkout master 30 | *** git fetch upstream 31 | *** git rebase upstream/master 32 | *** git fetch 33 | *** git push --force-with-lease origin master 34 | *** git branch -d update-tex-fmt 35 | *** git switch --create update-tex-fmt upstream/master 36 | *** nvim pkgs/by-name/te/tex-fmt/package.nix 37 | *** Update version and invalidate src.hash and cargoHash 38 | *** nix-build -A tex-fmt 39 | *** Fix both hashes, get a successful build 40 | *** git add pkgs/by-name/te/tex-fmt/package.nix 41 | *** git commit -m "tex-fmt: X.X.X -> Y.Y.Y" 42 | *** git push --set-upstream origin HEAD 43 | *** Go to GitHub and create a pull request 44 | *** Submit pull request and check relevant boxes 45 | ** Tidy repository 46 | *** Commit any new changes to NEWS.md or notes.org 47 | * CLI and config structure 48 | *** args.rs 49 | **** Core argument definitions 50 | **** Args struct defines arguments used internally by tex-fmt 51 | **** OptionArgs struct defines an intermediate target 52 | ***** CLI arguments are read into OptionArgs in cli.rs 53 | ***** Config file arguments are read into OptionArgs in config.rs 54 | ***** Default values for OptionArgs are defined here 55 | **** These OptionArgs are merged together 56 | **** Then converted into Args 57 | **** Conflicting arguments are resolved 58 | **** The Display trait is implemented for args 59 | *** command.rs 60 | **** Contains the clap command definition 61 | **** Sets options exposed to the user on the CLI 62 | *** cli.rs 63 | **** Logic for reading from CLI 64 | **** Clap uses command.rs to read from CLI 65 | **** This file then parses from clap into OptionArgs 66 | *** config.rs 67 | **** Logic for reading from config file 68 | **** Determines the config file path by looking in several places 69 | **** Reads from this path and parses to a toml Table 70 | **** Values are then assigned to an OptionArgs struct 71 | * Process for adding new arguments 72 | ** General 73 | *** args.rs 74 | **** Update Args struct if core argument 75 | **** Update OptionArgs struct 76 | **** Update Args resolve() if extra logic necessary 77 | **** Update Args fmt::Display if core argument 78 | ** CLI arguments 79 | *** command.rs 80 | **** Update clap command definition 81 | *** cli.rs 82 | **** Update get_cli_args() and add extra logic if needed 83 | ** Config arguments 84 | *** config.rs 85 | **** Update get_config_args() 86 | ** Fix compiler warnings 87 | ** Implement behaviour 88 | ** Add tests 89 | ** Update README 90 | *** CLI options 91 | *** Config options 92 | *** Usage section if commonly used option 93 | -------------------------------------------------------------------------------- /overlay.nix: -------------------------------------------------------------------------------- 1 | _: prev: { 2 | tex-fmt = prev.callPackage ./default.nix {}; 3 | } 4 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | {pkgs ? import {}}: 2 | pkgs.mkShell { 3 | inputsFrom = [(pkgs.callPackage ./default.nix {})]; 4 | buildInputs = let 5 | python = pkgs.python3.withPackages (ps: 6 | with ps; [ 7 | grip 8 | matplotlib 9 | pillow 10 | ]); 11 | in [ 12 | pkgs.alejandra 13 | pkgs.bacon 14 | pkgs.binaryen 15 | pkgs.cacert 16 | pkgs.cargo-edit 17 | pkgs.cargo-flamegraph 18 | pkgs.cargo-shear 19 | pkgs.clippy 20 | pkgs.diff-so-fancy 21 | pkgs.gh 22 | pkgs.hyperfine 23 | pkgs.lld 24 | pkgs.poppler_utils 25 | pkgs.ripgrep 26 | pkgs.rustfmt 27 | pkgs.shellcheck 28 | pkgs.texlive.combined.scheme-full 29 | pkgs.wasm-bindgen-cli 30 | python 31 | ]; 32 | } 33 | -------------------------------------------------------------------------------- /src/bin.rs: -------------------------------------------------------------------------------- 1 | //! tex-fmt 2 | //! An extremely fast LaTeX formatter written in Rust 3 | 4 | #![warn(missing_docs)] 5 | #![warn(clippy::nursery)] 6 | #![warn(clippy::cargo)] 7 | #![warn(clippy::missing_docs_in_private_items)] 8 | #![warn(clippy::pedantic)] 9 | #![allow(clippy::multiple_crate_versions)] 10 | 11 | use std::process::ExitCode; 12 | use tex_fmt::args::get_args; 13 | use tex_fmt::format::run; 14 | use tex_fmt::logging::{init_logger, print_logs, Log}; 15 | 16 | fn main() -> ExitCode { 17 | let mut args = get_args(); 18 | init_logger(args.verbosity); 19 | 20 | let mut logs = Vec::::new(); 21 | let mut exit_code = args.resolve(&mut logs); 22 | 23 | if exit_code == 0 { 24 | exit_code = run(&args, &mut logs); 25 | } 26 | 27 | print_logs(&mut logs); 28 | ExitCode::from(exit_code) 29 | } 30 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | //! Functionality to parse CLI arguments 2 | 3 | use crate::args::{OptionArgs, TabChar}; 4 | use clap::ArgMatches; 5 | use clap_complete::{generate, Shell}; 6 | use clap_mangen::Man; 7 | use log::LevelFilter; 8 | use std::io; 9 | 10 | // Get the clap CLI command from a separate file 11 | include!("command.rs"); 12 | 13 | /// Read `ArgMatches` flag into `Option` 14 | fn get_flag(arg_matches: &ArgMatches, flag: &str) -> Option { 15 | if arg_matches.get_flag(flag) { 16 | Some(true) 17 | } else { 18 | None 19 | } 20 | } 21 | 22 | /// Parse CLI arguments into `OptionArgs` struct 23 | /// 24 | /// # Panics 25 | /// 26 | /// This function panics if the man page cannot be written. 27 | pub fn get_cli_args(matches: Option) -> OptionArgs { 28 | let mut command = get_cli_command(); 29 | let arg_matches = match matches { 30 | Some(m) => m, 31 | None => command.clone().get_matches(), 32 | }; 33 | 34 | // Generate completions and exit 35 | if let Some(shell) = arg_matches.get_one::("completion") { 36 | generate(*shell, &mut command, "tex-fmt", &mut io::stdout()); 37 | std::process::exit(0); 38 | } 39 | 40 | // Generate man page and exit 41 | if arg_matches.get_flag("man") { 42 | let man = Man::new(command); 43 | man.render(&mut io::stdout()).unwrap(); 44 | std::process::exit(0); 45 | } 46 | 47 | let wrap: Option = if arg_matches.get_flag("nowrap") { 48 | Some(false) 49 | } else { 50 | None 51 | }; 52 | let tabchar = if arg_matches.get_flag("usetabs") { 53 | Some(TabChar::Tab) 54 | } else { 55 | None 56 | }; 57 | let verbosity = if arg_matches.get_flag("trace") { 58 | Some(LevelFilter::Trace) 59 | } else if arg_matches.get_flag("verbose") { 60 | Some(LevelFilter::Info) 61 | } else if arg_matches.get_flag("quiet") { 62 | Some(LevelFilter::Error) 63 | } else { 64 | None 65 | }; 66 | let args = OptionArgs { 67 | check: get_flag(&arg_matches, "check"), 68 | print: get_flag(&arg_matches, "print"), 69 | fail_on_change: get_flag(&arg_matches, "fail-on-change"), 70 | wrap, 71 | wraplen: arg_matches.get_one::("wraplen").copied(), 72 | wrapmin: None, 73 | tabsize: arg_matches.get_one::("tabsize").copied(), 74 | tabchar, 75 | stdin: get_flag(&arg_matches, "stdin"), 76 | config: arg_matches.get_one::("config").cloned(), 77 | noconfig: get_flag(&arg_matches, "noconfig"), 78 | lists: vec![], 79 | verbatims: vec![], 80 | no_indent_envs: vec![], 81 | wrap_chars: vec![], 82 | verbosity, 83 | arguments: get_flag(&arg_matches, "args"), 84 | files: arg_matches 85 | .get_many::("files") 86 | .unwrap_or_default() 87 | .map(ToOwned::to_owned) 88 | .collect::>(), 89 | }; 90 | args 91 | } 92 | -------------------------------------------------------------------------------- /src/command.rs: -------------------------------------------------------------------------------- 1 | use clap::{value_parser, Command, Arg, ArgAction}; 2 | use ArgAction::{Append, SetTrue}; 3 | use std::path::PathBuf; 4 | 5 | /// Construct the CLI command 6 | #[allow(clippy::too_many_lines)] 7 | #[must_use] 8 | pub fn get_cli_command() -> Command { 9 | Command::new("tex-fmt") 10 | .author("William George Underwood, wg.underwood13@gmail.com") 11 | .about(clap::crate_description!()) 12 | .version(clap::crate_version!()) 13 | .before_help(format!("tex-fmt {}", clap::crate_version!())) 14 | .arg( 15 | Arg::new("check") 16 | .short('c') 17 | .long("check") 18 | .action(SetTrue) 19 | .help("Check formatting, do not modify files"), 20 | ) 21 | .arg( 22 | Arg::new("print") 23 | .short('p') 24 | .long("print") 25 | .action(SetTrue) 26 | .help("Print to stdout, do not modify files"), 27 | ) 28 | .arg( 29 | Arg::new("fail-on-change") 30 | .short('f') 31 | .long("fail-on-change") 32 | .action(SetTrue) 33 | .help("Format files and return non-zero exit code if files are modified") 34 | ) 35 | .arg( 36 | Arg::new("nowrap") 37 | .short('n') 38 | .long("nowrap") 39 | .action(SetTrue) 40 | .help("Do not wrap long lines"), 41 | ) 42 | .arg( 43 | Arg::new("wraplen") 44 | .short('l') 45 | .long("wraplen") 46 | .value_name("N") 47 | .value_parser(value_parser!(u8)) 48 | .help("Line length for wrapping [default: 80]"), 49 | ) 50 | .arg( 51 | Arg::new("tabsize") 52 | .short('t') 53 | .long("tabsize") 54 | .value_name("N") 55 | .value_parser(value_parser!(u8)) 56 | .help("Number of characters to use as tab size [default: 2]"), 57 | ) 58 | .arg( 59 | Arg::new("usetabs") 60 | .long("usetabs") 61 | .action(SetTrue) 62 | .help("Use tabs instead of spaces for indentation"), 63 | ) 64 | .arg( 65 | Arg::new("stdin") 66 | .short('s') 67 | .long("stdin") 68 | .action(SetTrue) 69 | .help("Process stdin as a single file, output to stdout"), 70 | ) 71 | .arg( 72 | Arg::new("config") 73 | .long("config") 74 | .value_name("PATH") 75 | .value_parser(value_parser!(PathBuf)) 76 | .help("Path to config file") 77 | ) 78 | .arg( 79 | Arg::new("noconfig") 80 | .long("noconfig") 81 | .action(SetTrue) 82 | .help("Do not read any config file"), 83 | ) 84 | .arg( 85 | Arg::new("verbose") 86 | .short('v') 87 | .long("verbose") 88 | .action(SetTrue) 89 | .help("Show info messages"), 90 | ) 91 | .arg( 92 | Arg::new("quiet") 93 | .short('q') 94 | .long("quiet") 95 | .action(SetTrue) 96 | .help("Hide warning messages"), 97 | ) 98 | .arg( 99 | Arg::new("trace") 100 | .long("trace") 101 | .action(SetTrue) 102 | .help("Show trace messages"), 103 | ) 104 | .arg( 105 | Arg::new("completion") 106 | .long("completion") 107 | .value_name("SHELL") 108 | .value_parser(value_parser!(Shell)) 109 | .help("Generate shell completion script") 110 | ) 111 | .arg( 112 | Arg::new("man") 113 | .long("man") 114 | .action(SetTrue) 115 | .help("Generate man page"), 116 | ) 117 | .arg( 118 | Arg::new("args") 119 | .long("args") 120 | .action(SetTrue) 121 | .help("Print arguments passed to tex-fmt and exit"), 122 | ) 123 | .arg( 124 | Arg::new("files") 125 | .action(Append) 126 | .help("List of files to be formatted"), 127 | ) 128 | } 129 | -------------------------------------------------------------------------------- /src/comments.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for finding, extracting and removing LaTeX comments 2 | 3 | use crate::format::Pattern; 4 | 5 | /// Find the location where a comment begins in a line 6 | #[must_use] 7 | pub fn find_comment_index(line: &str, pattern: &Pattern) -> Option { 8 | // Often there is no '%' so check this first 9 | if pattern.contains_comment { 10 | let mut prev_c = ' '; 11 | for (i, c) in line.char_indices() { 12 | if c == '%' && prev_c != '\\' { 13 | return Some(i); 14 | } 15 | prev_c = c; 16 | } 17 | } 18 | None 19 | } 20 | 21 | /// Remove a comment from the end of a line 22 | #[must_use] 23 | pub fn remove_comment(line: &str, comment: Option) -> &str { 24 | comment.map_or_else(|| line, |c| &line[0..c]) 25 | } 26 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | //! Read arguments from a config file 2 | 3 | use crate::args::{OptionArgs, TabChar}; 4 | use dirs::config_dir; 5 | use log::LevelFilter; 6 | use std::env::current_dir; 7 | use std::fs::{metadata, read_to_string}; 8 | use std::path::PathBuf; 9 | use toml::Table; 10 | 11 | /// Config file name 12 | const CONFIG: &str = "tex-fmt.toml"; 13 | 14 | /// Try finding a config file in various sources 15 | fn resolve_config_path(args: &OptionArgs) -> Option { 16 | // Do not read config file 17 | if args.noconfig == Some(true) { 18 | return None; 19 | } 20 | // Named path passed as cli arg 21 | if args.config.is_some() { 22 | return args.config.clone(); 23 | } 24 | // Config file in current directory 25 | if let Ok(mut config) = current_dir() { 26 | config.push(CONFIG); 27 | if config.exists() { 28 | return Some(config); 29 | } 30 | } 31 | // Config file at git repository root 32 | if let Some(mut config) = find_git_root() { 33 | config.push(CONFIG); 34 | if config.exists() { 35 | return Some(config); 36 | } 37 | } 38 | // Config file in user home config directory 39 | if let Some(mut config) = config_dir() { 40 | config.push("tex-fmt"); 41 | config.push(CONFIG); 42 | if config.exists() { 43 | return Some(config); 44 | } 45 | } 46 | None 47 | } 48 | 49 | /// Get the git repository root directory 50 | fn find_git_root() -> Option { 51 | let mut depth = 0; 52 | let mut current_dir = current_dir().unwrap(); 53 | while depth < 100 { 54 | depth += 1; 55 | if metadata(current_dir.join(".git")) 56 | .map(|m| m.is_dir()) 57 | .unwrap_or(false) 58 | { 59 | return Some(current_dir); 60 | } 61 | if !current_dir.pop() { 62 | break; 63 | } 64 | } 65 | None 66 | } 67 | 68 | /// Read content from a config file path 69 | /// 70 | /// # Panics 71 | /// 72 | /// This function panics if the config file cannot be read. 73 | #[must_use] 74 | pub fn get_config(args: &OptionArgs) -> Option<(PathBuf, String, String)> { 75 | let config_path = resolve_config_path(args); 76 | config_path.as_ref()?; 77 | let config_path_string = config_path 78 | .clone() 79 | .unwrap() 80 | .into_os_string() 81 | .into_string() 82 | .unwrap(); 83 | let config = read_to_string(config_path.clone().unwrap()).unwrap(); 84 | Some((config_path.unwrap(), config_path_string, config)) 85 | } 86 | 87 | fn parse_array_string(name: &str, config: &Table) -> Vec { 88 | config 89 | .get(name) 90 | .and_then(|v| v.as_array()) 91 | .unwrap_or(&vec![]) 92 | .iter() 93 | .filter_map(|v| v.as_str().map(String::from)) 94 | .collect() 95 | } 96 | 97 | fn string_to_char(s: &str) -> char { 98 | let mut chars = s.chars(); 99 | let c = chars.next().expect("String is empty"); 100 | assert!( 101 | chars.next().is_none(), 102 | "String contains more than one character", 103 | ); 104 | c 105 | } 106 | 107 | /// Parse arguments from a config file path 108 | /// 109 | /// # Panics 110 | /// 111 | /// This function panics if the config file cannot be parsed into TOML 112 | #[must_use] 113 | pub fn get_config_args( 114 | config: Option<(PathBuf, String, String)>, 115 | ) -> Option { 116 | config.as_ref()?; 117 | let (config_path, config_path_string, config) = config.unwrap(); 118 | let config = config.parse::().unwrap_or_else(|_| { 119 | panic!("Failed to read config file at {config_path_string}") 120 | }); 121 | 122 | let verbosity = match config.get("verbosity").map(|x| x.as_str().unwrap()) { 123 | Some("error" | "quiet") => Some(LevelFilter::Error), 124 | Some("warn") => Some(LevelFilter::Warn), 125 | Some("info" | "verbose") => Some(LevelFilter::Info), 126 | Some("trace") => Some(LevelFilter::Trace), 127 | _ => None, 128 | }; 129 | 130 | let tabchar = match config.get("tabchar").map(|x| x.as_str().unwrap()) { 131 | Some("tab") => Some(TabChar::Tab), 132 | Some("space") => Some(TabChar::Space), 133 | _ => None, 134 | }; 135 | 136 | // Read wrap_chars to Vec not Vec 137 | let wrap_chars: Vec = parse_array_string("wrap-chars", &config) 138 | .iter() 139 | .map(|c| string_to_char(c)) 140 | .collect(); 141 | 142 | let args = OptionArgs { 143 | check: config.get("check").map(|x| x.as_bool().unwrap()), 144 | print: config.get("print").map(|x| x.as_bool().unwrap()), 145 | fail_on_change: config 146 | .get("fail-on-change") 147 | .map(|x| x.as_bool().unwrap()), 148 | wrap: config.get("wrap").map(|x| x.as_bool().unwrap()), 149 | wraplen: config 150 | .get("wraplen") 151 | .map(|x| x.as_integer().unwrap().try_into().unwrap()), 152 | wrapmin: config 153 | .get("wrapmin") 154 | .map(|x| x.as_integer().unwrap().try_into().unwrap()), 155 | tabsize: config 156 | .get("tabsize") 157 | .map(|x| x.as_integer().unwrap().try_into().unwrap()), 158 | tabchar, 159 | stdin: config.get("stdin").map(|x| x.as_bool().unwrap()), 160 | config: Some(config_path), 161 | noconfig: None, 162 | lists: parse_array_string("lists", &config), 163 | verbatims: parse_array_string("verbatims", &config), 164 | no_indent_envs: parse_array_string("no-indent-envs", &config), 165 | wrap_chars, 166 | verbosity, 167 | arguments: None, 168 | files: vec![], 169 | }; 170 | Some(args) 171 | } 172 | -------------------------------------------------------------------------------- /src/ignore.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for ignoring/skipping source lines 2 | 3 | use crate::format::State; 4 | use crate::logging::{record_line_log, Log}; 5 | use log::Level::Warn; 6 | 7 | /// Information on the ignored state of a line 8 | #[derive(Clone, Debug)] 9 | pub struct Ignore { 10 | /// Whether the line is in an ignore block 11 | pub actual: bool, 12 | /// Whether the line should be ignored/skipped 13 | pub visual: bool, 14 | } 15 | 16 | impl Ignore { 17 | /// Construct a new ignore state 18 | #[must_use] 19 | pub const fn new() -> Self { 20 | Self { 21 | actual: false, 22 | visual: false, 23 | } 24 | } 25 | } 26 | 27 | impl Default for Ignore { 28 | fn default() -> Self { 29 | Self::new() 30 | } 31 | } 32 | 33 | /// Determine whether a line should be ignored 34 | pub fn get_ignore( 35 | line: &str, 36 | state: &State, 37 | logs: &mut Vec, 38 | file: &str, 39 | warn: bool, 40 | ) -> Ignore { 41 | let skip = contains_ignore_skip(line); 42 | let begin = contains_ignore_begin(line); 43 | let end = contains_ignore_end(line); 44 | let actual: bool; 45 | let visual: bool; 46 | 47 | if skip { 48 | actual = state.ignore.actual; 49 | visual = true; 50 | } else if begin { 51 | actual = true; 52 | visual = true; 53 | if warn && state.ignore.actual { 54 | record_line_log( 55 | logs, 56 | Warn, 57 | file, 58 | state.linum_new, 59 | state.linum_old, 60 | line, 61 | "Cannot begin ignore block:", 62 | ); 63 | } 64 | } else if end { 65 | actual = false; 66 | visual = true; 67 | if warn && !state.ignore.actual { 68 | record_line_log( 69 | logs, 70 | Warn, 71 | file, 72 | state.linum_new, 73 | state.linum_old, 74 | line, 75 | "No ignore block to end.", 76 | ); 77 | } 78 | } else { 79 | actual = state.ignore.actual; 80 | visual = state.ignore.actual; 81 | } 82 | 83 | Ignore { actual, visual } 84 | } 85 | 86 | /// Check if a line contains a skip directive 87 | fn contains_ignore_skip(line: &str) -> bool { 88 | line.ends_with("% tex-fmt: skip") 89 | } 90 | 91 | /// Check if a line contains the start of an ignore block 92 | fn contains_ignore_begin(line: &str) -> bool { 93 | line.ends_with("% tex-fmt: off") 94 | } 95 | 96 | /// Check if a line contains the end of an ignore block 97 | fn contains_ignore_end(line: &str) -> bool { 98 | line.ends_with("% tex-fmt: on") 99 | } 100 | -------------------------------------------------------------------------------- /src/indent.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for indenting source lines 2 | 3 | use crate::args::Args; 4 | use crate::comments::{find_comment_index, remove_comment}; 5 | use crate::format::{Pattern, State}; 6 | use crate::logging::{record_line_log, Log}; 7 | use crate::regexes::{ENV_BEGIN, ENV_END, ITEM, VERB}; 8 | use core::cmp::max; 9 | use log::Level; 10 | use log::LevelFilter; 11 | 12 | /// Opening delimiters 13 | const OPENS: [char; 3] = ['{', '(', '[']; 14 | /// Closing delimiters 15 | const CLOSES: [char; 3] = ['}', ')', ']']; 16 | 17 | /// Information on the indentation state of a line 18 | #[derive(Debug, Clone)] 19 | pub struct Indent { 20 | /// The indentation level of a line 21 | pub actual: i8, 22 | /// The visual indentation level of a line 23 | pub visual: i8, 24 | } 25 | 26 | impl Indent { 27 | /// Construct a new indentation state 28 | #[must_use] 29 | pub const fn new() -> Self { 30 | Self { 31 | actual: 0, 32 | visual: 0, 33 | } 34 | } 35 | } 36 | 37 | impl Default for Indent { 38 | fn default() -> Self { 39 | Self::new() 40 | } 41 | } 42 | 43 | /// Calculate total indentation change due to the current line 44 | fn get_diff( 45 | line: &str, 46 | pattern: &Pattern, 47 | lists_begin: &[String], 48 | lists_end: &[String], 49 | no_indent_envs_begin: &[String], 50 | no_indent_envs_end: &[String], 51 | ) -> i8 { 52 | // Do not indent if line contains \verb|...| 53 | if pattern.contains_verb && line.contains(VERB) { 54 | return 0; 55 | } 56 | 57 | // Indentation for environments 58 | let mut diff: i8 = 0; 59 | if pattern.contains_env_begin && line.contains(ENV_BEGIN) { 60 | if no_indent_envs_begin.iter().any(|r| line.contains(r)) { 61 | return 0; 62 | } 63 | diff += 1; 64 | diff += i8::from(lists_begin.iter().any(|r| line.contains(r))); 65 | } else if pattern.contains_env_end && line.contains(ENV_END) { 66 | if no_indent_envs_end.iter().any(|r| line.contains(r)) { 67 | return 0; 68 | } 69 | diff -= 1; 70 | diff -= i8::from(lists_end.iter().any(|r| line.contains(r))); 71 | } 72 | 73 | // Indentation for delimiters 74 | diff += line 75 | .chars() 76 | .map(|x| i8::from(OPENS.contains(&x)) - i8::from(CLOSES.contains(&x))) 77 | .sum::(); 78 | 79 | diff 80 | } 81 | 82 | /// Calculate dedentation for the current line 83 | fn get_back( 84 | line: &str, 85 | pattern: &Pattern, 86 | state: &State, 87 | lists_end: &[String], 88 | no_indent_envs_end: &[String], 89 | ) -> i8 { 90 | // Only need to dedent if indentation is present 91 | if state.indent.actual == 0 { 92 | return 0; 93 | } 94 | let mut back: i8 = 0; 95 | 96 | // Don't apply any indenting if a \verb|...| is present 97 | if pattern.contains_verb && line.contains(VERB) { 98 | return 0; 99 | } 100 | 101 | // Calculate dedentation for environments 102 | if pattern.contains_env_end && line.contains(ENV_END) { 103 | // Some environments are not indented 104 | if no_indent_envs_end.iter().any(|r| line.contains(r)) { 105 | return 0; 106 | } 107 | // List environments get double indents for indenting items 108 | for r in lists_end { 109 | if line.contains(r) { 110 | return 2; 111 | } 112 | } 113 | // Other environments get single indents 114 | back = 1; 115 | } else if pattern.contains_item && line.contains(ITEM) { 116 | // Deindent items to make the rest of item environment appear indented 117 | back += 1; 118 | } 119 | 120 | // Dedent delimiters 121 | let mut cumul: i8 = back; 122 | for c in line.chars() { 123 | cumul -= i8::from(OPENS.contains(&c)); 124 | cumul += i8::from(CLOSES.contains(&c)); 125 | back = max(cumul, back); 126 | } 127 | 128 | back 129 | } 130 | 131 | /// Calculate indentation properties of the current line 132 | #[allow(clippy::too_many_arguments)] 133 | fn get_indent( 134 | line: &str, 135 | prev_indent: &Indent, 136 | pattern: &Pattern, 137 | state: &State, 138 | lists_begin: &[String], 139 | lists_end: &[String], 140 | no_indent_envs_begin: &[String], 141 | no_indent_envs_end: &[String], 142 | ) -> Indent { 143 | let diff = get_diff( 144 | line, 145 | pattern, 146 | lists_begin, 147 | lists_end, 148 | no_indent_envs_begin, 149 | no_indent_envs_end, 150 | ); 151 | let back = get_back(line, pattern, state, lists_end, no_indent_envs_end); 152 | let actual = prev_indent.actual + diff; 153 | let visual = prev_indent.actual - back; 154 | Indent { actual, visual } 155 | } 156 | 157 | /// Calculates the indent for `line` based on its contents. 158 | /// This functions saves the calculated [Indent], which might be 159 | /// negative, to the given [State], and then ensures that the returned 160 | /// [Indent] is non-negative. 161 | #[allow(clippy::too_many_arguments)] 162 | pub fn calculate_indent( 163 | line: &str, 164 | state: &mut State, 165 | logs: &mut Vec, 166 | file: &str, 167 | args: &Args, 168 | pattern: &Pattern, 169 | lists_begin: &[String], 170 | lists_end: &[String], 171 | no_indent_envs_begin: &[String], 172 | no_indent_envs_end: &[String], 173 | ) -> Indent { 174 | // Calculate the new indent by first removing the comment from the line 175 | // (if there is one) to ignore diffs from characters in there. 176 | let comment_index = find_comment_index(line, pattern); 177 | let line_strip = remove_comment(line, comment_index); 178 | let mut indent = get_indent( 179 | line_strip, 180 | &state.indent, 181 | pattern, 182 | state, 183 | lists_begin, 184 | lists_end, 185 | no_indent_envs_begin, 186 | no_indent_envs_end, 187 | ); 188 | 189 | // Record the indent to the logs. 190 | if args.verbosity == LevelFilter::Trace { 191 | record_line_log( 192 | logs, 193 | Level::Trace, 194 | file, 195 | state.linum_new, 196 | state.linum_old, 197 | line, 198 | &format!( 199 | "Indent: actual = {}, visual = {}:", 200 | indent.actual, indent.visual 201 | ), 202 | ); 203 | } 204 | 205 | // Save the indent to the state. Note, this indent might be negative; 206 | // it is saved without correction so that this is 207 | // not forgotten for the next iterations. 208 | state.indent = indent.clone(); 209 | 210 | // Update the last zero-indented line for use in error messages. 211 | if indent.visual == 0 && state.linum_new > state.linum_last_zero_indent { 212 | state.linum_last_zero_indent = state.linum_new; 213 | } 214 | 215 | // However, we can't negatively indent a line. 216 | // So we log the negative indent and reset the values to 0. 217 | if (indent.visual < 0) || (indent.actual < 0) { 218 | record_line_log( 219 | logs, 220 | Level::Warn, 221 | file, 222 | state.linum_new, 223 | state.linum_old, 224 | line, 225 | "Indent is negative.", 226 | ); 227 | indent.actual = indent.actual.max(0); 228 | indent.visual = indent.visual.max(0); 229 | 230 | // If this is the first negatively indented line, record in the state 231 | if state.linum_first_negative_indent.is_none() { 232 | state.linum_first_negative_indent = Some(state.linum_new); 233 | } 234 | } 235 | 236 | indent 237 | } 238 | 239 | /// Apply the given indentation to a line 240 | #[must_use] 241 | pub fn apply_indent( 242 | line: &str, 243 | indent: &Indent, 244 | args: &Args, 245 | indent_char: &str, 246 | ) -> String { 247 | let first_non_whitespace = line.chars().position(|c| !c.is_whitespace()); 248 | 249 | // If line is blank, return an empty line 250 | if first_non_whitespace.is_none() { 251 | return String::new(); 252 | } 253 | 254 | // If line is correctly indented, return it directly 255 | #[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] 256 | let n_indent_chars = (indent.visual * args.tabsize as i8) as usize; 257 | if first_non_whitespace == Some(n_indent_chars) { 258 | return line.into(); 259 | } 260 | 261 | // Otherwise, allocate enough memory to fit line with the added 262 | // indentation and insert the appropriate string slices 263 | let trimmed_line = line.trim_start(); 264 | let mut new_line = 265 | String::with_capacity(trimmed_line.len() + n_indent_chars); 266 | for idx in 0..n_indent_chars { 267 | new_line.insert_str(idx, indent_char); 268 | } 269 | new_line.insert_str(n_indent_chars, trimmed_line); 270 | new_line 271 | } 272 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Main library 2 | 3 | #![doc( 4 | html_logo_url = "https://raw.githubusercontent.com/WGUNDERWOOD/tex-fmt/main/extra/logo.svg" 5 | )] 6 | #![warn(clippy::pedantic)] 7 | 8 | pub mod args; 9 | pub mod cli; 10 | pub mod comments; 11 | pub mod config; 12 | pub mod format; 13 | pub mod ignore; 14 | pub mod indent; 15 | pub mod logging; 16 | pub mod read; 17 | pub mod regexes; 18 | pub mod subs; 19 | pub mod verbatim; 20 | pub mod wasm; 21 | pub mod wrap; 22 | pub mod write; 23 | 24 | #[cfg(test)] 25 | pub mod tests; 26 | 27 | #[cfg(any(target_family = "unix", target_family = "wasm"))] 28 | /// Line ending for unix 29 | const LINE_END: &str = "\n"; 30 | 31 | #[cfg(target_family = "windows")] 32 | /// Line ending for Windows 33 | const LINE_END: &str = "\r\n"; 34 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for logging 2 | 3 | use crate::args::Args; 4 | use colored::{Color, Colorize}; 5 | use env_logger::Builder; 6 | use log::Level; 7 | use log::Level::{Debug, Error, Info, Trace, Warn}; 8 | use log::LevelFilter; 9 | use std::cmp::Reverse; 10 | use std::io::Write; 11 | use std::path::Path; 12 | use web_time::Instant; 13 | 14 | /// Holds a log entry 15 | #[derive(Debug)] 16 | pub struct Log { 17 | /// Log entry level 18 | pub level: Level, 19 | /// Time when the entry was logged 20 | pub time: Instant, 21 | /// File name associated with the entry 22 | pub file: String, 23 | /// Line number in the formatted file 24 | pub linum_new: Option, 25 | /// Line number in the original file 26 | pub linum_old: Option, 27 | /// Line content 28 | pub line: Option, 29 | /// Entry-specific message 30 | pub message: String, 31 | } 32 | 33 | /// Append a log to the logs list 34 | fn record_log( 35 | logs: &mut Vec, 36 | level: Level, 37 | file: &str, 38 | linum_new: Option, 39 | linum_old: Option, 40 | line: Option, 41 | message: &str, 42 | ) { 43 | let log = Log { 44 | level, 45 | time: Instant::now(), 46 | file: file.to_string(), 47 | linum_new, 48 | linum_old, 49 | line, 50 | message: message.to_string(), 51 | }; 52 | logs.push(log); 53 | } 54 | 55 | /// Append a file log to the logs list 56 | pub fn record_file_log( 57 | logs: &mut Vec, 58 | level: Level, 59 | file: &str, 60 | message: &str, 61 | ) { 62 | record_log(logs, level, file, None, None, None, message); 63 | } 64 | 65 | /// Append a line log to the logs list 66 | pub fn record_line_log( 67 | logs: &mut Vec, 68 | level: Level, 69 | file: &str, 70 | linum_new: usize, 71 | linum_old: usize, 72 | line: &str, 73 | message: &str, 74 | ) { 75 | record_log( 76 | logs, 77 | level, 78 | file, 79 | Some(linum_new), 80 | Some(linum_old), 81 | Some(line.to_string()), 82 | message, 83 | ); 84 | } 85 | 86 | /// Get the color of a log level 87 | const fn get_log_color(log_level: Level) -> Color { 88 | match log_level { 89 | Info => Color::Cyan, 90 | Warn => Color::Yellow, 91 | Error => Color::Red, 92 | Trace => Color::Green, 93 | Debug => panic!(), 94 | } 95 | } 96 | 97 | /// Start the logger 98 | pub fn init_logger(level_filter: LevelFilter) { 99 | Builder::new() 100 | .filter_level(level_filter) 101 | .format(|buf, record| { 102 | writeln!( 103 | buf, 104 | "{}: {}", 105 | record 106 | .level() 107 | .to_string() 108 | .color(get_log_color(record.level())) 109 | .bold(), 110 | record.args() 111 | ) 112 | }) 113 | .init(); 114 | } 115 | 116 | /// Sort and remove duplicates 117 | fn preprocess_logs(logs: &mut Vec) { 118 | logs.sort_by_key(|l| { 119 | ( 120 | l.level, 121 | l.linum_new, 122 | l.linum_old, 123 | l.message.clone(), 124 | Reverse(l.time), 125 | ) 126 | }); 127 | logs.dedup_by(|a, b| { 128 | ( 129 | a.level, 130 | &a.file, 131 | a.linum_new, 132 | a.linum_old, 133 | &a.line, 134 | &a.message, 135 | ) == ( 136 | b.level, 137 | &b.file, 138 | b.linum_new, 139 | b.linum_old, 140 | &b.line, 141 | &b.message, 142 | ) 143 | }); 144 | logs.sort_by_key(|l| l.time); 145 | } 146 | 147 | /// Format a log entry 148 | fn format_log(log: &Log) -> String { 149 | let linum_new = log 150 | .linum_new 151 | .map_or_else(String::new, |i| format!("Line {i} ")); 152 | 153 | let linum_old = log 154 | .linum_old 155 | .map_or_else(String::new, |i| format!("({i}). ")); 156 | 157 | let line = log 158 | .line 159 | .as_ref() 160 | .map_or_else(String::new, |l| l.trim_start().to_string()); 161 | 162 | let log_string = format!( 163 | "{}{}{} {}", 164 | linum_new.white().bold(), 165 | linum_old.white().bold(), 166 | log.message.yellow().bold(), 167 | line, 168 | ); 169 | log_string 170 | } 171 | 172 | /// Format all of the logs collected 173 | #[allow(clippy::similar_names)] 174 | pub fn format_logs(logs: &mut Vec, args: &Args) -> String { 175 | preprocess_logs(logs); 176 | let mut logs_string = String::new(); 177 | for log in logs { 178 | if log.level <= args.verbosity { 179 | let log_string = format_log(log); 180 | logs_string.push_str(&log_string); 181 | logs_string.push('\n'); 182 | } 183 | } 184 | logs_string 185 | } 186 | 187 | /// Print all of the logs collected 188 | /// 189 | /// # Panics 190 | /// 191 | /// This function panics if the file path does not exist 192 | pub fn print_logs(logs: &mut Vec) { 193 | preprocess_logs(logs); 194 | for log in logs { 195 | let log_string = format!( 196 | "{} {}: {}", 197 | "tex-fmt".magenta().bold(), 198 | match log.file.as_str() { 199 | "" | "" => "".blue().bold(), 200 | _ => Path::new(&log.file) 201 | .file_name() 202 | .unwrap() 203 | .to_str() 204 | .unwrap() 205 | .blue() 206 | .bold(), 207 | }, 208 | format_log(log), 209 | ); 210 | 211 | match log.level { 212 | Error => log::error!("{log_string}"), 213 | Warn => log::warn!("{log_string}"), 214 | Info => log::info!("{log_string}"), 215 | Trace => log::trace!("{log_string}"), 216 | Debug => panic!(), 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/read.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for reading files 2 | 3 | use crate::logging::{record_file_log, Log}; 4 | use crate::regexes::EXTENSIONS; 5 | use log::Level::{Error, Trace}; 6 | use std::fs; 7 | use std::io::Read; 8 | 9 | /// Add a missing extension and read the file 10 | pub fn read(file: &str, logs: &mut Vec) -> Option<(String, String)> { 11 | // Check if file has an accepted extension 12 | let has_ext = EXTENSIONS.iter().any(|e| file.ends_with(e)); 13 | // If no valid extension, try adding .tex 14 | let mut new_file = file.to_owned(); 15 | if !has_ext { 16 | new_file.push_str(".tex"); 17 | } 18 | if let Ok(text) = fs::read_to_string(&new_file) { 19 | return Some((new_file, text)); 20 | } 21 | if has_ext { 22 | record_file_log(logs, Error, file, "Could not open file."); 23 | } else { 24 | record_file_log(logs, Error, file, "File type invalid."); 25 | } 26 | None 27 | } 28 | 29 | /// Attempt to read from stdin, return filename `` and text 30 | pub fn read_stdin(logs: &mut Vec) -> Option<(String, String)> { 31 | let mut text = String::new(); 32 | match std::io::stdin().read_to_string(&mut text) { 33 | Ok(bytes) => { 34 | record_file_log( 35 | logs, 36 | Trace, 37 | "", 38 | &format!("Read {bytes} bytes."), 39 | ); 40 | Some((String::from(""), text)) 41 | } 42 | Err(e) => { 43 | record_file_log( 44 | logs, 45 | Error, 46 | "", 47 | &format!("Could not read from stdin: {e}"), 48 | ); 49 | None 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/regexes.rs: -------------------------------------------------------------------------------- 1 | //! Regexes and matching utilities 2 | 3 | use crate::LINE_END; 4 | use regex::Regex; 5 | use std::sync::LazyLock; 6 | 7 | /// Match a LaTeX \item 8 | pub const ITEM: &str = "\\item"; 9 | 10 | /// Match a LaTeX \begin{...} 11 | pub const ENV_BEGIN: &str = "\\begin{"; 12 | 13 | /// Match a LaTeX \end{...} 14 | pub const ENV_END: &str = "\\end{"; 15 | 16 | /// Acceptable LaTeX file extensions 17 | pub const EXTENSIONS: [&str; 4] = [".tex", ".bib", ".sty", ".cls"]; 18 | /// Match a LaTeX \verb|...| 19 | pub const VERB: &str = "\\verb|"; 20 | 21 | /// Regex matches for sectioning commands 22 | const SPLITTING: [&str; 6] = [ 23 | r"\\begin\{", 24 | r"\\end\{", 25 | r"\\item(?:$|[^a-zA-Z])", 26 | r"\\(?:sub){0,2}section\*?\{", 27 | r"\\chapter\*?\{", 28 | r"\\part\*?\{", 29 | ]; 30 | 31 | // A static `String` which is a regex to match any of [`SPLITTING_COMMANDS`]. 32 | static SPLITTING_STRING: LazyLock = 33 | LazyLock::new(|| ["(", SPLITTING.join("|").as_str(), ")"].concat()); 34 | 35 | // Regex to match newlines 36 | pub static RE_NEWLINES: LazyLock = LazyLock::new(|| { 37 | Regex::new(&format!(r"{LINE_END}{LINE_END}({LINE_END})+")).unwrap() 38 | }); 39 | 40 | // Regex to match trailing new ines 41 | pub static RE_TRAIL: LazyLock = 42 | LazyLock::new(|| Regex::new(&format!(r" +{LINE_END}")).unwrap()); 43 | 44 | // Regex that matches splitting commands 45 | pub static RE_SPLITTING: LazyLock = 46 | LazyLock::new(|| Regex::new(SPLITTING_STRING.as_str()).unwrap()); 47 | 48 | // Matches splitting commands with non-whitespace characters before it. 49 | pub static RE_SPLITTING_SHARED_LINE: LazyLock = LazyLock::new(|| { 50 | Regex::new( 51 | [r"(:?\S.*?)", "(:?", SPLITTING_STRING.as_str(), ".*)"] 52 | .concat() 53 | .as_str(), 54 | ) 55 | .unwrap() 56 | }); 57 | 58 | // Matches any splitting command with non-whitespace 59 | // characters before it, catches the previous text in a group called 60 | // "prev" and captures the command itself and the remaining text 61 | // in a group called "env". 62 | pub static RE_SPLITTING_SHARED_LINE_CAPTURE: LazyLock = 63 | LazyLock::new(|| { 64 | Regex::new( 65 | [ 66 | r"(?P\S.*?)", 67 | "(?P", 68 | SPLITTING_STRING.as_str(), 69 | ".*)", 70 | ] 71 | .concat() 72 | .as_str(), 73 | ) 74 | .unwrap() 75 | }); 76 | -------------------------------------------------------------------------------- /src/subs.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for performing text substitutions 2 | 3 | use crate::args::Args; 4 | use crate::comments::find_comment_index; 5 | use crate::format::{Pattern, State}; 6 | use crate::logging::{record_line_log, Log}; 7 | use crate::regexes; 8 | use crate::LINE_END; 9 | use log::Level; 10 | use log::LevelFilter; 11 | 12 | /// Remove multiple line breaks 13 | #[must_use] 14 | pub fn remove_extra_newlines(text: &str) -> String { 15 | let double_line_end = format!("{LINE_END}{LINE_END}"); 16 | regexes::RE_NEWLINES 17 | .replace_all(text, double_line_end) 18 | .to_string() 19 | } 20 | 21 | /// Replace tabs with spaces 22 | #[must_use] 23 | pub fn remove_tabs(text: &str, args: &Args) -> String { 24 | let replace = (0..args.tabsize).map(|_| " ").collect::(); 25 | text.replace('\t', &replace) 26 | } 27 | 28 | /// Remove trailing spaces from line endings 29 | #[must_use] 30 | pub fn remove_trailing_spaces(text: &str) -> String { 31 | regexes::RE_TRAIL.replace_all(text, LINE_END).to_string() 32 | } 33 | 34 | /// Remove trailing blank lines from file 35 | #[must_use] 36 | pub fn remove_trailing_blank_lines(text: &str) -> String { 37 | let mut new_text = text.trim_end().to_string(); 38 | new_text.push_str(LINE_END); 39 | new_text 40 | } 41 | 42 | /// Check if line contains content which be split onto a new line 43 | /// 44 | /// # Panics 45 | /// 46 | /// This function should not panic as we already check for a regex match 47 | /// before finding the match index and unwrapping. 48 | #[must_use] 49 | pub fn needs_split(line: &str, pattern: &Pattern) -> bool { 50 | // Don't split anything if the line contains a \verb|...| 51 | if pattern.contains_verb && line.contains(regexes::VERB) { 52 | return false; 53 | } 54 | 55 | // Check if we should format this line and if we've matched an environment. 56 | let contains_splittable_env = pattern.contains_splitting 57 | && regexes::RE_SPLITTING_SHARED_LINE.is_match(line); 58 | 59 | // If we're not ignoring and we've matched an environment ... 60 | if contains_splittable_env { 61 | // ... return `true` if the comment index is `None` 62 | // (which implies the split point must be in text), otherwise 63 | // compare the index of the comment with the split point. 64 | find_comment_index(line, pattern).is_none_or(|comment_index| { 65 | if regexes::RE_SPLITTING_SHARED_LINE_CAPTURE 66 | .captures(line) 67 | .unwrap() // Matched split point so no panic. 68 | .get(2) 69 | .unwrap() // Regex has 4 groups so index 2 is in bounds. 70 | .start() 71 | > comment_index 72 | { 73 | // If split point is past the comment index, don't split. 74 | false 75 | } else { 76 | // Otherwise, split point is before comment and we do split. 77 | true 78 | } 79 | }) 80 | } else { 81 | // If ignoring or didn't match an environment, don't need a new line. 82 | false 83 | } 84 | } 85 | 86 | /// Ensure lines are split correctly. 87 | /// 88 | /// Returns a tuple containing: 89 | /// 1. a reference to the line that was given, shortened because of the split 90 | /// 2. a reference to the part of the line that was split 91 | /// 92 | /// # Panics 93 | /// 94 | /// This function should not panic as all regexes are validated. 95 | pub fn split_line<'a>( 96 | line: &'a str, 97 | state: &State, 98 | file: &str, 99 | args: &Args, 100 | logs: &mut Vec, 101 | ) -> (&'a str, &'a str) { 102 | let captures = regexes::RE_SPLITTING_SHARED_LINE_CAPTURE 103 | .captures(line) 104 | .unwrap(); 105 | 106 | let (line, [prev, rest, _]) = captures.extract(); 107 | 108 | if args.verbosity == LevelFilter::Trace { 109 | record_line_log( 110 | logs, 111 | Level::Trace, 112 | file, 113 | state.linum_new, 114 | state.linum_old, 115 | line, 116 | "Placing environment on new line.", 117 | ); 118 | } 119 | (prev, rest) 120 | } 121 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::args::*; 2 | use crate::cli::*; 3 | use crate::config::*; 4 | use crate::format::format_file; 5 | use crate::logging::*; 6 | use colored::Colorize; 7 | use merge::Merge; 8 | use similar::{ChangeTag, TextDiff}; 9 | use std::fs; 10 | use std::path::PathBuf; 11 | 12 | fn test_file( 13 | source_file: &str, 14 | target_file: &str, 15 | config_file: Option<&PathBuf>, 16 | cli_file: Option<&PathBuf>, 17 | ) -> bool { 18 | // Get arguments from CLI file 19 | let mut args = match cli_file { 20 | Some(f) => { 21 | let cli_args = fs::read_to_string(f).unwrap(); 22 | let cli_args = cli_args.strip_suffix("\n").unwrap(); 23 | let mut cli_args: Vec<&str> = cli_args.split_whitespace().collect(); 24 | cli_args.insert(0, "tex-fmt"); 25 | let matches = 26 | get_cli_command().try_get_matches_from(&cli_args).unwrap(); 27 | get_cli_args(Some(matches)) 28 | } 29 | None => OptionArgs::new(), 30 | }; 31 | 32 | // Merge arguments from config file 33 | args.config = config_file.cloned(); 34 | let config = get_config(&args); 35 | let config_args = get_config_args(config); 36 | if let Some(c) = config_args { 37 | args.merge(c); 38 | } 39 | 40 | // Merge in default arguments 41 | args.merge(OptionArgs::default()); 42 | let args = Args::from(args); 43 | 44 | // Run tex-fmt 45 | let mut logs = Vec::::new(); 46 | let source_text = fs::read_to_string(source_file).unwrap(); 47 | let target_text = fs::read_to_string(target_file).unwrap(); 48 | let fmt_source_text = 49 | format_file(&source_text, source_file, &args, &mut logs); 50 | 51 | if fmt_source_text != target_text { 52 | println!( 53 | "{} {} -> {}", 54 | "fail".red().bold(), 55 | source_file.yellow().bold(), 56 | target_file.yellow().bold() 57 | ); 58 | let diff = TextDiff::from_lines(&fmt_source_text, &target_text); 59 | for change in diff.iter_all_changes() { 60 | match change.tag() { 61 | ChangeTag::Delete => print!( 62 | "{} {}", 63 | format!("@ {:>3}:", change.old_index().unwrap()) 64 | .blue() 65 | .bold(), 66 | format!("- {change}").red().bold(), 67 | ), 68 | ChangeTag::Insert => print!( 69 | "{} {}", 70 | format!("@ {:>3}:", change.new_index().unwrap()) 71 | .blue() 72 | .bold(), 73 | format!("+ {change}").green().bold(), 74 | ), 75 | ChangeTag::Equal => {} 76 | } 77 | } 78 | } 79 | 80 | fmt_source_text == target_text 81 | } 82 | 83 | fn read_files_from_dir(dir: &PathBuf) -> Vec { 84 | let mut files: Vec = fs::read_dir(dir) 85 | .unwrap() 86 | .map(|f| f.unwrap().file_name().into_string().unwrap()) 87 | .collect(); 88 | files.sort(); 89 | files 90 | } 91 | 92 | fn get_config_file(dir: &fs::DirEntry) -> Option { 93 | let config_file = dir.path().join("tex-fmt.toml"); 94 | if config_file.exists() { 95 | Some(config_file) 96 | } else { 97 | None 98 | } 99 | } 100 | 101 | fn get_cli_file(dir: &fs::DirEntry) -> Option { 102 | let cli_file = dir.path().join("cli.txt"); 103 | if cli_file.exists() { 104 | Some(cli_file) 105 | } else { 106 | None 107 | } 108 | } 109 | 110 | fn test_source_target( 111 | source_file: &str, 112 | target_file: &str, 113 | config_file: Option<&PathBuf>, 114 | cli_file: Option<&PathBuf>, 115 | ) -> bool { 116 | let mut pass = true; 117 | if !test_file(target_file, target_file, config_file, cli_file) { 118 | print!( 119 | "{}", 120 | format!( 121 | "Config file: {config_file:?}\n\ 122 | CLI file: {cli_file:?}\n\ 123 | " 124 | ) 125 | .yellow() 126 | .bold() 127 | ); 128 | pass = false; 129 | } 130 | 131 | if !test_file(source_file, target_file, config_file, cli_file) { 132 | print!( 133 | "{}", 134 | format!( 135 | "Config file: {config_file:?}\n\ 136 | CLI file: {cli_file:?}\n\ 137 | " 138 | ) 139 | .yellow() 140 | .bold() 141 | ); 142 | pass = false; 143 | } 144 | pass 145 | } 146 | 147 | fn run_tests_in_dir(test_dir: &fs::DirEntry) -> bool { 148 | let mut pass = true; 149 | let config_file = get_config_file(test_dir); 150 | let cli_file = get_cli_file(test_dir); 151 | let source_dir = test_dir.path().join("source/"); 152 | let source_files = read_files_from_dir(&source_dir); 153 | let target_dir = test_dir.path().join("target/"); 154 | let target_files = read_files_from_dir(&target_dir); 155 | 156 | // Source and target file names should match 157 | #[allow(clippy::manual_assert)] 158 | if source_files != target_files { 159 | panic!("Source and target file names differ for {test_dir:?}") 160 | } 161 | 162 | // Test file formatting 163 | for file in source_files { 164 | let source_file = test_dir.path().join("source").join(file.clone()); 165 | let source_file = source_file.to_str().unwrap(); 166 | let target_file = test_dir.path().join("target").join(file.clone()); 167 | let target_file = target_file.to_str().unwrap(); 168 | 169 | // If both config and cli exist, either alone should work 170 | if config_file.is_some() && cli_file.is_some() { 171 | pass &= test_source_target( 172 | source_file, 173 | target_file, 174 | config_file.as_ref(), 175 | None, 176 | ); 177 | pass &= test_source_target( 178 | source_file, 179 | target_file, 180 | None, 181 | cli_file.as_ref(), 182 | ); 183 | } 184 | 185 | // Pass both config and cli, even if one or more are None 186 | pass &= test_source_target( 187 | source_file, 188 | target_file, 189 | config_file.as_ref(), 190 | cli_file.as_ref(), 191 | ); 192 | } 193 | 194 | pass 195 | } 196 | 197 | #[test] 198 | fn test_all() { 199 | let mut pass = true; 200 | let test_dirs = fs::read_dir("./tests/").unwrap(); 201 | for test_dir in test_dirs { 202 | pass &= run_tests_in_dir(&test_dir.unwrap()); 203 | } 204 | 205 | assert!(pass); 206 | } 207 | 208 | #[test] 209 | #[ignore] 210 | fn test_subset() { 211 | let test_names = [ 212 | //"wrap_chars", 213 | //"cv", 214 | //"short_document", 215 | "wrap", 216 | ]; 217 | let mut pass = true; 218 | let test_dirs = fs::read_dir("./tests/").unwrap().filter(|d| { 219 | test_names.iter().any(|t| { 220 | d.as_ref() 221 | .unwrap() 222 | .file_name() 223 | .into_string() 224 | .unwrap() 225 | .contains(t) 226 | }) 227 | }); 228 | for test_dir in test_dirs { 229 | pass &= run_tests_in_dir(&test_dir.unwrap()); 230 | } 231 | assert!(pass); 232 | } 233 | -------------------------------------------------------------------------------- /src/verbatim.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for ignoring verbatim environments 2 | 3 | use crate::format::{Pattern, State}; 4 | use crate::logging::{record_line_log, Log}; 5 | use log::Level::Warn; 6 | 7 | /// Information on the verbatim state of a line 8 | #[derive(Clone, Debug)] 9 | pub struct Verbatim { 10 | /// The verbatim depth of a line 11 | pub actual: i8, 12 | /// Whether the line is in a verbatim environment 13 | pub visual: bool, 14 | } 15 | 16 | impl Verbatim { 17 | /// Construct a new verbatim state 18 | #[must_use] 19 | pub const fn new() -> Self { 20 | Self { 21 | actual: 0, 22 | visual: false, 23 | } 24 | } 25 | } 26 | 27 | impl Default for Verbatim { 28 | fn default() -> Self { 29 | Self::new() 30 | } 31 | } 32 | 33 | /// Determine whether a line is in a verbatim environment 34 | #[allow(clippy::too_many_arguments)] 35 | pub fn get_verbatim( 36 | line: &str, 37 | state: &State, 38 | logs: &mut Vec, 39 | file: &str, 40 | warn: bool, 41 | pattern: &Pattern, 42 | verbatims_begin: &[String], 43 | verbatims_end: &[String], 44 | ) -> Verbatim { 45 | let diff = get_verbatim_diff(line, pattern, verbatims_begin, verbatims_end); 46 | let actual = state.verbatim.actual + diff; 47 | let visual = actual > 0 || state.verbatim.actual > 0; 48 | 49 | if warn && (actual < 0) { 50 | record_line_log( 51 | logs, 52 | Warn, 53 | file, 54 | state.linum_new, 55 | state.linum_old, 56 | line, 57 | "Verbatim count is negative.", 58 | ); 59 | } 60 | 61 | Verbatim { actual, visual } 62 | } 63 | 64 | /// Calculate total verbatim depth change 65 | fn get_verbatim_diff( 66 | line: &str, 67 | pattern: &Pattern, 68 | verbatims_begin: &[String], 69 | verbatims_end: &[String], 70 | ) -> i8 { 71 | if pattern.contains_env_begin 72 | && verbatims_begin.iter().any(|r| line.contains(r)) 73 | { 74 | 1 75 | } else if pattern.contains_env_end 76 | && verbatims_end.iter().any(|r| line.contains(r)) 77 | { 78 | -1 79 | } else { 80 | 0 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/wasm.rs: -------------------------------------------------------------------------------- 1 | //! Web assembly implementation 2 | 3 | use js_sys::{Object, Reflect}; 4 | use merge::Merge; 5 | use std::path::PathBuf; 6 | use wasm_bindgen::prelude::*; 7 | 8 | use crate::args::{Args, OptionArgs}; 9 | use crate::config::get_config_args; 10 | use crate::format::format_file; 11 | use crate::logging::{format_logs, Log}; 12 | 13 | /// Main function for WASM interface with JS 14 | /// 15 | /// # Panics 16 | /// 17 | /// This function panics if the config cannot be parsed 18 | #[wasm_bindgen] 19 | #[must_use] 20 | pub fn main(text: &str, config: &str) -> JsValue { 21 | // Get args 22 | let config = Some((PathBuf::new(), String::new(), config.to_string())); 23 | let mut args: OptionArgs = get_config_args(config).unwrap(); 24 | args.merge(OptionArgs::default()); 25 | let mut args = Args::from(args); 26 | args.stdin = true; 27 | 28 | // Run tex-fmt 29 | let mut logs = Vec::::new(); 30 | args.resolve(&mut logs); 31 | let file = "input"; 32 | let new_text = format_file(text, file, &args, &mut logs); 33 | let logs = format_logs(&mut logs, &args); 34 | 35 | // Wrap into JS object 36 | let js_object = Object::new(); 37 | Reflect::set(&js_object, &"output".into(), &new_text.into()).unwrap(); 38 | Reflect::set(&js_object, &"logs".into(), &logs.into()).unwrap(); 39 | js_object.into() 40 | } 41 | -------------------------------------------------------------------------------- /src/wrap.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for wrapping long lines 2 | 3 | use crate::args::Args; 4 | use crate::comments::find_comment_index; 5 | use crate::format::{Pattern, State}; 6 | use crate::logging::{record_line_log, Log}; 7 | use crate::regexes::VERB; 8 | use log::Level; 9 | use log::LevelFilter; 10 | 11 | /// String slice to start wrapped text lines 12 | pub const TEXT_LINE_START: &str = ""; 13 | /// String slice to start wrapped comment lines 14 | pub const COMMENT_LINE_START: &str = "% "; 15 | 16 | /// Check if a line needs wrapping 17 | #[must_use] 18 | pub fn needs_wrap(line: &str, indent_length: usize, args: &Args) -> bool { 19 | args.wrap && (line.chars().count() + indent_length > args.wraplen.into()) 20 | } 21 | 22 | fn is_wrap_point( 23 | i_byte: usize, 24 | c: char, 25 | prev_c: Option, 26 | inside_verb: bool, 27 | line_len: usize, 28 | args: &Args, 29 | ) -> bool { 30 | // Character c must be a valid wrapping character 31 | args.wrap_chars.contains(&c) 32 | // Must not be preceded by '\' 33 | && prev_c != Some('\\') 34 | // Do not break inside a \verb|...| 35 | && !inside_verb 36 | // No point breaking at the end of the line 37 | && (i_byte + 1 < line_len) 38 | } 39 | 40 | fn get_verb_end(verb_byte_start: Option, line: &str) -> Option { 41 | let verb_len = 6; 42 | verb_byte_start 43 | .map(|v| line[v + verb_len..].find('|').unwrap_or(v) + v + verb_len) 44 | } 45 | 46 | fn is_inside_verb( 47 | i_byte: usize, 48 | contains_verb: bool, 49 | verb_start: Option, 50 | verb_end: Option, 51 | ) -> bool { 52 | if contains_verb { 53 | (verb_start.unwrap() <= i_byte) && (i_byte <= verb_end.unwrap()) 54 | } else { 55 | false 56 | } 57 | } 58 | 59 | /// Find the best place to break a long line. 60 | /// Provided as a *byte* index, not a *char* index. 61 | fn find_wrap_point( 62 | line: &str, 63 | indent_length: usize, 64 | args: &Args, 65 | pattern: &Pattern, 66 | ) -> Option { 67 | let mut wrap_point: Option = None; 68 | let mut prev_c: Option = None; 69 | let contains_verb = pattern.contains_verb && line.contains(VERB); 70 | let verb_start: Option = 71 | contains_verb.then(|| line.find(VERB).unwrap()); 72 | let verb_end = get_verb_end(verb_start, line); 73 | let mut after_non_percent = verb_start == Some(0); 74 | let wrap_boundary = usize::from(args.wrapmin) - indent_length; 75 | let line_len = line.len(); 76 | 77 | for (i_char, (i_byte, c)) in line.char_indices().enumerate() { 78 | if i_char >= wrap_boundary && wrap_point.is_some() { 79 | break; 80 | } 81 | // Special wrapping for lines containing \verb|...| 82 | let inside_verb = 83 | is_inside_verb(i_byte, contains_verb, verb_start, verb_end); 84 | if is_wrap_point(i_byte, c, prev_c, inside_verb, line_len, args) { 85 | if after_non_percent { 86 | // Get index of the byte after which 87 | // line break will be inserted. 88 | // Note this may not be a valid char index. 89 | let wrap_byte = i_byte + c.len_utf8() - 1; 90 | // Don't wrap here if this is the end of the line anyway 91 | if wrap_byte + 1 < line_len { 92 | wrap_point = Some(wrap_byte); 93 | } 94 | } 95 | } else if c != '%' { 96 | after_non_percent = true; 97 | } 98 | prev_c = Some(c); 99 | } 100 | 101 | wrap_point 102 | } 103 | 104 | /// Wrap a long line into a short prefix and a suffix 105 | pub fn apply_wrap<'a>( 106 | line: &'a str, 107 | indent_length: usize, 108 | state: &State, 109 | file: &str, 110 | args: &Args, 111 | logs: &mut Vec, 112 | pattern: &Pattern, 113 | ) -> Option<[&'a str; 3]> { 114 | if args.verbosity == LevelFilter::Trace { 115 | record_line_log( 116 | logs, 117 | Level::Trace, 118 | file, 119 | state.linum_new, 120 | state.linum_old, 121 | line, 122 | "Wrapping long line.", 123 | ); 124 | } 125 | let wrap_point = find_wrap_point(line, indent_length, args, pattern); 126 | let comment_index = find_comment_index(line, pattern); 127 | 128 | match wrap_point { 129 | Some(p) if p <= args.wraplen.into() => {} 130 | _ => { 131 | record_line_log( 132 | logs, 133 | Level::Warn, 134 | file, 135 | state.linum_new, 136 | state.linum_old, 137 | line, 138 | "Line cannot be wrapped.", 139 | ); 140 | } 141 | } 142 | 143 | wrap_point.map(|p| { 144 | let this_line = &line[0..=p]; 145 | let next_line_start = comment_index.map_or("", |c| { 146 | if p > c { 147 | COMMENT_LINE_START 148 | } else { 149 | TEXT_LINE_START 150 | } 151 | }); 152 | let next_line = &line[p + 1..]; 153 | dbg!(&this_line); 154 | dbg!(&next_line); 155 | [this_line, next_line_start, next_line] 156 | }) 157 | } 158 | -------------------------------------------------------------------------------- /src/write.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for writing formatted files 2 | 3 | use crate::args::Args; 4 | use crate::logging::{record_file_log, Log}; 5 | use log::Level::{Error, Info}; 6 | use std::fs; 7 | use std::path; 8 | 9 | /// Write a formatted file to disk 10 | fn write_file(file: &str, text: &str) { 11 | let filepath = path::Path::new(&file).canonicalize().unwrap(); 12 | fs::write(filepath, text).expect("Could not write the file"); 13 | } 14 | 15 | /// Handle the newly formatted file 16 | pub fn process_output( 17 | args: &Args, 18 | file: &str, 19 | text: &str, 20 | new_text: &str, 21 | logs: &mut Vec, 22 | ) -> u8 { 23 | if args.print { 24 | print!("{}", &new_text); 25 | } else if args.check && text != new_text { 26 | record_file_log(logs, Error, file, "Incorrect formatting."); 27 | return 1; 28 | } else if text != new_text { 29 | write_file(file, new_text); 30 | if args.fail_on_change { 31 | record_file_log(logs, Info, file, "Fixed incorrect formatting."); 32 | return 1; 33 | } 34 | } 35 | 0 36 | } 37 | -------------------------------------------------------------------------------- /tests/brackets/source/brackets.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | Matching brackets on a line do nothing (like this). 6 | 7 | Matching brackets on two lines also do nothing (like this 8 | longer example). 9 | 10 | Matching brackets on three lines get an indent (like this 11 | much much longer example 12 | right here on these lines). 13 | 14 | Matching brackets on more lines also get an indent (like this 15 | much much 16 | much much 17 | much longer example 18 | here). 19 | 20 | The brackets could start at the beginning of the line 21 | (so maybe 22 | they look 23 | like this). 24 | 25 | [They could 26 | be any shape 27 | of bracket] 28 | 29 | {Even braces get 30 | the same 31 | indents too} 32 | 33 | What about equations? They are the same: 34 | $(1 + 2 + 3)$ 35 | 36 | $(1 + 2 37 | + 3 + 4 38 | + 5 + 7 39 | + 8 + 9)$ 40 | 41 | And the dollars can go anywhere as expected: 42 | 43 | $ 44 | (1 + 2 45 | + 3 + 4 46 | + 5 + 7 47 | + 8 + 9) 48 | $ 49 | 50 | Note that dollars themselves are not indented 51 | 52 | \end{document} 53 | -------------------------------------------------------------------------------- /tests/brackets/target/brackets.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | Matching brackets on a line do nothing (like this). 6 | 7 | Matching brackets on two lines also do nothing (like this 8 | longer example). 9 | 10 | Matching brackets on three lines get an indent (like this 11 | much much longer example 12 | right here on these lines). 13 | 14 | Matching brackets on more lines also get an indent (like this 15 | much much 16 | much much 17 | much longer example 18 | here). 19 | 20 | The brackets could start at the beginning of the line 21 | (so maybe 22 | they look 23 | like this). 24 | 25 | [They could 26 | be any shape 27 | of bracket] 28 | 29 | {Even braces get 30 | the same 31 | indents too} 32 | 33 | What about equations? They are the same: 34 | $(1 + 2 + 3)$ 35 | 36 | $(1 + 2 37 | + 3 + 4 38 | + 5 + 7 39 | + 8 + 9)$ 40 | 41 | And the dollars can go anywhere as expected: 42 | 43 | $ 44 | (1 + 2 45 | + 3 + 4 46 | + 5 + 7 47 | + 8 + 9) 48 | $ 49 | 50 | Note that dollars themselves are not indented 51 | 52 | \end{document} 53 | -------------------------------------------------------------------------------- /tests/comments/source/comments.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | % Comments should be indented along with other text 6 | (these parentheses 7 | make the middle line here 8 | % and this comment aligns with the text 9 | indented as usual) 10 | 11 | % Comments do not directly affect indenting, 12 | % so they can contain arbitrary brackets (((( 13 | % which may not match. 14 | 15 | % Similarly they might contain \begin{align} 16 | unmatched % environment tags. 17 | 18 | This is a percent sign \% and not a comment 19 | 20 | Some lines might have both \% percents % and comments \end{align} 21 | 22 | \end{document} 23 | -------------------------------------------------------------------------------- /tests/comments/target/comments.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | % Comments should be indented along with other text 6 | (these parentheses 7 | make the middle line here 8 | % and this comment aligns with the text 9 | indented as usual) 10 | 11 | % Comments do not directly affect indenting, 12 | % so they can contain arbitrary brackets (((( 13 | % which may not match. 14 | 15 | % Similarly they might contain \begin{align} 16 | unmatched % environment tags. 17 | 18 | This is a percent sign \% and not a comment 19 | 20 | Some lines might have both \% percents % and comments \end{align} 21 | 22 | \end{document} 23 | -------------------------------------------------------------------------------- /tests/cv/source/cv.tex: -------------------------------------------------------------------------------- 1 | % !TeX program = lualatex 2 | 3 | \documentclass{wgu-cv} 4 | 5 | \yourname{William G Underwood} 6 | \youraddress{ 7 | ORFE Department, 8 | Sherrerd Hall, 9 | Charlton Street, 10 | Princeton, 11 | NJ 08544, 12 | USA 13 | } 14 | \youremail{wgu2@princeton.edu} 15 | \yourwebsite{wgunderwood.github.io} 16 | 17 | \begin{document} 18 | 19 | \maketitle 20 | 21 | \section{Employment} 22 | 23 | \subsection{Postdoctoral Research Associate in Statistics} 24 | {Jul 2024 -- Jul 2026} 25 | \subsubsection{University of Cambridge} 26 | 27 | \begin{itemize} 28 | \item Advisor: Richard Samworth, 29 | Department of Pure Mathematics and Mathematical Statistics 30 | \item Funding: European Research Council Advanced Grant 101019498 31 | \end{itemize} 32 | 33 | \subsection{Assistant in Instruction} 34 | {Sep 2020 -- May 2024} 35 | \subsubsection{Princeton University} 36 | 37 | \begin{itemize} 38 | 39 | \item 40 | ORF 499: 41 | Senior Thesis, 42 | Spring 2024 43 | 44 | \item 45 | ORF 498: 46 | Senior Independent Research Foundations, 47 | Fall 2023 48 | 49 | \item 50 | SML 201: 51 | Introduction to Data Science, 52 | Fall 2023 53 | 54 | \item 55 | ORF 363: 56 | Computing and Optimization, 57 | Spring 2023, Fall 2020 58 | 59 | \item 60 | ORF 524: 61 | Statistical Theory and Methods, 62 | Fall 2022, Fall 2021 63 | 64 | \item 65 | ORF 526: 66 | Probability Theory, 67 | Fall 2022 68 | 69 | \item 70 | ORF 245: 71 | Fundamentals of Statistics, 72 | Spring 2021 73 | 74 | \end{itemize} 75 | 76 | \section{Education} 77 | 78 | \subsection{PhD in Operations Research \& Financial Engineering} 79 | {Sep 2019 -- May 2024} 80 | \subsubsection{Princeton University} 81 | 82 | \begin{itemize} 83 | \item Dissertation: 84 | Estimation and Inference in Modern Nonparametric Statistics 85 | \item Advisor: 86 | Matias Cattaneo, Department of Operations Research \& Financial Engineering 87 | \end{itemize} 88 | 89 | \subsection{MA in Operations Research \& Financial Engineering} 90 | {Sep 2019 -- Sep 2021} 91 | \subsubsection{Princeton University} 92 | 93 | \subsection{MMath in Mathematics \& Statistics} 94 | {Oct 2015 -- Jun 2019} 95 | \subsubsection{University of Oxford} 96 | 97 | \begin{itemize} 98 | \item Dissertation: 99 | Motif-Based Spectral Clustering of Weighted Directed Networks 100 | \item Supervisor: 101 | Mihai Cucuringu, 102 | Department of Statistics 103 | \end{itemize} 104 | 105 | \section{Research \& publications} 106 | 107 | \subsection{Articles}{} 108 | \begin{itemize} 109 | 110 | \item Uniform inference for kernel density estimators with dyadic data, 111 | with M D Cattaneo and Y Feng. 112 | \emph{Journal of the American Statistical Association}, forthcoming, 2024. 113 | \arxiv{2201.05967}. 114 | 115 | \item Motif-based spectral clustering of weighted directed networks, 116 | with A Elliott and M Cucuringu. 117 | \emph{Applied Network Science}, 5(62), 2020. 118 | \arxiv{2004.01293}. 119 | 120 | \item Simple Poisson PCA: an algorithm for (sparse) feature extraction 121 | with simultaneous dimension determination, 122 | with L Smallman and A Artemiou. 123 | \emph{Computational Statistics}, 35:559--577, 2019. 124 | 125 | \end{itemize} 126 | 127 | \subsection{Preprints}{} 128 | \begin{itemize} 129 | 130 | \item Inference with Mondrian random forests, 131 | with M D Cattaneo and J M Klusowski, 2023. \\ 132 | \arxiv{2310.09702}. 133 | 134 | \item Yurinskii's coupling for martingales, 135 | with M D Cattaneo and R P Masini. 136 | \emph{Annals of Statistics}, reject and resubmit, 2023. 137 | \arxiv{2210.00362}. 138 | 139 | \end{itemize} 140 | 141 | \pagebreak 142 | 143 | \subsection{Works in progress}{} 144 | \begin{itemize} 145 | 146 | \item Higher-order extensions to the Lindeberg method, 147 | with M D Cattaneo and R P Masini. 148 | 149 | \item Adaptive Mondrian random forests, 150 | with M D Cattaneo, R Chandak and J M Klusowski. 151 | \end{itemize} 152 | 153 | \subsection{Presentations}{} 154 | \begin{itemize} 155 | 156 | \item Statistics Seminar, University of Pittsburgh, February 2024 157 | \item Statistics Seminar, University of Illinois, January 2024 158 | \item Statistics Seminar, University of Michigan, January 2024 159 | \item PhD Poster Session, Two Sigma Investments, July 2023 160 | \item Research Symposium, Two Sigma Investments, June 2022 161 | \item Statistics Laboratory, Princeton University, September 2021 162 | \end{itemize} 163 | 164 | \subsection{Software}{} 165 | \begin{itemize} 166 | 167 | \item MondrianForests: Mondrian random forests in Julia, 2023. \\ 168 | \github{wgunderwood/MondrianForests.jl} 169 | 170 | \item DyadicKDE: dyadic kernel density estimation in Julia, 2022. \\ 171 | \github{wgunderwood/DyadicKDE.jl} 172 | 173 | \item motifcluster: motif-based spectral clustering 174 | in R, Python and Julia, 2020. \\ 175 | \github{wgunderwood/motifcluster} 176 | 177 | \end{itemize} 178 | 179 | \section{Awards \& funding} 180 | \vspace{-0.22cm} 181 | 182 | \begin{itemize} 183 | \item School of Engineering and Applied Science Award for Excellence, 184 | Princeton University 185 | \hfill 2022% 186 | \item Francis Robbins Upton Fellowship in Engineering, 187 | Princeton University 188 | \hfill 2019% 189 | \item Royal Statistical Society Prize, 190 | Royal Statistical Society \& University of Oxford 191 | \hfill 2019% 192 | \item Gibbs Statistics Prize, 193 | University of Oxford 194 | \hfill 2019% 195 | \item James Fund for Mathematics Research Grant, 196 | St John's College, University of Oxford 197 | \hfill 2017% 198 | \item Casberd Scholarship, 199 | St John's College, University of Oxford 200 | \hfill 2016% 201 | \end{itemize} 202 | 203 | \section{Professional experience} 204 | 205 | \subsection{Quantitative Research Intern} 206 | {Jun 2023 -- Aug 2023} 207 | \subsubsection{Two Sigma Investments} 208 | \vspace{-0.20cm} 209 | 210 | \subsection{Machine Learning Consultant} 211 | {Oct 2018 -- Nov 2018} 212 | \subsubsection{Mercury Digital Assets} 213 | \vspace{-0.18cm} 214 | 215 | \subsection{Educational Consultant} 216 | {Feb 2018 -- Sep 2018} 217 | \subsubsection{Polaris \& Dawn} 218 | \vspace{-0.20cm} 219 | 220 | \subsection{Premium Tutor} 221 | {Feb 2016 -- Oct 2018} 222 | \subsubsection{MyTutor} 223 | \vspace{-0.20cm} 224 | 225 | \subsection{Statistics \& Machine Learning Researcher} 226 | {Aug 2017 -- Sep 2017} 227 | \subsubsection{Cardiff University} 228 | \vspace{-0.20cm} 229 | 230 | \subsection{Data Science Intern} 231 | {Jun 2017 -- Aug 2017} 232 | \subsubsection{Rolls-Royce} 233 | \vspace{-0.20cm} 234 | 235 | \subsection{Peer review}{} 236 | 237 | \emph{Econometric Theory, 238 | Journal of the American Statistical Association, 239 | Journal of Business \& Economic Statistics, 240 | Journal of Causal Inference, 241 | Journal of Econometrics, 242 | Operations Research.} 243 | 244 | \section{References} 245 | \vspace{-0.22cm} 246 | 247 | \begin{itemize} 248 | 249 | \item 250 | Matias Cattaneo, 251 | Professor, 252 | ORFE, 253 | Princeton University 254 | 255 | \item 256 | Jason Klusowski, 257 | Assistant Professor, 258 | ORFE, 259 | Princeton University 260 | 261 | \item 262 | Jianqing Fan, 263 | Professor, 264 | ORFE, 265 | Princeton University 266 | 267 | \item 268 | Ricardo Masini, 269 | Assistant Professor, 270 | Statistics, 271 | University of California, Davis 272 | 273 | \end{itemize} 274 | 275 | \end{document} 276 | -------------------------------------------------------------------------------- /tests/cv/source/wgu-cv.cls: -------------------------------------------------------------------------------- 1 | %! TeX root = WGUnderwood.tex 2 | 3 | % class 4 | \NeedsTeXFormat{LaTeX2e} 5 | \ProvidesClass{wgu-cv} 6 | 7 | % packages 8 | \LoadClass[10pt]{article} 9 | \RequirePackage[margin=1in,top=0.9in]{geometry} 10 | \RequirePackage{hyperref} 11 | %\RequirePackage{fontspec} 12 | \RequirePackage{microtype} 13 | \RequirePackage{fancyhdr} 14 | \RequirePackage{enumitem} 15 | \RequirePackage{ifthen} 16 | 17 | % variables 18 | \def\yourname#1{\def\@yourname{#1}} 19 | \def\youraddress#1{\def\@youraddress{#1}} 20 | \def\youremail#1{\def\@youremail{#1}} 21 | \def\yourwebsite#1{\def\@yourwebsite{#1}} 22 | 23 | % settings 24 | %\setmainfont{Libre Baskerville}[Scale=0.9] 25 | %\setmonofont{Source Code Pro}[Scale=0.97] 26 | \geometry{a4paper} 27 | \setlength\parindent{0pt} 28 | \bibliographystyle{abbrvnat} 29 | \pagestyle{fancy} 30 | \renewcommand{\headrulewidth}{0pt} 31 | \cfoot{\thepage} 32 | \rfoot{\today} 33 | \setlist{ 34 | leftmargin=0.5cm, 35 | topsep=0cm, 36 | partopsep=0cm, 37 | parsep=-0.04cm, % item spacing 38 | before=\vspace{0.12cm}, 39 | after=\vspace{0.08cm}, 40 | } 41 | 42 | % arxiv 43 | \newcommand{\arxiv}[1]{% 44 | \href{https://arxiv.org/abs/#1}{% 45 | \texttt{arXiv{:}{\allowbreak}#1}}% 46 | } 47 | 48 | % github 49 | \newcommand{\github}[1]{% 50 | GitHub: \href{https://github.com/#1}{% 51 | \texttt{#1}}% 52 | } 53 | 54 | % title 55 | \renewcommand{\maketitle}{% 56 | \vspace*{-1.2cm}% 57 | \begin{center}% 58 | \begin{huge}% 59 | \@yourname \\ 60 | \end{huge}% 61 | \vspace{0.5cm}% 62 | \@youraddress \\ 63 | \vspace{0.16cm}% 64 | \begin{minipage}{0.45\textwidth}% 65 | \centering% 66 | \href{mailto:\@youremail}{\nolinkurl{\@youremail}}% 67 | \end{minipage}% 68 | \begin{minipage}{0.45\textwidth}% 69 | \centering% 70 | \href{https://\@yourwebsite}{\nolinkurl{\@yourwebsite}}% 71 | \end{minipage} 72 | \end{center}% 73 | } 74 | 75 | % section 76 | \renewcommand{\section}[1]{% 77 | \vspace{0.3cm}% 78 | \par\hbox{\large\textbf{#1}\strut}% 79 | \vspace{-0.25cm}% 80 | \rule{\textwidth}{0.8pt}% 81 | \vspace{-0.15cm}% 82 | } 83 | 84 | % subsection 85 | \renewcommand{\subsection}[2]{% 86 | \vspace{0.30cm}% 87 | \textbf{#1}% 88 | \hfill{#2}% 89 | \vspace{0.03cm}% 90 | } 91 | 92 | % subsubsection 93 | \renewcommand{\subsubsection}[1]{% 94 | \linebreak 95 | \textit{#1}% 96 | \vspace{0.05cm}% 97 | } 98 | -------------------------------------------------------------------------------- /tests/cv/target/cv.tex: -------------------------------------------------------------------------------- 1 | % !TeX program = lualatex 2 | 3 | \documentclass{wgu-cv} 4 | 5 | \yourname{William G Underwood} 6 | \youraddress{ 7 | ORFE Department, 8 | Sherrerd Hall, 9 | Charlton Street, 10 | Princeton, 11 | NJ 08544, 12 | USA 13 | } 14 | \youremail{wgu2@princeton.edu} 15 | \yourwebsite{wgunderwood.github.io} 16 | 17 | \begin{document} 18 | 19 | \maketitle 20 | 21 | \section{Employment} 22 | 23 | \subsection{Postdoctoral Research Associate in Statistics} 24 | {Jul 2024 -- Jul 2026} 25 | \subsubsection{University of Cambridge} 26 | 27 | \begin{itemize} 28 | \item Advisor: Richard Samworth, 29 | Department of Pure Mathematics and Mathematical Statistics 30 | \item Funding: European Research Council Advanced Grant 101019498 31 | \end{itemize} 32 | 33 | \subsection{Assistant in Instruction} 34 | {Sep 2020 -- May 2024} 35 | \subsubsection{Princeton University} 36 | 37 | \begin{itemize} 38 | 39 | \item 40 | ORF 499: 41 | Senior Thesis, 42 | Spring 2024 43 | 44 | \item 45 | ORF 498: 46 | Senior Independent Research Foundations, 47 | Fall 2023 48 | 49 | \item 50 | SML 201: 51 | Introduction to Data Science, 52 | Fall 2023 53 | 54 | \item 55 | ORF 363: 56 | Computing and Optimization, 57 | Spring 2023, Fall 2020 58 | 59 | \item 60 | ORF 524: 61 | Statistical Theory and Methods, 62 | Fall 2022, Fall 2021 63 | 64 | \item 65 | ORF 526: 66 | Probability Theory, 67 | Fall 2022 68 | 69 | \item 70 | ORF 245: 71 | Fundamentals of Statistics, 72 | Spring 2021 73 | 74 | \end{itemize} 75 | 76 | \section{Education} 77 | 78 | \subsection{PhD in Operations Research \& Financial Engineering} 79 | {Sep 2019 -- May 2024} 80 | \subsubsection{Princeton University} 81 | 82 | \begin{itemize} 83 | \item Dissertation: 84 | Estimation and Inference in Modern Nonparametric Statistics 85 | \item Advisor: 86 | Matias Cattaneo, Department of Operations Research \& Financial Engineering 87 | \end{itemize} 88 | 89 | \subsection{MA in Operations Research \& Financial Engineering} 90 | {Sep 2019 -- Sep 2021} 91 | \subsubsection{Princeton University} 92 | 93 | \subsection{MMath in Mathematics \& Statistics} 94 | {Oct 2015 -- Jun 2019} 95 | \subsubsection{University of Oxford} 96 | 97 | \begin{itemize} 98 | \item Dissertation: 99 | Motif-Based Spectral Clustering of Weighted Directed Networks 100 | \item Supervisor: 101 | Mihai Cucuringu, 102 | Department of Statistics 103 | \end{itemize} 104 | 105 | \section{Research \& publications} 106 | 107 | \subsection{Articles}{} 108 | \begin{itemize} 109 | 110 | \item Uniform inference for kernel density estimators with dyadic data, 111 | with M D Cattaneo and Y Feng. 112 | \emph{Journal of the American Statistical Association}, forthcoming, 2024. 113 | \arxiv{2201.05967}. 114 | 115 | \item Motif-based spectral clustering of weighted directed networks, 116 | with A Elliott and M Cucuringu. 117 | \emph{Applied Network Science}, 5(62), 2020. 118 | \arxiv{2004.01293}. 119 | 120 | \item Simple Poisson PCA: an algorithm for (sparse) feature extraction 121 | with simultaneous dimension determination, 122 | with L Smallman and A Artemiou. 123 | \emph{Computational Statistics}, 35:559--577, 2019. 124 | 125 | \end{itemize} 126 | 127 | \subsection{Preprints}{} 128 | \begin{itemize} 129 | 130 | \item Inference with Mondrian random forests, 131 | with M D Cattaneo and J M Klusowski, 2023. \\ 132 | \arxiv{2310.09702}. 133 | 134 | \item Yurinskii's coupling for martingales, 135 | with M D Cattaneo and R P Masini. 136 | \emph{Annals of Statistics}, reject and resubmit, 2023. 137 | \arxiv{2210.00362}. 138 | 139 | \end{itemize} 140 | 141 | \pagebreak 142 | 143 | \subsection{Works in progress}{} 144 | \begin{itemize} 145 | 146 | \item Higher-order extensions to the Lindeberg method, 147 | with M D Cattaneo and R P Masini. 148 | 149 | \item Adaptive Mondrian random forests, 150 | with M D Cattaneo, R Chandak and J M Klusowski. 151 | \end{itemize} 152 | 153 | \subsection{Presentations}{} 154 | \begin{itemize} 155 | 156 | \item Statistics Seminar, University of Pittsburgh, February 2024 157 | \item Statistics Seminar, University of Illinois, January 2024 158 | \item Statistics Seminar, University of Michigan, January 2024 159 | \item PhD Poster Session, Two Sigma Investments, July 2023 160 | \item Research Symposium, Two Sigma Investments, June 2022 161 | \item Statistics Laboratory, Princeton University, September 2021 162 | \end{itemize} 163 | 164 | \subsection{Software}{} 165 | \begin{itemize} 166 | 167 | \item MondrianForests: Mondrian random forests in Julia, 2023. \\ 168 | \github{wgunderwood/MondrianForests.jl} 169 | 170 | \item DyadicKDE: dyadic kernel density estimation in Julia, 2022. \\ 171 | \github{wgunderwood/DyadicKDE.jl} 172 | 173 | \item motifcluster: motif-based spectral clustering 174 | in R, Python and Julia, 2020. \\ 175 | \github{wgunderwood/motifcluster} 176 | 177 | \end{itemize} 178 | 179 | \section{Awards \& funding} 180 | \vspace{-0.22cm} 181 | 182 | \begin{itemize} 183 | \item School of Engineering and Applied Science Award for Excellence, 184 | Princeton University 185 | \hfill 2022% 186 | \item Francis Robbins Upton Fellowship in Engineering, 187 | Princeton University 188 | \hfill 2019% 189 | \item Royal Statistical Society Prize, 190 | Royal Statistical Society \& University of Oxford 191 | \hfill 2019% 192 | \item Gibbs Statistics Prize, 193 | University of Oxford 194 | \hfill 2019% 195 | \item James Fund for Mathematics Research Grant, 196 | St John's College, University of Oxford 197 | \hfill 2017% 198 | \item Casberd Scholarship, 199 | St John's College, University of Oxford 200 | \hfill 2016% 201 | \end{itemize} 202 | 203 | \section{Professional experience} 204 | 205 | \subsection{Quantitative Research Intern} 206 | {Jun 2023 -- Aug 2023} 207 | \subsubsection{Two Sigma Investments} 208 | \vspace{-0.20cm} 209 | 210 | \subsection{Machine Learning Consultant} 211 | {Oct 2018 -- Nov 2018} 212 | \subsubsection{Mercury Digital Assets} 213 | \vspace{-0.18cm} 214 | 215 | \subsection{Educational Consultant} 216 | {Feb 2018 -- Sep 2018} 217 | \subsubsection{Polaris \& Dawn} 218 | \vspace{-0.20cm} 219 | 220 | \subsection{Premium Tutor} 221 | {Feb 2016 -- Oct 2018} 222 | \subsubsection{MyTutor} 223 | \vspace{-0.20cm} 224 | 225 | \subsection{Statistics \& Machine Learning Researcher} 226 | {Aug 2017 -- Sep 2017} 227 | \subsubsection{Cardiff University} 228 | \vspace{-0.20cm} 229 | 230 | \subsection{Data Science Intern} 231 | {Jun 2017 -- Aug 2017} 232 | \subsubsection{Rolls-Royce} 233 | \vspace{-0.20cm} 234 | 235 | \subsection{Peer review}{} 236 | 237 | \emph{Econometric Theory, 238 | Journal of the American Statistical Association, 239 | Journal of Business \& Economic Statistics, 240 | Journal of Causal Inference, 241 | Journal of Econometrics, 242 | Operations Research.} 243 | 244 | \section{References} 245 | \vspace{-0.22cm} 246 | 247 | \begin{itemize} 248 | 249 | \item 250 | Matias Cattaneo, 251 | Professor, 252 | ORFE, 253 | Princeton University 254 | 255 | \item 256 | Jason Klusowski, 257 | Assistant Professor, 258 | ORFE, 259 | Princeton University 260 | 261 | \item 262 | Jianqing Fan, 263 | Professor, 264 | ORFE, 265 | Princeton University 266 | 267 | \item 268 | Ricardo Masini, 269 | Assistant Professor, 270 | Statistics, 271 | University of California, Davis 272 | 273 | \end{itemize} 274 | 275 | \end{document} 276 | -------------------------------------------------------------------------------- /tests/cv/target/wgu-cv.cls: -------------------------------------------------------------------------------- 1 | %! TeX root = WGUnderwood.tex 2 | 3 | % class 4 | \NeedsTeXFormat{LaTeX2e} 5 | \ProvidesClass{wgu-cv} 6 | 7 | % packages 8 | \LoadClass[10pt]{article} 9 | \RequirePackage[margin=1in,top=0.9in]{geometry} 10 | \RequirePackage{hyperref} 11 | %\RequirePackage{fontspec} 12 | \RequirePackage{microtype} 13 | \RequirePackage{fancyhdr} 14 | \RequirePackage{enumitem} 15 | \RequirePackage{ifthen} 16 | 17 | % variables 18 | \def\yourname#1{\def\@yourname{#1}} 19 | \def\youraddress#1{\def\@youraddress{#1}} 20 | \def\youremail#1{\def\@youremail{#1}} 21 | \def\yourwebsite#1{\def\@yourwebsite{#1}} 22 | 23 | % settings 24 | %\setmainfont{Libre Baskerville}[Scale=0.9] 25 | %\setmonofont{Source Code Pro}[Scale=0.97] 26 | \geometry{a4paper} 27 | \setlength\parindent{0pt} 28 | \bibliographystyle{abbrvnat} 29 | \pagestyle{fancy} 30 | \renewcommand{\headrulewidth}{0pt} 31 | \cfoot{\thepage} 32 | \rfoot{\today} 33 | \setlist{ 34 | leftmargin=0.5cm, 35 | topsep=0cm, 36 | partopsep=0cm, 37 | parsep=-0.04cm, % item spacing 38 | before=\vspace{0.12cm}, 39 | after=\vspace{0.08cm}, 40 | } 41 | 42 | % arxiv 43 | \newcommand{\arxiv}[1]{% 44 | \href{https://arxiv.org/abs/#1}{% 45 | \texttt{arXiv{:}{\allowbreak}#1}}% 46 | } 47 | 48 | % github 49 | \newcommand{\github}[1]{% 50 | GitHub: \href{https://github.com/#1}{% 51 | \texttt{#1}}% 52 | } 53 | 54 | % title 55 | \renewcommand{\maketitle}{% 56 | \vspace*{-1.2cm}% 57 | \begin{center}% 58 | \begin{huge}% 59 | \@yourname \\ 60 | \end{huge}% 61 | \vspace{0.5cm}% 62 | \@youraddress \\ 63 | \vspace{0.16cm}% 64 | \begin{minipage}{0.45\textwidth}% 65 | \centering% 66 | \href{mailto:\@youremail}{\nolinkurl{\@youremail}}% 67 | \end{minipage}% 68 | \begin{minipage}{0.45\textwidth}% 69 | \centering% 70 | \href{https://\@yourwebsite}{\nolinkurl{\@yourwebsite}}% 71 | \end{minipage} 72 | \end{center}% 73 | } 74 | 75 | % section 76 | \renewcommand{\section}[1]{% 77 | \vspace{0.3cm}% 78 | \par\hbox{\large\textbf{#1}\strut}% 79 | \vspace{-0.25cm}% 80 | \rule{\textwidth}{0.8pt}% 81 | \vspace{-0.15cm}% 82 | } 83 | 84 | % subsection 85 | \renewcommand{\subsection}[2]{% 86 | \vspace{0.30cm}% 87 | \textbf{#1}% 88 | \hfill{#2}% 89 | \vspace{0.03cm}% 90 | } 91 | 92 | % subsubsection 93 | \renewcommand{\subsubsection}[1]{% 94 | \linebreak 95 | \textit{#1}% 96 | \vspace{0.05cm}% 97 | } 98 | -------------------------------------------------------------------------------- /tests/empty/source/empty.tex: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/empty/target/empty.tex: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/environments/source/document.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | Documents should not be globally indented. 6 | 7 | \end{document} 8 | -------------------------------------------------------------------------------- /tests/environments/source/environment_lines.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | \newenvironment{env1}{}{} 6 | \newenvironment{env2}{}{} 7 | \newenvironment{env3}{}{} 8 | \newenvironment{env4}{}{} 9 | 10 | % environments on separate lines 11 | \begin{env1} 12 | \begin{env2} 13 | \end{env2} 14 | \end{env1} 15 | 16 | % environments on shared lines 17 | \begin{env1}\begin{env2} 18 | \end{env2}\end{env1} 19 | 20 | % environments on shared lines with spaces 21 | \begin{env1} \begin{env2} 22 | \end{env2} \end{env1} 23 | 24 | % environments all on same line 25 | \begin{env1}\begin{env2}\end{env2}\end{env1} % with a comment \begin{env1} 26 | 27 | % environments with extra brackets 28 | \begin{env1}(a)(b \begin{env2}[c{d}e] \end{env2}[f]g)\end{env1} 29 | 30 | % environments and a long line 31 | \begin{env1}\begin{env2}\begin{env3}\begin{env4}\end{env4}\end{env3}\end{env2}\end{env1} 32 | 33 | \end{document} 34 | -------------------------------------------------------------------------------- /tests/environments/target/document.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | Documents should not be globally indented. 6 | 7 | \end{document} 8 | -------------------------------------------------------------------------------- /tests/environments/target/environment_lines.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | \newenvironment{env1}{}{} 6 | \newenvironment{env2}{}{} 7 | \newenvironment{env3}{}{} 8 | \newenvironment{env4}{}{} 9 | 10 | % environments on separate lines 11 | \begin{env1} 12 | \begin{env2} 13 | \end{env2} 14 | \end{env1} 15 | 16 | % environments on shared lines 17 | \begin{env1} 18 | \begin{env2} 19 | \end{env2} 20 | \end{env1} 21 | 22 | % environments on shared lines with spaces 23 | \begin{env1} 24 | \begin{env2} 25 | \end{env2} 26 | \end{env1} 27 | 28 | % environments all on same line 29 | \begin{env1} 30 | \begin{env2} 31 | \end{env2} 32 | \end{env1} % with a comment \begin{env1} 33 | 34 | % environments with extra brackets 35 | \begin{env1}(a)(b 36 | \begin{env2}[c{d}e] 37 | \end{env2}[f]g) 38 | \end{env1} 39 | 40 | % environments and a long line 41 | \begin{env1} 42 | \begin{env2} 43 | \begin{env3} 44 | \begin{env4} 45 | \end{env4} 46 | \end{env3} 47 | \end{env2} 48 | \end{env1} 49 | 50 | \end{document} 51 | -------------------------------------------------------------------------------- /tests/higher_categories_thesis/source/quiver.sty: -------------------------------------------------------------------------------- 1 | % *** quiver *** 2 | % A package for drawing commutative diagrams exported from https://q.uiver.app. 3 | % 4 | % This package is currently a wrapper around the `tikz-cd` package, importing necessary TikZ 5 | % libraries, and defining a new TikZ style for curves of a fixed height. 6 | % 7 | % Version: 1.4.2 8 | % Authors: 9 | % - varkor (https://github.com/varkor) 10 | % - AndréC (https://tex.stackexchange.com/users/138900/andr%C3%A9c) 11 | 12 | \NeedsTeXFormat{LaTeX2e} 13 | \ProvidesPackage{quiver}[2021/01/11 quiver] 14 | 15 | % `tikz-cd` is necessary to draw commutative diagrams. 16 | \RequirePackage{tikz-cd} 17 | % `amssymb` is necessary for `\lrcorner` and `\ulcorner`. 18 | \RequirePackage{amssymb} 19 | % `calc` is necessary to draw curved arrows. 20 | \usetikzlibrary{calc} 21 | % `pathmorphing` is necessary to draw squiggly arrows. 22 | \usetikzlibrary{decorations.pathmorphing} 23 | 24 | % A TikZ style for curved arrows of a fixed height, due to AndréC. 25 | \tikzset{curve/.style={settings={#1},to path={(\tikztostart) 26 | .. controls ($(\tikztostart)!\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$) % tex-fmt: skip 27 | and ($(\tikztostart)!1-\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$) % tex-fmt: skip 28 | .. (\tikztotarget)\tikztonodes}}, 29 | settings/.code={\tikzset{quiver/.cd,#1} 30 | \def\pv##1{\pgfkeysvalueof{/tikz/quiver/##1}}}, 31 | quiver/.cd,pos/.initial=0.35,height/.initial=0} 32 | 33 | % TikZ arrowhead/tail styles. 34 | \tikzset{tail reversed/.code={\pgfsetarrowsstart{tikzcd to}}} 35 | \tikzset{2tail/.code={\pgfsetarrowsstart{Implies[reversed]}}} 36 | \tikzset{2tail reversed/.code={\pgfsetarrowsstart{Implies}}} 37 | % TikZ arrow styles. 38 | \tikzset{no body/.style={/tikz/dash pattern=on 0 off 1mm}} 39 | 40 | \endinput 41 | -------------------------------------------------------------------------------- /tests/higher_categories_thesis/target/quiver.sty: -------------------------------------------------------------------------------- 1 | % *** quiver *** 2 | % A package for drawing commutative diagrams exported from https://q.uiver.app. 3 | % 4 | % This package is currently a wrapper around the `tikz-cd` package, 5 | % importing necessary TikZ 6 | % libraries, and defining a new TikZ style for curves of a fixed height. 7 | % 8 | % Version: 1.4.2 9 | % Authors: 10 | % - varkor (https://github.com/varkor) 11 | % - AndréC (https://tex.stackexchange.com/users/138900/andr%C3%A9c) 12 | 13 | \NeedsTeXFormat{LaTeX2e} 14 | \ProvidesPackage{quiver}[2021/01/11 quiver] 15 | 16 | % `tikz-cd` is necessary to draw commutative diagrams. 17 | \RequirePackage{tikz-cd} 18 | % `amssymb` is necessary for `\lrcorner` and `\ulcorner`. 19 | \RequirePackage{amssymb} 20 | % `calc` is necessary to draw curved arrows. 21 | \usetikzlibrary{calc} 22 | % `pathmorphing` is necessary to draw squiggly arrows. 23 | \usetikzlibrary{decorations.pathmorphing} 24 | 25 | % A TikZ style for curved arrows of a fixed height, due to AndréC. 26 | \tikzset{curve/.style={settings={#1},to path={(\tikztostart) 27 | .. controls ($(\tikztostart)!\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$) % tex-fmt: skip 28 | and ($(\tikztostart)!1-\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$) % tex-fmt: skip 29 | .. (\tikztotarget)\tikztonodes}}, 30 | settings/.code={\tikzset{quiver/.cd,#1} 31 | \def\pv##1{\pgfkeysvalueof{/tikz/quiver/##1}}}, 32 | quiver/.cd,pos/.initial=0.35,height/.initial=0} 33 | 34 | % TikZ arrowhead/tail styles. 35 | \tikzset{tail reversed/.code={\pgfsetarrowsstart{tikzcd to}}} 36 | \tikzset{2tail/.code={\pgfsetarrowsstart{Implies[reversed]}}} 37 | \tikzset{2tail reversed/.code={\pgfsetarrowsstart{Implies}}} 38 | % TikZ arrow styles. 39 | \tikzset{no body/.style={/tikz/dash pattern=on 0 off 1mm}} 40 | 41 | \endinput 42 | -------------------------------------------------------------------------------- /tests/ignore/source/ignore.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | Lines which end with the ignore keyword are not indented or wrapped even if they are long % tex-fmt: skip 6 | 7 | % tex-fmt: off 8 | It is also possible to ignore blocks 9 | of lines together and not indent them 10 | even like this 11 | % tex-fmt: on 12 | 13 | Not ignored 14 | 15 | % tex-fmt: on 16 | 17 | Not ignored 18 | 19 | % tex-fmt: off 20 | 21 | Ignored 22 | 23 | % tex-fmt: off 24 | 25 | Ignored 26 | 27 | % tex-fmt: on 28 | 29 | Not ignored 30 | 31 | % tex-fmt: off 32 | 33 | Ignored 34 | 35 | \end{document} 36 | -------------------------------------------------------------------------------- /tests/ignore/target/ignore.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | Lines which end with the ignore keyword are not indented or wrapped even if they are long % tex-fmt: skip 6 | 7 | % tex-fmt: off 8 | It is also possible to ignore blocks 9 | of lines together and not indent them 10 | even like this 11 | % tex-fmt: on 12 | 13 | Not ignored 14 | 15 | % tex-fmt: on 16 | 17 | Not ignored 18 | 19 | % tex-fmt: off 20 | 21 | Ignored 22 | 23 | % tex-fmt: off 24 | 25 | Ignored 26 | 27 | % tex-fmt: on 28 | 29 | Not ignored 30 | 31 | % tex-fmt: off 32 | 33 | Ignored 34 | 35 | \end{document} 36 | -------------------------------------------------------------------------------- /tests/linear_map_chinese/tex-fmt.toml: -------------------------------------------------------------------------------- 1 | wrap-chars = [",", "。", ":", ";", "?"] 2 | -------------------------------------------------------------------------------- /tests/lists/source/lists.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | \begin{itemize} 6 | 7 | \item Lists with items on one line 8 | 9 | \item Lists with items 10 | on multiple lines 11 | 12 | % comments before a list item 13 | \item Another item 14 | 15 | \item Another item 16 | % comments inside a list item 17 | Or even just % trailing comments 18 | 19 | \item Every \item should start \item a new line 20 | 21 | \end{itemize} 22 | 23 | \begin{myitemize} 24 | 25 | \item Custom list environments can be specified in the configuration file. 26 | 27 | \end{myitemize} 28 | 29 | Commands such as itemsep should not be affected. 30 | \setlength{\itemsep}{0pt} 31 | 32 | \end{document} 33 | -------------------------------------------------------------------------------- /tests/lists/target/lists.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | \begin{itemize} 6 | 7 | \item Lists with items on one line 8 | 9 | \item Lists with items 10 | on multiple lines 11 | 12 | % comments before a list item 13 | \item Another item 14 | 15 | \item Another item 16 | % comments inside a list item 17 | Or even just % trailing comments 18 | 19 | \item Every 20 | \item should start 21 | \item a new line 22 | 23 | \end{itemize} 24 | 25 | \begin{myitemize} 26 | 27 | \item Custom list environments can be specified in the configuration file. 28 | 29 | \end{myitemize} 30 | 31 | Commands such as itemsep should not be affected. 32 | \setlength{\itemsep}{0pt} 33 | 34 | \end{document} 35 | -------------------------------------------------------------------------------- /tests/lists/tex-fmt.toml: -------------------------------------------------------------------------------- 1 | lists = ["myitemize"] 2 | -------------------------------------------------------------------------------- /tests/masters_dissertation/source/ociamthesis.cls: -------------------------------------------------------------------------------- 1 | % ociamthesis v2.2 2 | % By Keith A. Gillow 3 | % Version 1.0 released 26/11/1997 4 | %-------------------------- identification --------------------- 5 | \NeedsTeXFormat{LaTeX2e} 6 | \ProvidesClass{ociamthesis}[2010/11/22 v2.2 OCIAM thesis class] 7 | %-------------------------- initial code ----------------------- 8 | \def\logoversion{squarelogo} 9 | \DeclareOption{beltcrest}{\def\logoversion{beltcrest}} 10 | \DeclareOption{shieldcrest}{\def\logoversion{shieldcrest}} 11 | \DeclareOption*{\PassOptionsToClass{\CurrentOption}{report}} 12 | \ProcessOptions\relax 13 | \LoadClass[a4paper]{report} 14 | % As an alternative to the above could use next line for twosided output 15 | %\LoadClass[a4paper,twoside,openright]{report} 16 | 17 | \RequirePackage{graphicx} % needed for latest frontpage logo 18 | \RequirePackage{ifthen} % needed for option parsing for logo 19 | 20 | \raggedbottom 21 | 22 | %define the default submitted text 23 | \newcommand{\submittedtext}{{A thesis submitted for the degree of}} 24 | 25 | % 26 | % DECLARATIONS 27 | % 28 | % These macros are used to declare arguments needed for the 29 | % construction of the title page and other preamble. 30 | 31 | % The year and term the thesis is submitted 32 | \def\degreedate#1{\gdef\@degreedate{#1}} 33 | % The full (unabbreviated) name of the degree 34 | \def\degree#1{\gdef\@degree{#1}} 35 | % The name of your Oxford college (e.g. Christ Church, Pembroke) 36 | \def\college#1{\gdef\@college{#1}} 37 | 38 | 39 | % 40 | % Setup chosen crest/logo 41 | % 42 | 43 | \ifthenelse{\equal{\logoversion}{shieldcrest}}% 44 | { 45 | % Traditional Oxford shield crest 46 | %Using latex metafont (Mathematical Institute system) 47 | \font\crestfont=oxcrest40 scaled\magstep3 48 | \def\logo{{\crestfont \char1}} 49 | %For comlab system replace 1st line above with 50 | %\font\crestfont=crest scaled\magstep3 51 | }{} 52 | 53 | \ifthenelse{\equal{\logoversion}{beltcrest}}% 54 | { 55 | % Newer Oxford Belt crest 56 | %Using latex metafont (Mathematical Institute system) 57 | \font\beltcrestfont=oxbeltcrest 58 | \def\logo{{\beltcrestfont \char0}} 59 | %For comlab system replace 1st line above with 60 | %\font\beltcrestfont=newcrest 61 | }{} 62 | 63 | \ifthenelse{\equal{\logoversion}{squarelogo}}% 64 | { 65 | % Latest Logo, Square version (the default!) 66 | % you need an oxlogo.eps or oxlogo.pdf file as appropriate 67 | \def\logo{{ 68 | %\includegraphics[width=32mm,draft=false]{../graphics/branding/oxlogo} 69 | }} 70 | }{} 71 | 72 | % 73 | % Define text area of page and margin offsets 74 | % 75 | \setlength{\topmargin}{0.0in} %0.0in 76 | \setlength{\oddsidemargin}{0.167in} % 0.33in 77 | \setlength{\evensidemargin}{-0.08in} %-0.08in 78 | \setlength{\textheight}{9.2in} %9.0in 79 | \setlength{\textwidth}{6.0in} %6.0in 80 | \setlength{\headheight}{15pt} % not set 81 | \setlength{\voffset}{-0.2in} % not set 82 | 83 | % 84 | % Environments 85 | % 86 | 87 | % This macro define an environment for front matter that is always 88 | % single column even in a double-column document. 89 | 90 | \newenvironment{alwayssingle}{% 91 | \@restonecolfalse 92 | \if@twocolumn\@restonecoltrue\onecolumn 93 | \else\if@openright\cleardoublepage\else\clearpage\fi 94 | \fi}% 95 | {\if@restonecol\twocolumn 96 | \else\newpage\thispagestyle{empty}\fi} 97 | 98 | %define title page layout 99 | \renewcommand{\maketitle}{% 100 | \begin{alwayssingle} 101 | \renewcommand{\footnotesize}{\small} 102 | \renewcommand{\footnoterule}{\relax} 103 | \thispagestyle{empty} 104 | \null\vfill 105 | \begin{center} 106 | { \Huge {\bfseries {\@title}} \par} 107 | {\large \vspace*{40mm} {\logo \par} \vspace*{25mm}} 108 | {{\Large \@author} \par} 109 | {\large \vspace*{1.5ex} % 1ex 110 | {{\@college} \par} 111 | \vspace*{1ex} 112 | {University of Oxford \par} 113 | \vspace*{25mm} 114 | {{\submittedtext} \par} 115 | \vspace*{1ex} 116 | {\it {\@degree} \par} 117 | \vspace*{2ex} 118 | {\@degreedate}} 119 | \end{center} 120 | \null\vfill 121 | \end{alwayssingle}} 122 | 123 | % DEDICATION 124 | % 125 | % The dedication environment makes sure the dedication gets its 126 | % own page and is set out in verse format. 127 | 128 | \newenvironment{dedication} 129 | {\begin{alwayssingle} 130 | \thispagestyle{empty} 131 | \begin{center} 132 | \vspace*{1.5cm} 133 | {\LARGE } 134 | \end{center} 135 | \vspace{0.5cm} 136 | \begin{verse}\begin{center}} 137 | {\end{center}\end{verse}\end{alwayssingle}} 138 | 139 | 140 | % ACKNOWLEDGEMENTS 141 | % 142 | % The acknowledgements environment puts a large, bold, centered 143 | % "Acknowledgements" label at the top of the page. The acknowledgements 144 | % themselves appear in a quote environment, i.e. tabbed in at both sides, and 145 | % on its own page. 146 | 147 | \newenvironment{acknowledgements} 148 | {\begin{alwayssingle} \thispagestyle{empty} 149 | \begin{center} 150 | \vspace*{1.5cm} 151 | {\Large \bfseries Acknowledgements} 152 | \end{center} 153 | \vspace{0.5cm} 154 | \begin{quote}} 155 | {\end{quote}\end{alwayssingle}} 156 | 157 | % The acknowledgementslong environment puts a large, bold, centered 158 | % "Acknowledgements" label at the top of the page. The acknowledgement itself 159 | % does not appears in a quote environment so you can get more in. 160 | 161 | \newenvironment{acknowledgementslong} 162 | {\begin{alwayssingle} \thispagestyle{empty} 163 | \begin{center} 164 | \vspace*{1.5cm} 165 | {\Large \bfseries Acknowledgements} 166 | \end{center} 167 | \vspace{0.5cm}} 168 | {\end{alwayssingle}} 169 | 170 | % STATEMENT OF ORIGINALITY (AS SUGGESTED BY GSW) 171 | % 172 | % The originality environment puts a large, bold, centered 173 | % "Statement of originality" label at the top of the page. The statement 174 | % of originality itself appears in a quote environment, i.e. tabbed in at 175 | % both sides, and on its own page. 176 | 177 | \newenvironment{originality} 178 | {\begin{alwayssingle} \thispagestyle{empty} 179 | \begin{center} 180 | \vspace*{1.5cm} 181 | {\Large \bfseries Statement of Originality} 182 | \end{center} 183 | \vspace{0.5cm} 184 | \begin{quote}} 185 | {\end{quote}\end{alwayssingle}} 186 | 187 | % The originalitylong environment puts a large, bold, centered 188 | % "Statement of originality" label at the top of the page. The statement 189 | % of originality itself does not appears in a quote environment so you can 190 | % get more in. 191 | 192 | \newenvironment{originalitylong} 193 | {\begin{alwayssingle} \thispagestyle{empty} 194 | \begin{center} 195 | \vspace*{1.5cm} 196 | {\Large \bfseries Statement of Originality} 197 | \end{center} 198 | \vspace{0.5cm}} 199 | {\end{alwayssingle}} 200 | 201 | 202 | %ABSTRACT 203 | % 204 | %The abstract environment puts a large, bold, centered "Abstract" label at 205 | %the top of the page. The abstract itself appears in a quote environment, 206 | %i.e. tabbed in at both sides, and on its own page. 207 | 208 | \renewenvironment{abstract} {\begin{alwayssingle} \thispagestyle{empty} 209 | \begin{center} 210 | \vspace*{1.5cm} 211 | {\Large \bfseries Abstract} 212 | \end{center} 213 | \vspace{0.5cm} 214 | \begin{quote}} 215 | {\end{quote}\end{alwayssingle}} 216 | 217 | %The abstractlong environment puts a large, bold, centered "Abstract" label at 218 | %the top of the page. The abstract itself does not appears in a quote 219 | %environment so you can get more in. 220 | 221 | \newenvironment{abstractlong} {\begin{alwayssingle} \thispagestyle{empty} 222 | \begin{center} 223 | \vspace*{1.5cm} 224 | {\Large \bfseries Abstract} 225 | \end{center} 226 | \vspace{0.5cm}} 227 | {\end{alwayssingle}} 228 | 229 | %The abstractseparate environment is for running of a page with the abstract 230 | %on including title and author etc as required to be handed in separately 231 | 232 | \newenvironment{abstractseparate} {\begin{alwayssingle} \thispagestyle{empty} 233 | \vspace*{-1in} 234 | \begin{center} 235 | { \Large {\bfseries {\@title}} \par} 236 | {{\large \vspace*{1ex} \@author} \par} 237 | {\large \vspace*{1ex} 238 | {{\@college} \par} 239 | {University of Oxford \par} 240 | \vspace*{1ex} 241 | {{\it \submittedtext} \par} 242 | {\it {\@degree} \par} 243 | \vspace*{2ex} 244 | {\@degreedate}} 245 | \end{center}} 246 | {\end{alwayssingle}} 247 | 248 | %ROMANPAGES 249 | % 250 | % The romanpages environment set the page numbering to lowercase roman one 251 | % for the contents and figures lists. It also resets 252 | % page-numbering for the remainder of the dissertation (arabic, starting at 1). 253 | 254 | \newenvironment{romanpages} 255 | {\cleardoublepage\setcounter{page}{1}\renewcommand{\thepage}{\roman{page}}} 256 | {\cleardoublepage\renewcommand{\thepage}{\arabic{page}}\setcounter{page}{1}} 257 | -------------------------------------------------------------------------------- /tests/masters_dissertation/target/ociamthesis.cls: -------------------------------------------------------------------------------- 1 | % ociamthesis v2.2 2 | % By Keith A. Gillow 3 | % Version 1.0 released 26/11/1997 4 | %-------------------------- identification --------------------- 5 | \NeedsTeXFormat{LaTeX2e} 6 | \ProvidesClass{ociamthesis}[2010/11/22 v2.2 OCIAM thesis class] 7 | %-------------------------- initial code ----------------------- 8 | \def\logoversion{squarelogo} 9 | \DeclareOption{beltcrest}{\def\logoversion{beltcrest}} 10 | \DeclareOption{shieldcrest}{\def\logoversion{shieldcrest}} 11 | \DeclareOption*{\PassOptionsToClass{\CurrentOption}{report}} 12 | \ProcessOptions\relax 13 | \LoadClass[a4paper]{report} 14 | % As an alternative to the above could use next line for twosided output 15 | %\LoadClass[a4paper,twoside,openright]{report} 16 | 17 | \RequirePackage{graphicx} % needed for latest frontpage logo 18 | \RequirePackage{ifthen} % needed for option parsing for logo 19 | 20 | \raggedbottom 21 | 22 | %define the default submitted text 23 | \newcommand{\submittedtext}{{A thesis submitted for the degree of}} 24 | 25 | % 26 | % DECLARATIONS 27 | % 28 | % These macros are used to declare arguments needed for the 29 | % construction of the title page and other preamble. 30 | 31 | % The year and term the thesis is submitted 32 | \def\degreedate#1{\gdef\@degreedate{#1}} 33 | % The full (unabbreviated) name of the degree 34 | \def\degree#1{\gdef\@degree{#1}} 35 | % The name of your Oxford college (e.g. Christ Church, Pembroke) 36 | \def\college#1{\gdef\@college{#1}} 37 | 38 | % 39 | % Setup chosen crest/logo 40 | % 41 | 42 | \ifthenelse{\equal{\logoversion}{shieldcrest}}% 43 | { 44 | % Traditional Oxford shield crest 45 | %Using latex metafont (Mathematical Institute system) 46 | \font\crestfont=oxcrest40 scaled\magstep3 47 | \def\logo{{\crestfont \char1}} 48 | %For comlab system replace 1st line above with 49 | %\font\crestfont=crest scaled\magstep3 50 | }{} 51 | 52 | \ifthenelse{\equal{\logoversion}{beltcrest}}% 53 | { 54 | % Newer Oxford Belt crest 55 | %Using latex metafont (Mathematical Institute system) 56 | \font\beltcrestfont=oxbeltcrest 57 | \def\logo{{\beltcrestfont \char0}} 58 | %For comlab system replace 1st line above with 59 | %\font\beltcrestfont=newcrest 60 | }{} 61 | 62 | \ifthenelse{\equal{\logoversion}{squarelogo}}% 63 | { 64 | % Latest Logo, Square version (the default!) 65 | % you need an oxlogo.eps or oxlogo.pdf file as appropriate 66 | \def\logo{{ 67 | %\includegraphics[width=32mm,draft=false]{../graphics/branding/oxlogo} 68 | }} 69 | }{} 70 | 71 | % 72 | % Define text area of page and margin offsets 73 | % 74 | \setlength{\topmargin}{0.0in} %0.0in 75 | \setlength{\oddsidemargin}{0.167in} % 0.33in 76 | \setlength{\evensidemargin}{-0.08in} %-0.08in 77 | \setlength{\textheight}{9.2in} %9.0in 78 | \setlength{\textwidth}{6.0in} %6.0in 79 | \setlength{\headheight}{15pt} % not set 80 | \setlength{\voffset}{-0.2in} % not set 81 | 82 | % 83 | % Environments 84 | % 85 | 86 | % This macro define an environment for front matter that is always 87 | % single column even in a double-column document. 88 | 89 | \newenvironment{alwayssingle}{% 90 | \@restonecolfalse 91 | \if@twocolumn\@restonecoltrue\onecolumn 92 | \else\if@openright\cleardoublepage\else\clearpage\fi 93 | \fi}% 94 | {\if@restonecol\twocolumn 95 | \else\newpage\thispagestyle{empty}\fi} 96 | 97 | %define title page layout 98 | \renewcommand{\maketitle}{% 99 | \begin{alwayssingle} 100 | \renewcommand{\footnotesize}{\small} 101 | \renewcommand{\footnoterule}{\relax} 102 | \thispagestyle{empty} 103 | \null\vfill 104 | \begin{center} 105 | { \Huge {\bfseries {\@title}} \par} 106 | {\large \vspace*{40mm} {\logo \par} \vspace*{25mm}} 107 | {{\Large \@author} \par} 108 | {\large \vspace*{1.5ex} % 1ex 109 | {{\@college} \par} 110 | \vspace*{1ex} 111 | {University of Oxford \par} 112 | \vspace*{25mm} 113 | {{\submittedtext} \par} 114 | \vspace*{1ex} 115 | {\it {\@degree} \par} 116 | \vspace*{2ex} 117 | {\@degreedate}} 118 | \end{center} 119 | \null\vfill 120 | \end{alwayssingle}} 121 | 122 | % DEDICATION 123 | % 124 | % The dedication environment makes sure the dedication gets its 125 | % own page and is set out in verse format. 126 | 127 | \newenvironment{dedication} 128 | { 129 | \begin{alwayssingle} 130 | \thispagestyle{empty} 131 | \begin{center} 132 | \vspace*{1.5cm} 133 | {\LARGE } 134 | \end{center} 135 | \vspace{0.5cm} 136 | \begin{verse} 137 | \begin{center}} 138 | { 139 | \end{center} 140 | \end{verse} 141 | \end{alwayssingle}} 142 | 143 | % ACKNOWLEDGEMENTS 144 | % 145 | % The acknowledgements environment puts a large, bold, centered 146 | % "Acknowledgements" label at the top of the page. The acknowledgements 147 | % themselves appear in a quote environment, i.e. tabbed in at both sides, and 148 | % on its own page. 149 | 150 | \newenvironment{acknowledgements} 151 | { 152 | \begin{alwayssingle} \thispagestyle{empty} 153 | \begin{center} 154 | \vspace*{1.5cm} 155 | {\Large \bfseries Acknowledgements} 156 | \end{center} 157 | \vspace{0.5cm} 158 | \begin{quote}} 159 | { 160 | \end{quote} 161 | \end{alwayssingle}} 162 | 163 | % The acknowledgementslong environment puts a large, bold, centered 164 | % "Acknowledgements" label at the top of the page. The acknowledgement itself 165 | % does not appears in a quote environment so you can get more in. 166 | 167 | \newenvironment{acknowledgementslong} 168 | { 169 | \begin{alwayssingle} \thispagestyle{empty} 170 | \begin{center} 171 | \vspace*{1.5cm} 172 | {\Large \bfseries Acknowledgements} 173 | \end{center} 174 | \vspace{0.5cm}} 175 | { 176 | \end{alwayssingle}} 177 | 178 | % STATEMENT OF ORIGINALITY (AS SUGGESTED BY GSW) 179 | % 180 | % The originality environment puts a large, bold, centered 181 | % "Statement of originality" label at the top of the page. The statement 182 | % of originality itself appears in a quote environment, i.e. tabbed in at 183 | % both sides, and on its own page. 184 | 185 | \newenvironment{originality} 186 | { 187 | \begin{alwayssingle} \thispagestyle{empty} 188 | \begin{center} 189 | \vspace*{1.5cm} 190 | {\Large \bfseries Statement of Originality} 191 | \end{center} 192 | \vspace{0.5cm} 193 | \begin{quote}} 194 | { 195 | \end{quote} 196 | \end{alwayssingle}} 197 | 198 | % The originalitylong environment puts a large, bold, centered 199 | % "Statement of originality" label at the top of the page. The statement 200 | % of originality itself does not appears in a quote environment so you can 201 | % get more in. 202 | 203 | \newenvironment{originalitylong} 204 | { 205 | \begin{alwayssingle} \thispagestyle{empty} 206 | \begin{center} 207 | \vspace*{1.5cm} 208 | {\Large \bfseries Statement of Originality} 209 | \end{center} 210 | \vspace{0.5cm}} 211 | { 212 | \end{alwayssingle}} 213 | 214 | %ABSTRACT 215 | % 216 | %The abstract environment puts a large, bold, centered "Abstract" label at 217 | %the top of the page. The abstract itself appears in a quote environment, 218 | %i.e. tabbed in at both sides, and on its own page. 219 | 220 | \renewenvironment{abstract} { 221 | \begin{alwayssingle} \thispagestyle{empty} 222 | \begin{center} 223 | \vspace*{1.5cm} 224 | {\Large \bfseries Abstract} 225 | \end{center} 226 | \vspace{0.5cm} 227 | \begin{quote}} 228 | { 229 | \end{quote} 230 | \end{alwayssingle}} 231 | 232 | %The abstractlong environment puts a large, bold, centered "Abstract" label at 233 | %the top of the page. The abstract itself does not appears in a quote 234 | %environment so you can get more in. 235 | 236 | \newenvironment{abstractlong} { 237 | \begin{alwayssingle} \thispagestyle{empty} 238 | \begin{center} 239 | \vspace*{1.5cm} 240 | {\Large \bfseries Abstract} 241 | \end{center} 242 | \vspace{0.5cm}} 243 | { 244 | \end{alwayssingle}} 245 | 246 | %The abstractseparate environment is for running of a page with the abstract 247 | %on including title and author etc as required to be handed in separately 248 | 249 | \newenvironment{abstractseparate} { 250 | \begin{alwayssingle} \thispagestyle{empty} 251 | \vspace*{-1in} 252 | \begin{center} 253 | { \Large {\bfseries {\@title}} \par} 254 | {{\large \vspace*{1ex} \@author} \par} 255 | {\large \vspace*{1ex} 256 | {{\@college} \par} 257 | {University of Oxford \par} 258 | \vspace*{1ex} 259 | {{\it \submittedtext} \par} 260 | {\it {\@degree} \par} 261 | \vspace*{2ex} 262 | {\@degreedate}} 263 | \end{center}} 264 | { 265 | \end{alwayssingle}} 266 | 267 | %ROMANPAGES 268 | % 269 | % The romanpages environment set the page numbering to lowercase roman one 270 | % for the contents and figures lists. It also resets 271 | % page-numbering for the remainder of the dissertation (arabic, starting at 1). 272 | 273 | \newenvironment{romanpages} 274 | {\cleardoublepage\setcounter{page}{1}\renewcommand{\thepage}{\roman{page}}} 275 | {\cleardoublepage\renewcommand{\thepage}{\arabic{page}}\setcounter{page}{1}} 276 | -------------------------------------------------------------------------------- /tests/no_indent_envs/source/no_indent_envs.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | Documents are not indented. 6 | 7 | \begin{mydocument} 8 | 9 | Neither is this environment. 10 | 11 | \begin{proof} 12 | 13 | This environment is indented. 14 | 15 | \begin{myproof} 16 | 17 | But not this custom environment. 18 | 19 | \end{myproof} 20 | 21 | \end{proof} 22 | 23 | \end{mydocument} 24 | 25 | \end{document} 26 | -------------------------------------------------------------------------------- /tests/no_indent_envs/target/no_indent_envs.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | Documents are not indented. 6 | 7 | \begin{mydocument} 8 | 9 | Neither is this environment. 10 | 11 | \begin{proof} 12 | 13 | This environment is indented. 14 | 15 | \begin{myproof} 16 | 17 | But not this custom environment. 18 | 19 | \end{myproof} 20 | 21 | \end{proof} 22 | 23 | \end{mydocument} 24 | 25 | \end{document} 26 | -------------------------------------------------------------------------------- /tests/no_indent_envs/tex-fmt.toml: -------------------------------------------------------------------------------- 1 | no-indent-envs = ["mydocument", "myproof"] 2 | -------------------------------------------------------------------------------- /tests/phd_dissertation/source/puthesis.cls: -------------------------------------------------------------------------------- 1 | \NeedsTeXFormat{LaTeX2e} 2 | \ProvidesClass{puthesis} 3 | \RequirePackage{setspace} 4 | \RequirePackage{xcolor} 5 | \def\current@color{ Black} 6 | \newcounter{subyear} 7 | \setcounter{subyear}{\number\year} 8 | \def\submitted#1{\gdef\@submitted{#1}} 9 | \def\@submittedyear{\ifnum\month>10 \stepcounter{subyear}\thesubyear 10 | \else\thesubyear\fi} 11 | \def\@submittedmonth{\ifnum\month>10 January\else\ifnum\month>8 November 12 | \else\ifnum\month>6 September\else May\fi\fi\fi} 13 | \def\adviser#1{\gdef\@adviser{#1}} 14 | \long\def\@abstract{\@latex@error{No \noexpand\abstract given}\@ehc} 15 | \newcommand*{\frontmatter}{ 16 | %\pagenumbering{roman} 17 | } 18 | \newcommand*{\mainmatter}{ 19 | %\pagenumbering{arabic} 20 | } 21 | \newcommand*{\makelot}{} 22 | \newcommand*{\makelof}{} 23 | \newcommand*{\makelos}{} 24 | \newcommand*{\begincmd}{ 25 | \doublespacing 26 | \frontmatter\maketitlepage\makecopyrightpage\makeabstract 27 | \makeacknowledgments\makededication\tableofcontents\clearpage 28 | \makelot\clearpage\makelof\clearpage\makelos 29 | \clearpage\mainmatter} 30 | \def\@submitted{\@submittedmonth~\@submittedyear} 31 | \def\@dept{Operations Research and Financial Engineering} 32 | \def\@deptpref{Department of} 33 | \def\departmentprefix#1{\gdef\@deptpref{#1}} 34 | \def\department#1{\gdef\@dept{#1}} 35 | \long\def\acknowledgments#1{\gdef\@acknowledgments{#1}} 36 | \def\dedication#1{\gdef\@dedication{#1}} 37 | \newcommand{\maketitlepage}{{ 38 | \thispagestyle{empty} 39 | \sc 40 | \vspace*{0in} 41 | \begin{center} 42 | \LARGE \@title 43 | \end{center} 44 | \vspace{.6in} 45 | \begin{center} 46 | \@author 47 | \end{center} 48 | \vspace{.6in} 49 | \begin{center} 50 | A Dissertation \\ 51 | Presented to the Faculty \\ 52 | of Princeton University \\ 53 | in Candidacy for the Degree \\ 54 | of Doctor of Philosophy 55 | \end{center} 56 | \vspace{.3in} 57 | \begin{center} 58 | Recommended for Acceptance \\ 59 | by the \@deptpref \\ 60 | \@dept \\ 61 | Adviser: \@adviser 62 | \end{center} 63 | \vspace{.3in} 64 | \begin{center} 65 | \@submitted 66 | \end{center} 67 | \clearpage 68 | }} 69 | \newcommand*{\makecopyrightpage}{ 70 | \thispagestyle{empty} 71 | \vspace*{0in} 72 | \begin{center} 73 | \copyright\ Copyright by \@author, \number\year. \\ 74 | All rights reserved. 75 | \end{center} 76 | \clearpage} 77 | \newcommand*{\makeabstract}{ 78 | \newpage 79 | \addcontentsline{toc}{section}{Abstract} 80 | \begin{center} 81 | \Large \textbf{Abstract} 82 | \end{center} 83 | \@abstract 84 | \clearpage 85 | } 86 | \def\makeacknowledgments{ 87 | \ifx\@acknowledgments\undefined 88 | \else 89 | \addcontentsline{toc}{section}{Acknowledgments} 90 | \begin{center} 91 | \Large \textbf{Acknowledgments} 92 | \end{center} 93 | \@acknowledgments 94 | \clearpage 95 | \fi 96 | } 97 | \def\makededication{ 98 | \ifx\@dedication\undefined 99 | \else 100 | \vspace*{1.5in} 101 | \begin{flushright} 102 | \@dedication 103 | \end{flushright} 104 | \clearpage 105 | \fi 106 | } 107 | \DeclareOption{myorder}{ 108 | \renewcommand*{\begincmd}{\doublespacing}} 109 | \DeclareOption{lot}{\renewcommand*{\makelot}{ 110 | \addcontentsline{toc}{section}{List of Tables}\listoftables}} 111 | \DeclareOption{lof}{\renewcommand*{\makelof}{ 112 | \addcontentsline{toc}{section}{List of Figures and Tables}\listoffigures}} 113 | \DeclareOption{los}{ 114 | \renewcommand*{\makelos}{ 115 | \RequirePackage{losymbol} 116 | \section*{List of Symbols\@mkboth {LIST OF SYMBOLS}{LIST OF SYMBOLS}} 117 | \@starttoc{los} 118 | \addcontentsline{toc}{section}{List of Symbols} 119 | } 120 | } 121 | \DeclareOption*{\PassOptionsToClass{\CurrentOption}{report}} 122 | \ProcessOptions 123 | \LoadClass{report} 124 | \setlength{\oddsidemargin}{0.2in} 125 | \setlength{\evensidemargin}{0.2in} 126 | \setlength{\topmargin}{0in} 127 | \setlength{\headheight}{0in} 128 | \setlength{\headsep}{0in} 129 | \setlength{\textheight}{8.9in} 130 | \setlength{\textwidth}{6.1in} 131 | \setlength{\footskip}{0.5in} 132 | \long\def\abstract#1{\gdef\@abstract{#1}} 133 | \AtBeginDocument{\begincmd} 134 | \endinput 135 | -------------------------------------------------------------------------------- /tests/phd_dissertation/target/puthesis.cls: -------------------------------------------------------------------------------- 1 | \NeedsTeXFormat{LaTeX2e} 2 | \ProvidesClass{puthesis} 3 | \RequirePackage{setspace} 4 | \RequirePackage{xcolor} 5 | \def\current@color{ Black} 6 | \newcounter{subyear} 7 | \setcounter{subyear}{\number\year} 8 | \def\submitted#1{\gdef\@submitted{#1}} 9 | \def\@submittedyear{\ifnum\month>10 \stepcounter{subyear}\thesubyear 10 | \else\thesubyear\fi} 11 | \def\@submittedmonth{\ifnum\month>10 January\else\ifnum\month>8 November 12 | \else\ifnum\month>6 September\else May\fi\fi\fi} 13 | \def\adviser#1{\gdef\@adviser{#1}} 14 | \long\def\@abstract{\@latex@error{No \noexpand\abstract given}\@ehc} 15 | \newcommand*{\frontmatter}{ 16 | %\pagenumbering{roman} 17 | } 18 | \newcommand*{\mainmatter}{ 19 | %\pagenumbering{arabic} 20 | } 21 | \newcommand*{\makelot}{} 22 | \newcommand*{\makelof}{} 23 | \newcommand*{\makelos}{} 24 | \newcommand*{\begincmd}{ 25 | \doublespacing 26 | \frontmatter\maketitlepage\makecopyrightpage\makeabstract 27 | \makeacknowledgments\makededication\tableofcontents\clearpage 28 | \makelot\clearpage\makelof\clearpage\makelos 29 | \clearpage\mainmatter} 30 | \def\@submitted{\@submittedmonth~\@submittedyear} 31 | \def\@dept{Operations Research and Financial Engineering} 32 | \def\@deptpref{Department of} 33 | \def\departmentprefix#1{\gdef\@deptpref{#1}} 34 | \def\department#1{\gdef\@dept{#1}} 35 | \long\def\acknowledgments#1{\gdef\@acknowledgments{#1}} 36 | \def\dedication#1{\gdef\@dedication{#1}} 37 | \newcommand{\maketitlepage}{{ 38 | \thispagestyle{empty} 39 | \sc 40 | \vspace*{0in} 41 | \begin{center} 42 | \LARGE \@title 43 | \end{center} 44 | \vspace{.6in} 45 | \begin{center} 46 | \@author 47 | \end{center} 48 | \vspace{.6in} 49 | \begin{center} 50 | A Dissertation \\ 51 | Presented to the Faculty \\ 52 | of Princeton University \\ 53 | in Candidacy for the Degree \\ 54 | of Doctor of Philosophy 55 | \end{center} 56 | \vspace{.3in} 57 | \begin{center} 58 | Recommended for Acceptance \\ 59 | by the \@deptpref \\ 60 | \@dept \\ 61 | Adviser: \@adviser 62 | \end{center} 63 | \vspace{.3in} 64 | \begin{center} 65 | \@submitted 66 | \end{center} 67 | \clearpage 68 | }} 69 | \newcommand*{\makecopyrightpage}{ 70 | \thispagestyle{empty} 71 | \vspace*{0in} 72 | \begin{center} 73 | \copyright\ Copyright by \@author, \number\year. \\ 74 | All rights reserved. 75 | \end{center} 76 | \clearpage} 77 | \newcommand*{\makeabstract}{ 78 | \newpage 79 | \addcontentsline{toc}{section}{Abstract} 80 | \begin{center} 81 | \Large \textbf{Abstract} 82 | \end{center} 83 | \@abstract 84 | \clearpage 85 | } 86 | \def\makeacknowledgments{ 87 | \ifx\@acknowledgments\undefined 88 | \else 89 | \addcontentsline{toc}{section}{Acknowledgments} 90 | \begin{center} 91 | \Large \textbf{Acknowledgments} 92 | \end{center} 93 | \@acknowledgments 94 | \clearpage 95 | \fi 96 | } 97 | \def\makededication{ 98 | \ifx\@dedication\undefined 99 | \else 100 | \vspace*{1.5in} 101 | \begin{flushright} 102 | \@dedication 103 | \end{flushright} 104 | \clearpage 105 | \fi 106 | } 107 | \DeclareOption{myorder}{ 108 | \renewcommand*{\begincmd}{\doublespacing}} 109 | \DeclareOption{lot}{\renewcommand*{\makelot}{ 110 | \addcontentsline{toc}{section}{List of Tables}\listoftables}} 111 | \DeclareOption{lof}{\renewcommand*{\makelof}{ 112 | \addcontentsline{toc}{section}{List of Figures and Tables}\listoffigures}} 113 | \DeclareOption{los}{ 114 | \renewcommand*{\makelos}{ 115 | \RequirePackage{losymbol} 116 | \section*{List of Symbols\@mkboth {LIST OF SYMBOLS}{LIST OF SYMBOLS}} 117 | \@starttoc{los} 118 | \addcontentsline{toc}{section}{List of Symbols} 119 | } 120 | } 121 | \DeclareOption*{\PassOptionsToClass{\CurrentOption}{report}} 122 | \ProcessOptions 123 | \LoadClass{report} 124 | \setlength{\oddsidemargin}{0.2in} 125 | \setlength{\evensidemargin}{0.2in} 126 | \setlength{\topmargin}{0in} 127 | \setlength{\headheight}{0in} 128 | \setlength{\headsep}{0in} 129 | \setlength{\textheight}{8.9in} 130 | \setlength{\textwidth}{6.1in} 131 | \setlength{\footskip}{0.5in} 132 | \long\def\abstract#1{\gdef\@abstract{#1}} 133 | \AtBeginDocument{\begincmd} 134 | \endinput 135 | -------------------------------------------------------------------------------- /tests/readme/source/readme.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | \begin{itemize} 6 | \item Lists with items 7 | over multiple lines 8 | \end{itemize} 9 | 10 | \begin{equation} 11 | E = m c^2 12 | \end{equation} 13 | 14 | \end{document} 15 | -------------------------------------------------------------------------------- /tests/readme/target/readme.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | \begin{itemize} 6 | \item Lists with items 7 | over multiple lines 8 | \end{itemize} 9 | 10 | \begin{equation} 11 | E = m c^2 12 | \end{equation} 13 | 14 | \end{document} 15 | -------------------------------------------------------------------------------- /tests/sections/source/sections.tex: -------------------------------------------------------------------------------- 1 | \documentclass{book} 2 | 3 | \begin{document} 4 | 5 | \section{Section test} 6 | 7 | Sectioning commands should be moved to their own lines.\subsection{Result} Even if there is more than one.\subsection{Result 2} 8 | 9 | Also \section*{A} unnumbered sectioning commands \subsection*{B} should be split onto their own lines, even if there \subsubsection*{C} is more than one. 10 | 11 | All of this \part{D} should also hold \part*{E} for parts \chapter{F} and chapters \chapter*{G}. 12 | 13 | \end{document} 14 | -------------------------------------------------------------------------------- /tests/sections/target/sections.tex: -------------------------------------------------------------------------------- 1 | \documentclass{book} 2 | 3 | \begin{document} 4 | 5 | \section{Section test} 6 | 7 | Sectioning commands should be moved to their own lines. 8 | \subsection{Result} Even if there is more than one. 9 | \subsection{Result 2} 10 | 11 | Also 12 | \section*{A} unnumbered sectioning commands 13 | \subsection*{B} should be split onto their own lines, even if there 14 | \subsubsection*{C} is more than one. 15 | 16 | All of this 17 | \part{D} should also hold 18 | \part*{E} for parts 19 | \chapter{F} and chapters 20 | \chapter*{G}. 21 | 22 | \end{document} 23 | -------------------------------------------------------------------------------- /tests/short_document/source/short_document.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \usepackage{amsmath} 4 | \usepackage{amsthm} 5 | 6 | \newtheorem{theorem}{Theorem} 7 | 8 | \title{Testing \texttt{tex-fmt}} 9 | \author{William G.\ Underwood} 10 | \begin{document} 11 | \maketitle 12 | 13 | \begin{align} 14 | E = m c^2 \\ 15 | 1 + 2 16 | + (3 + 4) 17 | + (5 + 6 18 | + 7 + 8) 19 | + (9 + 10 20 | + 11 + 12 21 | + 13 + 14) 22 | \end{align} 23 | 24 | \begin{itemize} 25 | \item Item one % trailing comment with ]) brackets 26 | \item Item two on 27 | multiple lines 28 | \item Item three 29 | \begin{itemize} 30 | \item Subitem one of item two % this line has trailing spaces 31 | \item Subitem two of item two 32 | \end{itemize} 33 | \item Item four % trailing comment % with [( brackets 34 | \item 35 | \end{itemize} 36 | 37 | \begin{theorem}[Pythagoras]% 38 | \label{thm:pythagoras} 39 | 40 | For a right triangle with hypotenuse $c$ and other sides $a$ and $b$, 41 | we have 42 | % 43 | \begin{align*} 44 | a^2 + b^2 = c^2 45 | \end{align*} 46 | % 47 | % some comments 48 | 49 | \end{theorem} 50 | 51 | This line contains \emph{emphasized} text. 52 | \emph{This line contains only emphasized text, 53 | and is broken over two lines}. 54 | \emph{This line contains only 55 | emphasized text, 56 | and is broken over three lines}. 57 | 58 | \end{document} 59 | 60 | % This file ends with trailing newlines 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /tests/short_document/target/short_document.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \usepackage{amsmath} 4 | \usepackage{amsthm} 5 | 6 | \newtheorem{theorem}{Theorem} 7 | 8 | \title{Testing \texttt{tex-fmt}} 9 | \author{William G.\ Underwood} 10 | \begin{document} 11 | \maketitle 12 | 13 | \begin{align} 14 | E = m c^2 \\ 15 | 1 + 2 16 | + (3 + 4) 17 | + (5 + 6 18 | + 7 + 8) 19 | + (9 + 10 20 | + 11 + 12 21 | + 13 + 14) 22 | \end{align} 23 | 24 | \begin{itemize} 25 | \item Item one % trailing comment with ]) brackets 26 | \item Item two on 27 | multiple lines 28 | \item Item three 29 | \begin{itemize} 30 | \item Subitem one of item two % this line has trailing spaces 31 | \item Subitem two of item two 32 | \end{itemize} 33 | \item Item four % trailing comment % with [( brackets 34 | \item 35 | \end{itemize} 36 | 37 | \begin{theorem}[Pythagoras]% 38 | \label{thm:pythagoras} 39 | 40 | For a right triangle with hypotenuse $c$ and other sides $a$ and $b$, 41 | we have 42 | % 43 | \begin{align*} 44 | a^2 + b^2 = c^2 45 | \end{align*} 46 | % 47 | % some comments 48 | 49 | \end{theorem} 50 | 51 | This line contains \emph{emphasized} text. 52 | \emph{This line contains only emphasized text, 53 | and is broken over two lines}. 54 | \emph{This line contains only 55 | emphasized text, 56 | and is broken over three lines}. 57 | 58 | \end{document} 59 | 60 | % This file ends with trailing newlines 61 | -------------------------------------------------------------------------------- /tests/tabsize/cli.txt: -------------------------------------------------------------------------------- 1 | --tabsize 4 2 | --wraplen 80 3 | -------------------------------------------------------------------------------- /tests/tabsize/source/tabsize.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | \begin{itemize} 6 | \item Lists with items 7 | over multiple lines 8 | \end{itemize} 9 | 10 | \begin{equation} 11 | E = m c^2 12 | \end{equation} 13 | 14 | \end{document} 15 | -------------------------------------------------------------------------------- /tests/tabsize/target/tabsize.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | \begin{itemize} 6 | \item Lists with items 7 | over multiple lines 8 | \end{itemize} 9 | 10 | \begin{equation} 11 | E = m c^2 12 | \end{equation} 13 | 14 | \end{document} 15 | -------------------------------------------------------------------------------- /tests/tabsize/tex-fmt.toml: -------------------------------------------------------------------------------- 1 | tabsize = 4 2 | -------------------------------------------------------------------------------- /tests/unicode/source/unicode.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | This is a long line with a unicode arrow in the middle of it ↓ which should be split correctly 6 | 7 | Here an indent begins ( 8 | and should not be closed with this arrow and comment ↓% 9 | until the next parenthesis 10 | ) 11 | 12 | This line contains some French accent characters éééééééééééééééééééééééééééééé 13 | which include zero-width chars, so look narrower than they are. 14 | 15 | \end{document} 16 | -------------------------------------------------------------------------------- /tests/unicode/target/unicode.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | This is a long line with a unicode arrow in the middle of it ↓ which 6 | should be split correctly 7 | 8 | Here an indent begins ( 9 | and should not be closed with this arrow and comment ↓% 10 | until the next parenthesis 11 | ) 12 | 13 | This line contains some French accent characters éééééééééééééééééééééééééééééé 14 | which include zero-width chars, so look narrower than they are. 15 | 16 | \end{document} 17 | -------------------------------------------------------------------------------- /tests/verb/source/verb.tex: -------------------------------------------------------------------------------- 1 | \begin{document} 2 | 3 | % Do not break a line inside inline verb environment 4 | 5 | \verb|code code code code code code code code code code code code code code code code| 6 | 7 | % Ok to break a line before or after inline verb environment 8 | 9 | Some words in a sentence \verb|code code code code code code code code code code code| 10 | 11 | \verb|code code code code code code code code code code code| some words in a sentence 12 | 13 | % Indenting should be as usual 14 | 15 | \begin{itemize} 16 | \item 17 | \verb|code code code code code code code code code code code code code code code| 18 | \end{itemize} 19 | 20 | % Do not split \begin{environment} onto a new line if inside \verb|...| 21 | 22 | \verb|\end{description}| 23 | 24 | \verb|\begin{description}| 25 | 26 | % This should not affect line breaking 27 | 28 | Some words in a long sentence which could be broken before the verb part of this line \verb|\begin{description}| 29 | 30 | \end{document} 31 | -------------------------------------------------------------------------------- /tests/verb/target/verb.tex: -------------------------------------------------------------------------------- 1 | \begin{document} 2 | 3 | % Do not break a line inside inline verb environment 4 | 5 | \verb|code code code code code code code code code code code code code code code code| 6 | 7 | % Ok to break a line before or after inline verb environment 8 | 9 | Some words in a sentence 10 | \verb|code code code code code code code code code code code| 11 | 12 | \verb|code code code code code code code code code code code| some 13 | words in a sentence 14 | 15 | % Indenting should be as usual 16 | 17 | \begin{itemize} 18 | \item 19 | \verb|code code code code code code code code code code code code code code code| 20 | \end{itemize} 21 | 22 | % Do not split \begin{environment} onto a new line if inside \verb|...| 23 | 24 | \verb|\end{description}| 25 | 26 | \verb|\begin{description}| 27 | 28 | % This should not affect line breaking 29 | 30 | Some words in a long sentence which could be broken before the verb 31 | part of this line \verb|\begin{description}| 32 | 33 | \end{document} 34 | -------------------------------------------------------------------------------- /tests/verbatim/source/verbatim.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | \usepackage{listings} 3 | 4 | \begin{document} 5 | 6 | \begin{verbatim} 7 | 8 | code code code code code code code code code code code code code code code code code code 9 | 10 | code code code code code code code code code code code code code code code code code code 11 | 12 | \item \item \item 13 | 14 | \begin{align} E = mc^2 \end{align} 15 | 16 | \end{verbatim} 17 | 18 | \begin{lstlisting}[caption={A very long and complicated caption that does not fit into one line}] 19 | Code 20 | \end{lstlisting} 21 | 22 | % Here is a special user-defined verbatim environment 23 | 24 | \begin{myverbatim} 25 | code code code code code code code code code code code code code code code code code code 26 | \end{myverbatim} 27 | 28 | \end{document} 29 | -------------------------------------------------------------------------------- /tests/verbatim/target/verbatim.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | \usepackage{listings} 3 | 4 | \begin{document} 5 | 6 | \begin{verbatim} 7 | 8 | code code code code code code code code code code code code code code code code code code 9 | 10 | code code code code code code code code code code code code code code code code code code 11 | 12 | \item \item \item 13 | 14 | \begin{align} E = mc^2 \end{align} 15 | 16 | \end{verbatim} 17 | 18 | \begin{lstlisting}[caption={A very long and complicated caption that does not fit into one line}] 19 | Code 20 | \end{lstlisting} 21 | 22 | % Here is a special user-defined verbatim environment 23 | 24 | \begin{myverbatim} 25 | code code code code code code code code code code code code code code code code code code 26 | \end{myverbatim} 27 | 28 | \end{document} 29 | -------------------------------------------------------------------------------- /tests/verbatim/tex-fmt.toml: -------------------------------------------------------------------------------- 1 | verbatims = ["myverbatim"] 2 | -------------------------------------------------------------------------------- /tests/wrap/source/heavy_wrap.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \usepackage{amsmath} 4 | \usepackage{amsthm} 5 | 6 | \newtheorem{definition}{Definition} 7 | 8 | \begin{document} 9 | 10 | \begin{definition} 11 | \begin{definition} 12 | \begin{definition} 13 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 14 | \end{definition} 15 | \end{definition} 16 | \end{definition} 17 | 18 | \end{document} 19 | -------------------------------------------------------------------------------- /tests/wrap/source/wrap.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | % no comment 6 | This line is too long because it has more than eighty characters inside it. Therefore it should be split. 7 | 8 | % break before comment 9 | This line is too long because it has more than eighty characters inside it. Therefore it % should be split. 10 | 11 | % break after spaced comment 12 | This line is too long because it has more than % eighty characters inside it. Therefore it should be split. 13 | 14 | % break after non-spaced comment 15 | This line is too long because it has more than% eighty characters inside it. Therefore it should be split. 16 | 17 | % unbreakable line 18 | Thislineistoolongbecauseithasmorethan%eightycharactersinsideit.Buttherearenospacessoitcannotbesplit. 19 | 20 | % line can be broken after 80 chars 21 | Thislineistoolongbecauseithasmorethaneightycharactersinsideitandtherearenospacesuntillater where there are some spaces so we can split this line here 22 | 23 | % long line only after indenting 24 | ( 25 | 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 123 26 | ) 27 | 28 | % double break after comment 29 | This line has a long comment. % This comment is very long so needs to be split over three lines which is another edge case which should be checked here with all these extra words 30 | 31 | % double break after only comment 32 | % This line is all a long comment. This comment is very long so needs to be split over three lines which is another edge case which should be checked here with all these extra words 33 | 34 | % lines containing \ 35 | This line would usually be split at the special character part with a slash\ but it's best to break the line earlier. 36 | 37 | % long lines with brackets 38 | (This line is too long because it has more than eighty characters inside it. Therefore it should be split. It also needs splitting onto multiple lines, and the middle lines should be indented due to these brackets.) 39 | 40 | % long lines with double brackets 41 | ((This line is too long because it has more than eighty characters inside it. Therefore it should be split. It also needs splitting onto multiple lines, and the middle lines should be doubly indented due to these brackets.)) 42 | 43 | \end{document} 44 | -------------------------------------------------------------------------------- /tests/wrap/target/heavy_wrap.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \usepackage{amsmath} 4 | \usepackage{amsthm} 5 | 6 | \newtheorem{definition}{Definition} 7 | 8 | \begin{document} 9 | 10 | \begin{definition} 11 | \begin{definition} 12 | \begin{definition} 13 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 14 | eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut 15 | enim ad minim veniam, quis nostrud exercitation ullamco laboris 16 | nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor 17 | in reprehenderit in voluptate velit esse cillum dolore eu 18 | fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 19 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 20 | \end{definition} 21 | \end{definition} 22 | \end{definition} 23 | 24 | \end{document} 25 | -------------------------------------------------------------------------------- /tests/wrap/target/wrap.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | % no comment 6 | This line is too long because it has more than eighty characters 7 | inside it. Therefore it should be split. 8 | 9 | % break before comment 10 | This line is too long because it has more than eighty characters 11 | inside it. Therefore it % should be split. 12 | 13 | % break after spaced comment 14 | This line is too long because it has more than % eighty characters 15 | % inside it. Therefore it should be split. 16 | 17 | % break after non-spaced comment 18 | This line is too long because it has more than% eighty characters 19 | % inside it. Therefore it should be split. 20 | 21 | % unbreakable line 22 | Thislineistoolongbecauseithasmorethan%eightycharactersinsideit.Buttherearenospacessoitcannotbesplit. 23 | 24 | % line can be broken after 80 chars 25 | Thislineistoolongbecauseithasmorethaneightycharactersinsideitandtherearenospacesuntillater 26 | where there are some spaces so we can split this line here 27 | 28 | % long line only after indenting 29 | ( 30 | 1234567890 1234567890 1234567890 1234567890 1234567890 1234567890 31 | 1234567890 123 32 | ) 33 | 34 | % double break after comment 35 | This line has a long comment. % This comment is very long so needs to 36 | % be split over three lines which is another edge case which should 37 | % be checked here with all these extra words 38 | 39 | % double break after only comment 40 | % This line is all a long comment. This comment is very long so needs 41 | % to be split over three lines which is another edge case which 42 | % should be checked here with all these extra words 43 | 44 | % lines containing \ 45 | This line would usually be split at the special character part with a 46 | slash\ but it's best to break the line earlier. 47 | 48 | % long lines with brackets 49 | (This line is too long because it has more than eighty characters 50 | inside it. Therefore it should be split. It also needs splitting onto 51 | multiple lines, and the middle lines should be indented due to these brackets.) 52 | 53 | % long lines with double brackets 54 | ((This line is too long because it has more than eighty characters 55 | inside it. Therefore it should be split. It also needs splitting onto 56 | multiple lines, and the middle lines should be doubly indented due to 57 | these brackets.)) 58 | 59 | \end{document} 60 | -------------------------------------------------------------------------------- /tests/wrap_chars/source/wrap_chars.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | This,file,is,similar,to,the,regular,wrapping,test,,but,uses,commas, 6 | instead,of,spaces,,and,attempts,to,split,at,commas. 7 | 8 | % no comment 9 | This,line,is,too,long,because,it,has,more,than,eighty,characters,inside,it.,Therefore,it,should,be,split. 10 | 11 | % break before comment 12 | This,line,is,too,long,because,it,has,more,than,eighty,characters,inside,it.,Therefore,it,%,should,be,split. 13 | 14 | % break after spaced comment 15 | This,line,is,too,long,because,it,has,more,than,%,eighty,characters,inside,it.,Therefore,it,should,be,split. 16 | 17 | % break after non-spaced comment 18 | This,line,is,too,long,because,it,has,more,than%,eighty,characters,inside,it.,Therefore,it,should,be,split. 19 | 20 | % unbreakable line 21 | Thislineistoolongbecauseithasmorethan%eightycharactersinsideit.Buttherearenospacessoitcannotbesplit. 22 | 23 | % line can be broken after 80 chars 24 | Thislineistoolongbecauseithasmorethaneightycharactersinsideitandtherearenospacesuntillater,where,there,are,some,spaces,so,we,can,split,this,line,here 25 | 26 | % long line only after indenting 27 | ( 28 | 1234567890,1234567890,1234567890,1234567890,1234567890,1234567890,1234567890,123 29 | ) 30 | 31 | % double break after comment 32 | This,line,has,a,long,comment.,%,This,comment,is,very,long,so,needs,to,be,split,over,three,lines,which,is,another,edge,case,which,should,be,checked,here,with,all,these,extra,words 33 | 34 | % double break after only comment 35 | % This,line,is,all,a,long,comment.,This,comment,is,very,long,so,needs,to,be,split,over,three,lines,which,is,another,edge,case,which,should,be,checked,here,with,all,these,extra,words 36 | 37 | % lines containing \ 38 | This,line,would,usually,be,split,at,the,special,character,part,with,a,slash\,but,it's,best,to,break,the,line,earlier. 39 | 40 | % long lines with brackets 41 | (This,line,is,too,long,because,it,has,more,than,eighty,characters,inside,it.,Therefore,it,should,be,split.,It,also,needs,splitting,onto,multiple,lines,,and,the,middle,lines,should,be,indented,due,to,these,brackets.) 42 | 43 | % long lines with double brackets 44 | ((This,line,is,too,long,because,it,has,more,than,eighty,characters,inside,it.,Therefore,it,should,be,split.,It,also,needs,splitting,onto,multiple,lines,,and,the,middle,lines,should,be,doubly,indented,due,to,these,brackets.)) 45 | 46 | % long line ending with non-ascii char 47 | thisisthefilethisisthefilethisisthefilethisisthefilethisisthefilethisisthefilethisisthefile。 48 | 49 | \end{document} 50 | -------------------------------------------------------------------------------- /tests/wrap_chars/source/wrap_chars_chinese.tex: -------------------------------------------------------------------------------- 1 | 的不变子空间包括可交换的算子的值域与零空间,其自身及其多项式的值域与子空间自然也包含在内。特别的,自身零空间的子空间与包含值域的子空间也是不变子空间。子空间的交与和也不变。 2 | 3 | 从线性变换的角度看,这是一个从\(V\) 到\(\F\) 的线性映射,也就是一个\textbf{线性泛函}。从某种意义上,线性泛函就是一个行向量。这样理解不仅可以省去不断把线性泛函看作函数而非``对象'', 4 | -------------------------------------------------------------------------------- /tests/wrap_chars/target/wrap_chars.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | 3 | \begin{document} 4 | 5 | This,file,is,similar,to,the,regular,wrapping,test,,but,uses,commas, 6 | instead,of,spaces,,and,attempts,to,split,at,commas. 7 | 8 | % no comment 9 | This,line,is,too,long,because,it,has,more,than,eighty,characters, 10 | inside,it.,Therefore,it,should,be,split. 11 | 12 | % break before comment 13 | This,line,is,too,long,because,it,has,more,than,eighty,characters, 14 | inside,it.,Therefore,it,%,should,be,split. 15 | 16 | % break after spaced comment 17 | This,line,is,too,long,because,it,has,more,than,%,eighty,characters, 18 | % inside,it.,Therefore,it,should,be,split. 19 | 20 | % break after non-spaced comment 21 | This,line,is,too,long,because,it,has,more,than%,eighty,characters, 22 | % inside,it.,Therefore,it,should,be,split. 23 | 24 | % unbreakable line 25 | Thislineistoolongbecauseithasmorethan%eightycharactersinsideit.Buttherearenospacessoitcannotbesplit. 26 | 27 | % line can be broken after 80 chars 28 | Thislineistoolongbecauseithasmorethaneightycharactersinsideitandtherearenospacesuntillater, 29 | where,there,are,some,spaces,so,we,can,split,this,line,here 30 | 31 | % long line only after indenting 32 | ( 33 | 1234567890,1234567890,1234567890,1234567890,1234567890,1234567890, 34 | 1234567890,123 35 | ) 36 | 37 | % double break after comment 38 | This,line,has,a,long,comment.,%,This,comment,is,very,long,so,needs,to, 39 | % be,split,over,three,lines,which,is,another,edge,case,which,should, 40 | % be,checked,here,with,all,these,extra,words 41 | 42 | % double break after only comment 43 | % This,line,is,all,a,long,comment.,This,comment,is,very,long,so,needs, 44 | % to,be,split,over,three,lines,which,is,another,edge,case,which, 45 | % should,be,checked,here,with,all,these,extra,words 46 | 47 | % lines containing \ 48 | This,line,would,usually,be,split,at,the,special,character,part,with,a, 49 | slash\,but,it's,best,to,break,the,line,earlier. 50 | 51 | % long lines with brackets 52 | (This,line,is,too,long,because,it,has,more,than,eighty,characters, 53 | inside,it.,Therefore,it,should,be,split.,It,also,needs,splitting,onto, 54 | multiple,lines,,and,the,middle,lines,should,be,indented,due,to,these,brackets.) 55 | 56 | % long lines with double brackets 57 | ((This,line,is,too,long,because,it,has,more,than,eighty,characters, 58 | inside,it.,Therefore,it,should,be,split.,It,also,needs,splitting,onto, 59 | multiple,lines,,and,the,middle,lines,should,be,doubly,indented,due,to, 60 | these,brackets.)) 61 | 62 | % long line ending with non-ascii char 63 | thisisthefilethisisthefilethisisthefilethisisthefilethisisthefilethisisthefilethisisthefile。 64 | 65 | \end{document} 66 | -------------------------------------------------------------------------------- /tests/wrap_chars/target/wrap_chars_chinese.tex: -------------------------------------------------------------------------------- 1 | 的不变子空间包括可交换的算子的值域与零空间,其自身及其多项式的值域与子空间自然也包含在内。特别的, 2 | 自身零空间的子空间与包含值域的子空间也是不变子空间。子空间的交与和也不变。 3 | 4 | 从线性变换的角度看,这是一个从\(V\) 到\(\F\) 的线性映射,也就是一个\textbf{线性泛函}。从某种意义上, 5 | 线性泛函就是一个行向量。这样理解不仅可以省去不断把线性泛函看作函数而非``对象'', 6 | -------------------------------------------------------------------------------- /tests/wrap_chars/tex-fmt.toml: -------------------------------------------------------------------------------- 1 | wrap-chars = [",", ",", "。", ":", ";", "?"] 2 | -------------------------------------------------------------------------------- /tex-fmt.toml: -------------------------------------------------------------------------------- 1 | # tex-fmt.toml 2 | check = false 3 | print = false 4 | wrap = true 5 | wraplen = 80 6 | tabsize = 2 7 | tabchar = "space" 8 | stdin = false 9 | verbosity = "warn" 10 | lists = [] 11 | no-indent-envs = [] 12 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | tex-fmt | LaTeX formatter 8 | 9 | 124 | 125 | 126 | 127 | 128 |
129 | 130 | 131 |
132 |

tex-fmt

133 |

An extremely fast LaTeX formatter

134 |
135 | 136 | 137 | 138 |
139 |
140 | 141 | 142 | 145 | 146 |
147 | 148 | 149 |
150 | 151 |
152 |

Input

153 | 154 |
155 | 156 |
157 |

Output

158 | 159 |
160 | 161 |
162 | 163 | 164 |
165 | 166 |
167 |

Config

168 | 169 |
170 | 171 |
172 |

Logs

173 | 174 |
175 | 176 |
177 | 178 | 179 | 180 | 181 | 182 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /web/index.js: -------------------------------------------------------------------------------- 1 | // Import wasm 2 | import init, { main } from './pkg/tex_fmt.js'; 3 | 4 | // Initialize wasm 5 | (async () => { 6 | try { 7 | await init(); 8 | console.log('WASM initialized successfully.'); 9 | } catch (error) { 10 | console.error('Error initializing WASM :', error); 11 | alert('Failed to initialize WASM. Check console for details.'); 12 | } 13 | })(); 14 | 15 | // Submit button logic 16 | document.getElementById('formatButton').addEventListener( 17 | 'click', async () => { 18 | const copyMessage = document.getElementById('copyMessage'); 19 | copyMessage.innerText = "" 20 | const inputText = document.getElementById('inputText').value; 21 | const outputText = document.getElementById('outputText'); 22 | const logText = document.getElementById('logText'); 23 | try { 24 | const configText = document.getElementById('configText').value; 25 | const result = await main(inputText, configText); 26 | outputText.value = result.output; 27 | logText.value = result.logs; 28 | } catch (error) { 29 | console.error('Error calling WebAssembly function:', error); 30 | alert('An error occurred. Check the console for details.'); 31 | } 32 | } 33 | ); 34 | 35 | // Copy output text to clipboard 36 | document.getElementById('copyButton').addEventListener( 37 | 'click', () => { 38 | const outputText = document.getElementById('outputText'); 39 | outputText.select(); 40 | outputText.setSelectionRange(0, 99999); 41 | try { 42 | document.execCommand('copy'); 43 | const copyMessage = document.getElementById('copyMessage'); 44 | copyMessage.innerText = "Copied!" 45 | outputText.blur(); 46 | } catch (err) { 47 | console.error('Failed to copy text: ', err); 48 | } 49 | } 50 | ); 51 | --------------------------------------------------------------------------------