├── fixtures ├── qelectrotech.1 ├── docker-rmi.fish ├── docker-rmi.1.deroffed ├── docker-rmi.1 ├── qelectrotech.fish ├── qelectrotech.1.deroffed └── mlterm.fish ├── CODEOWNERS ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── Cargo.toml ├── src ├── util.rs ├── main.rs └── deroff.rs ├── CODE_OF_CONDUCT.md ├── README.md └── Cargo.lock /fixtures/qelectrotech.1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dc/fish-manpage-completions/HEAD/fixtures/qelectrotech.1 -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @scooter-dangle @PureW @necaris @kbknapp @danpaz @pickfire 2 | 3 | CODEOWNERS @scooter-dangle @PureW @necaris @kbknapp @danpaz 4 | 5 | .github/ @scooter-dangle @PureW @necaris @kbknapp @danpaz 6 | -------------------------------------------------------------------------------- /fixtures/docker-rmi.fish: -------------------------------------------------------------------------------- 1 | # docker-rmi 2 | # Autogenerated from man page docker-rmi.1 3 | complete -c docker-rmi -s f -l force --description ' Force removal of the image.' 4 | complete -c docker-rmi -s h -l help --description ' help for rmi.' 5 | complete -c docker-rmi -l no-prune --description ' Do not delete untagged parents SEE ALSO.' 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /fixtures/docker-rmi.1.deroffed: -------------------------------------------------------------------------------- 1 | 2 | NAME 3 | 4 | docker-rmi - Remove one or more images 5 | SYNOPSIS 6 | 7 | docker rmi [OPTIONS] IMAGE [IMAGE...] 8 | DESCRIPTION 9 | 10 | Alias for docker image rm. 11 | OPTIONS 12 | 13 | -f, --force[=false] 14 | Force removal of the image 15 | -h, --help[=false] 16 | help for rmi 17 | --no-prune[=false] 18 | Do not delete untagged parents 19 | SEE ALSO 20 | 21 | docker(1) 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: cargo 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fish-manpage-completions" 3 | version = "0.1.0" 4 | authors = ["Scott Steele "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | structopt = "0.3.26" 9 | itertools = "0.10" 10 | regex = "1.9.3" 11 | lazy_static = "1.4.0" 12 | bstr = "1.9.0" 13 | flate2 = "1.0.26" 14 | bzip2 = "0.4.4" 15 | xz2 = "0.1.7" 16 | dirs = "5" 17 | tracing = "0.1.37" 18 | tracing-subscriber = "0.3" 19 | indicatif = { version = "0.17.3", features = ["rayon"] } 20 | rayon = "1.7.0" 21 | clap = { version = "4.3.8", features = ["derive"] } 22 | clap_complete = "4.1.5" 23 | 24 | [dev-dependencies] 25 | pretty_assertions = "1" 26 | -------------------------------------------------------------------------------- /fixtures/docker-rmi.1: -------------------------------------------------------------------------------- 1 | .TH "DOCKER" "1" "Feb 2019" "Docker Community" "" 2 | .nh 3 | .ad l 4 | 5 | 6 | .SH NAME 7 | .PP 8 | docker\-rmi \- Remove one or more images 9 | 10 | 11 | .SH SYNOPSIS 12 | .PP 13 | \fBdocker rmi [OPTIONS] IMAGE [IMAGE...]\fP 14 | 15 | 16 | .SH DESCRIPTION 17 | .PP 18 | Alias for \fB\fCdocker image rm\fR\&. 19 | 20 | 21 | .SH OPTIONS 22 | .PP 23 | \fB\-f\fP, \fB\-\-force\fP[=false] 24 | Force removal of the image 25 | 26 | .PP 27 | \fB\-h\fP, \fB\-\-help\fP[=false] 28 | help for rmi 29 | 30 | .PP 31 | \fB\-\-no\-prune\fP[=false] 32 | Do not delete untagged parents 33 | 34 | 35 | .SH SEE ALSO 36 | .PP 37 | \fBdocker(1)\fP 38 | -------------------------------------------------------------------------------- /fixtures/qelectrotech.fish: -------------------------------------------------------------------------------- 1 | # qelectrotech 2 | # Autogenerated from man page qelectrotech.1 3 | complete -c qelectrotech -l common-elements-dir --description 'Utilise le dossier REP comme racine de la collection d\'éléments commune.' 4 | complete -c qelectrotech -l config-dir --description 'Utilise le dossier REP comme dossier de configuration de l\'utilisateur couran…' 5 | complete -c qelectrotech -l lang-dir --description 'Recherche les fichiers de traduction de l\'application dans le dossier REP.' 6 | complete -c qelectrotech -l help --description 'Affiche une courte description des options disponibles.' 7 | complete -c qelectrotech -s v -l version --description 'Affiche la version de l\'application (exemple : 0. 1).' 8 | complete -c qelectrotech -l license --description 'Affiche la licence de l\'application (GNU/GPL).' 9 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | SCCACHE_REGION: us-east-1 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | 20 | - name: Format 21 | env: 22 | SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }} 23 | SCCACHE_AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_AWS_ACCESS_KEY_ID }} 24 | SCCACHE_AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_AWS_SECRET_ACCESS_KEY }} 25 | run: | 26 | docker run \ 27 | --rm \ 28 | --tty \ 29 | --volume "$PWD:/code" \ 30 | --workdir /code \ 31 | --env SCCACHE_REGION \ 32 | --env SCCACHE_BUCKET \ 33 | --env SCCACHE_AWS_SECRET_ACCESS_KEY \ 34 | --env SCCACHE_AWS_ACCESS_KEY_ID \ 35 | scoots/rust-con-sccache:latest \ 36 | cargo fmt --all -- --check --verbose 37 | 38 | - name: Test 39 | env: 40 | SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }} 41 | SCCACHE_AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_AWS_ACCESS_KEY_ID }} 42 | SCCACHE_AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_AWS_SECRET_ACCESS_KEY }} 43 | run: | 44 | docker run \ 45 | --rm \ 46 | --tty \ 47 | --volume "$PWD:/code" \ 48 | --workdir /code \ 49 | --env SCCACHE_REGION \ 50 | --env SCCACHE_BUCKET \ 51 | --env SCCACHE_AWS_SECRET_ACCESS_KEY \ 52 | --env SCCACHE_AWS_ACCESS_KEY_ID \ 53 | scoots/rust-con-sccache:latest \ 54 | bash -c 'cargo test --verbose --all && sccache-wrapper.sh --show-stats' 55 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! regex { 3 | ($pattern: expr) => {{ 4 | lazy_static::lazy_static! { 5 | static ref REGEX: regex::Regex = regex::Regex::new($pattern).unwrap(); 6 | } 7 | ®EX 8 | }}; 9 | } 10 | 11 | use crate::char_len; 12 | use std::{collections::HashMap, iter::FromIterator}; 13 | 14 | pub struct TranslationTable { 15 | table: HashMap, 16 | } 17 | impl TranslationTable { 18 | pub fn new(f: &str, t: &str) -> Result { 19 | if char_len(f) != char_len(t) { 20 | Err("Arguments passed to `TranslationTable::new` must have equal character length") 21 | } else { 22 | Ok(TranslationTable { 23 | table: HashMap::from_iter(f.chars().zip(t.chars())), 24 | }) 25 | } 26 | } 27 | 28 | pub fn translate(&self, s: &str) -> String { 29 | s.chars() 30 | .map(|c| *self.table.get(&c).unwrap_or(&c)) 31 | .collect() 32 | } 33 | } 34 | 35 | #[test] 36 | fn test_translationtable_new() { 37 | assert!(TranslationTable::new("aaa", "Incorrect Length").is_err()); 38 | assert!(TranslationTable::new("aaa", "bbb").is_ok()); 39 | let tr = TranslationTable::new("ab!", "cd.").unwrap(); 40 | 41 | let mut expected = HashMap::new(); 42 | expected.insert('a', 'c'); 43 | expected.insert('b', 'd'); 44 | expected.insert('!', '.'); 45 | 46 | assert_eq!(tr.table, expected); 47 | 48 | // Unicode tests 49 | let tr = TranslationTable::new("🗻🚀🚁", "mrh").unwrap(); 50 | let mut expected = HashMap::new(); 51 | expected.insert('🗻', 'm'); 52 | expected.insert('🚀', 'r'); 53 | expected.insert('🚁', 'h'); 54 | 55 | assert_eq!(tr.table, expected); 56 | } 57 | 58 | #[test] 59 | fn test_translationtable_translate() { 60 | let tr = TranslationTable::new("ab!", "cd.").unwrap(); 61 | 62 | assert_eq!(tr.translate("aabb!!"), "ccdd..".to_owned()); 63 | 64 | assert_eq!(tr.translate("Hello World!"), "Hello World.".to_owned(),); 65 | 66 | assert_eq!(tr.translate("applebees!"), "cppledees.".to_owned(),); 67 | 68 | // Unicode tests 69 | let tr = TranslationTable::new("🗻🚀🚁", "mrh").unwrap(); 70 | 71 | assert_eq!( 72 | tr.translate("This 🗻 is a mountain!"), 73 | "This m is a mountain!".to_owned(), 74 | ); 75 | 76 | assert_eq!( 77 | tr.translate("This 🚀 is a rocket! (rocket.rs :))"), 78 | "This r is a rocket! (rocket.rs :))".to_owned(), 79 | ); 80 | 81 | assert_eq!( 82 | tr.translate("This 🚁 is a helicopter!"), 83 | "This h is a helicopter!".to_owned(), 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /fixtures/qelectrotech.1.deroffed: -------------------------------------------------------------------------------- 1 | 2 | NOM 3 | qelectrotech - Éditeur de schémas électriques 4 | SYNOPSIS 5 | qelectrotech 6 | [--common-elements-dir=REP] 7 | [--config-dir=REP] 8 | [--lang-dir=REP] 9 | [--help] 10 | [-v|--version] 11 | [--license] 12 | [FICHIER]... 13 | 14 | DESCRIPTION 15 | QElectroTech est un éditeur de schémas électriques. Les schémas (*.qet) et les éléments électriques (*.elmt) sont enregistrés au format XML. 16 | Les éléments disposables sur le schéma peuvent provenir de la collection commune ou de la collection utilisateur. 17 | Typiquement, la collection commune est accessible à tous les utilisateurs mais elle n'est pas éditable par eux. 18 | La collection utilisateur est propre à chaque utilisateur et peut être modifiée comme bon lui semble. 19 | OPTIONS 20 | 21 | --common-elements-dir=REP 22 | Utilise le dossier REP comme racine de la collection d'éléments commune. Note : cette option n'est activée que si la directive QET_ALLOW_OVERRIDE_CED_OPTION a été spécifiée durant la compilation. 23 | 24 | --config-dir=REP 25 | Utilise le dossier REP comme dossier de configuration de l'utilisateur courant. Ce dossier accueille un fichier qelectrotech.conf contenant la configuration de l'application et un sous-dossier elements contenant la collection d'éléments de l'utilisateur. Note : cette option n'est activée que si la directive QET_ALLOW_OVERRIDE_CD_OPTION a été spécifiée durant la compilation. 26 | 27 | --lang-dir=REP 28 | Recherche les fichiers de traduction de l'application dans le dossier REP. 29 | 30 | --help 31 | Affiche une courte description des options disponibles 32 | 33 | -v, --version 34 | Affiche la version de l'application (exemple : 0.1). 35 | 36 | --license 37 | Affiche la licence de l'application (GNU/GPL). 38 | À noter que si l'une des trois dernières options est specifiée dans la ligne de commande, le programme s'arrête après affichage de l'information correspondante. 39 | Si une instance de l'application a déjà été lancée par l'utilisateur, c'est celle-ci qui prendra en compte la ligne de commande, et notamment les fichiers à ouvrir. 40 | Les options redéfinissant les dossiers (collection commune, répertoire de configuration et fichiers de langue) ne seront toutefois pas pris en compte. 41 | Si le nom d'un fichier à ouvrir se termine par .elmt, QElectroTech essaiera de l'ouvrir dans un éditeur d'élément. 42 | Autrement, il les considérera comme des schémas. 43 | AUTEURS 44 | Benoît Ansieau 45 | 46 | Xavier Guerrin 47 | 48 | Laurent Trinques 49 | 50 | Joshua Claveau 51 | 52 | Cyril.frausti 53 | SIGNALER DES BUGS 54 | Si vous rencontrez un comportement qui vous paraît anormal dans l'application, consultez notre FAQ et notre BugTracker pour voir si le problème n'est pas déjà connu. Dans la négative, soumettez un rapport de bug via le BugTracker. 55 | 56 | COPYRIGHT 57 | Copyright © Les développeurs de QElectroTech 58 | 59 | Licence : GNU/GPL v2+ : 60 | 61 | Ce programme est un logiciel libre. Vous pouvez le modifier et le redistribuer. Il est fourni tel quel et SANS AUCUNE GARANTIE. 62 | 63 | VOIR AUSSI 64 | Site officiel : 65 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at rust.dc.meetup@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fish-manpage-completions 2 | ======================== 3 | 4 | ![Build Status](https://github.com/rust-dc/fish-manpage-completions/actions/workflows/main.yml/badge.svg) 5 | 6 | Port! Not Starboard! From Python to Rust! 7 | 8 | Current status 9 | -------------- 10 | 11 | It works(!), although there might be minor recent enhancements to the upstream Python original missing from this version. 12 | 13 | ### Install 14 | 15 | Having already installed Rust as noted below, run 16 | 17 | ```sh 18 | cargo install --locked --path . 19 | ``` 20 | 21 | at the top-level of this repo. 22 | 23 | ### Using 24 | 25 | Once installed, run 26 | 27 | ```sh 28 | fish-manpage-completions \ 29 | --manpath \ 30 | --directory ~/.local/share/fish/generated_completions \ 31 | --progress 32 | ``` 33 | 34 | You can also, of course, redefine fish's built-in `fish_update_completions` function to use this rather than the original Python source. 35 | 36 | ```fish 37 | function fish_update_completions 38 | fish-manpage-completions \ 39 | --manpath \ 40 | --directory ~/.local/share/fish/generated_completions \ 41 | --progress 42 | end 43 | 44 | funcsave fish_update_completions 45 | ``` 46 | 47 | Contributing 48 | ------------ 49 | 50 | For in-depth discussion of a particularly hairy function, open a dedicated issue 51 | or pipe up on [the RustDC Zulip chat](https://rust-dc.zulipchat.com/). 52 | 53 | Getting Started 54 | --------------- 55 | 56 | Cargo and rustc are a prerequisite, to install check https://rustup.rs/. 57 | 58 | To build and run tests. 59 | 60 | ```fish 61 | cargo test 62 | ``` 63 | 64 | Examples 65 | -------- 66 | 67 | The following initial snippet of the `tr` program's manpage 68 | 69 | ```roff 70 | .Dd July 23, 2004 71 | .Dt TR 1 72 | .Os 73 | .Sh NAME 74 | .Nm tr 75 | .Nd translate characters 76 | .Sh SYNOPSIS 77 | .Nm 78 | .Op Fl Ccsu 79 | .Ar string1 string2 80 | .Nm 81 | .Op Fl Ccu 82 | .Fl d 83 | .Ar string1 84 | .Nm 85 | .Op Fl Ccu 86 | .Fl s 87 | .Ar string1 88 | .Nm 89 | .Op Fl Ccu 90 | .Fl ds 91 | .Ar string1 string2 92 | .Sh DESCRIPTION 93 | The 94 | .Nm 95 | utility copies the standard input to the standard output with substitution 96 | or deletion of selected characters. 97 | .Pp 98 | The following options are available: 99 | .Bl -tag -width Ds 100 | .It Fl C 101 | Complement the set of characters in 102 | .Ar string1 , 103 | that is 104 | .Dq Fl C Li ab 105 | includes every character except for 106 | .Ql a 107 | and 108 | .Ql b . 109 | .It Fl c 110 | Same as 111 | .Fl C 112 | but complement the set of values in 113 | .Ar string1 . 114 | .It Fl d 115 | Delete characters in 116 | .Ar string1 117 | from the input. 118 | .It Fl s 119 | Squeeze multiple occurrences of the characters listed in the last 120 | operand (either 121 | .Ar string1 122 | or 123 | .Ar string2 ) 124 | in the input into a single instance of the character. 125 | This occurs after all deletion and translation is completed. 126 | .It Fl u 127 | Guarantee that any output is unbuffered. 128 | ``` 129 | 130 | is parsed, and the description extracted to form part of the simple completion 131 | available. 132 | 133 | Piped through 134 | 135 | ```sh 136 | cat PATH/TO/TR.1 | cargo run -- --stdout 137 | ``` 138 | 139 | will eventually (when we get enough functionality ported) yield the following 140 | 141 | ```fish 142 | # Autogenerated from man page STDIN 143 | complete -c tr -s u --description 'Guarantee that any output is unbuffered.' 144 | 145 | ``` 146 | 147 | Passing this as source into fish will cause this description to be displayed 148 | when fish shows `tr` as an auto-completeable command. 149 | -------------------------------------------------------------------------------- /fixtures/mlterm.fish: -------------------------------------------------------------------------------- 1 | # mlterm 2 | # Autogenerated from man page mlterm.1 3 | complete -c mlterm -s A -l aa --description 'Use anti-aliased fonts. This option works only with Xft or cairo for now.' 4 | complete -c mlterm -s B -l sbbg --description 'Specify a background color of a scrollbar.' 5 | complete -c mlterm -s C -l ctl --description 'Enable complex text layouting on UTF8 encoding to support indic scripts and R…' 6 | complete -c mlterm -s E -l km --description 'Specify encoding.' 7 | complete -c mlterm -s F -l sbfg --description 'Specify a foreground color of a scrollbar.' 8 | complete -c mlterm -s G -l vertical --description 'Specify vertical writing mode.' 9 | complete -c mlterm -s H -l bright --description 'Brightness of background images in percent.' 10 | complete -c mlterm -s I -l icon --description 'Specify a name to be used when a mlterm window is iconified.' 11 | complete -c mlterm -s J -l dyncomb --description 'Enable dynamic character combining.' 12 | complete -c mlterm -s K -l metakey --description 'Specify a key to be interpreted as a META key.' 13 | complete -c mlterm -s L -l ls --description 'Whether to use login shell or not. The default is false.' 14 | complete -c mlterm -s M -l im --description 'Specify an input method. (See doc/ja/README. ja in detail) . PP .' 15 | complete -c mlterm -s N -l name --description 'Specify application name. The default is "mlterm".' 16 | complete -c mlterm -s O -l sbmod --description 'Specify the side to show a scrollbar.' 17 | complete -c mlterm -s P -l clip --description 'Whether to enable CLIPBOARD (not only PRIMARY) selection.' 18 | complete -c mlterm -s Q -l vcur --description 'Change interpretation of cursor keys to be natural in vertical writing mode.' 19 | complete -c mlterm -s R -l fsrange --description 'Set acceptable range of font size.' 20 | complete -c mlterm -s S -l sbview --description 'Select a type of scrollbar. See SCROLLBAR section below for details.' 21 | complete -c mlterm -s T -l title --description 'Specify a title for a mlterm window. The default is "mlterm".' 22 | complete -c mlterm -s U -l viaucs --description 'Force to convert a selection (i. e.' 23 | complete -c mlterm -s V -l varwidth --description 'Use variable column width.' 24 | complete -c mlterm -s W -l sep --description 'Delimiter characters used for word selection, which are consulted when you do…' 25 | complete -c mlterm -s X -l alpha --description 'Alpha in pseudo or true transparent. The default is 255.' 26 | complete -c mlterm -s Y -l decsp --description 'Use dynamically composed line drawing character set of DEC special.' 27 | complete -c mlterm -s Z -l multicol --description 'Treat fullwidth characters (east Asian characters in most cases; which occupi…' 28 | complete -c mlterm -s 0 -l crbg --description 'Specify background color for cursor (default is same to foreground color).' 29 | complete -c mlterm -s 1 -l wscr --description 'Specify actual window width, by percentage against calculated value by multip…' 30 | complete -c mlterm -s 3 -l contrast --description 'Contrast of background image in percent.' 31 | complete -c mlterm -s 4 -l gamma --description 'Gamma of background image in percent.' 32 | complete -c mlterm -s 5 -l big5bug --description 'Enable a workaround for Big5 CTEXT bugs (which had been existed until XFree86…' 33 | complete -c mlterm -s 6 -l stbs --description 'Don\'t exit backscroll mode when console applications output something.' 34 | complete -c mlterm -s 7 -l bel --description 'Behavior when BEL (0x07) is received.' 35 | complete -c mlterm -s 8 -l 88591 --description 'Use ISO8859-1 fonts for US-ASCII part of various encodings.' 36 | complete -c mlterm -s 9 -l crfg --description 'Specify foreground color for cursor (default is same to background color).' 37 | complete -c mlterm -s '$' -l mc --description 'Doubleclick/tripleclick interval in millisecond. The default is 250.' 38 | complete -c mlterm -s '%' -l logseq --description 'Enable logging.' 39 | complete -c mlterm -s '&' -l borderless --description 'Asks the window manager to use no decorations at all.' 40 | complete -c mlterm -s @ -l screens --description 'Specify number of screens (sessions) to be used in start up.' 41 | complete -c mlterm -s '*' -l type --description 'Specify the rendering engine to be used to draw fonts.' 42 | complete -c mlterm -s '#' -l initstr --description 'Specify a string to be automatically sent after initialization of session.' 43 | complete -c mlterm -s a -l ac --description 'Specify number of columns to be occupied by a Unicode\'s "EastAsianAmbiguous" …' 44 | complete -c mlterm -s b -l bg --description 'Specify background color (default white).' 45 | complete -c mlterm -s c -l cp932 --description 'Use CP932 mapping table to convert from JIS X 0208 to Unicode when displaying…' 46 | complete -c mlterm -s d -l display --description 'Specify X display to connect with.' 47 | complete -c mlterm -s e --description 'Invoke the command in the mlterm window.' 48 | complete -c mlterm -s f -l fg --description 'Foreground color (default black).' 49 | complete -c mlterm -s g -l geometry --description 'Specify size and position of the window; see X(7).' 50 | complete -c mlterm -s h -l help --description 'Show help messages.' 51 | complete -c mlterm -s i -l xim --description 'Whether to use XIM (X Input Method).' 52 | complete -c mlterm -s j -l daemon --description 'Start as a daemon process.' 53 | complete -c mlterm -s k -l meta --description 'Behavior of META key. esc for sending ESC and none for ignoring META key.' 54 | complete -c mlterm -s l -l sl --description 'Specify number of lines of backlog or "unlimited".' 55 | complete -c mlterm -s m -l comb --description 'Enable combining characters by overstriking glyphs (recommended for TIS-620, …' 56 | complete -c mlterm -s n -l noucsfont --description 'Use non-Unicode fonts even when mlterm encoding is UTF-8.' 57 | complete -c mlterm -s o -l lsp --description 'Specify number of extra pixels between lines. The default is 0.' 58 | complete -c mlterm -s p -l pic --description 'Path for a wallpaper (background) image.' 59 | complete -c mlterm -s q -l extkey --description 'Enable extended keys for backscroll mode. The default is false.' 60 | complete -c mlterm -s r -l fade --description 'Specify fading ratio for unfocused windows.' 61 | complete -c mlterm -s s -l mdi --description 'Whether to use multiple document interface. The default is true.' 62 | complete -c mlterm -s t -l transbg --description 'Whether to enable pseudo transparent background.' 63 | complete -c mlterm -s u -l onlyucsfont --description 'Use Unicode fonts even when mlterm encoding is not UTF-8.' 64 | complete -c mlterm -s v -l version --description 'Show version information.' 65 | complete -c mlterm -s w -l fontsize --description 'Specify font size in pixel. The default is 16.' 66 | complete -c mlterm -s x -l tw --description 'Specify tab width. The default is 8.' 67 | complete -c mlterm -s y -l term --description 'Specify terminal type, i. e. , the value of TERM variable.' 68 | complete -c mlterm -s z -l largesmall --description 'Specify the step of changing font size in pixel when you pushed "Font size la…' 69 | complete -c mlterm -l aafont --description 'Whether to use ~/. mlterm/*aafont configurations with the use of fontconfig.' 70 | complete -c mlterm -l ade --description 'Specify character encodings detected automatically.' 71 | complete -c mlterm -l auto --description 'Automatically detect appropriate character encoding from the encodings specif…' 72 | complete -c mlterm -l altbuf --description 'Whether to enable alternate screen buffer.' 73 | complete -c mlterm -l bc --description 'Whether to broadcast input or pasted characters to all ptys whose value of "i…' 74 | complete -c mlterm -l bd --description 'Specify the color to use to display bold characters.' 75 | complete -c mlterm -l bdfont --description 'Use bold font for characters with the bold attribute. The default is true.' 76 | complete -c mlterm -l bimode --description 'Specify bidi mode. Valid values are: normal, left and right.' 77 | complete -c mlterm -l bisep --description 'Specify separator characters to render bidi text.' 78 | complete -c mlterm -l bl --description 'Specify the color to use to display blinking characters.' 79 | complete -c mlterm -l blink --description 'Blink cursor. The default is false.' 80 | complete -c mlterm -l blpos --description 'Specify the position (offset from the default baseline) of baseline.' 81 | complete -c mlterm -l border --description 'Specify inner border width. The default is 2. The maximum value is 224.' 82 | complete -c mlterm -l boxdraw --description 'Use either unicode font or DEC Special font forcibly to draw box-drawing char…' 83 | complete -c mlterm -l ciphlist --description 'Specify ciphers (comma separated list) for encrypting the ssh session.' 84 | complete -c mlterm -l ckm --description 'Specify encoding of the console where mlterm-con works.' 85 | complete -c mlterm -l co --description 'Specify the color to use to display crossed-out characters.' 86 | complete -c mlterm -l colors --description 'Whether to recognize ANSI color change escape sequences. The default is true.' 87 | complete -c mlterm -l csc --description 'Specify the number of sixel graphics colors of the console where mlterm-con w…' 88 | complete -c mlterm -l csp --description 'Specify number of extra pixels between lines.' 89 | complete -c mlterm -l csz --description 'Specify cell width and height in pixel which mlterm-con uses if it doesn\'t ge…' 90 | complete -c mlterm -l da1 --description 'Specify primary device attributes string. The default is 63;1;2;3;4;7;29.' 91 | complete -c mlterm -l da2 --description 'Specify secondary device attributes string. The default is 24;279;0.' 92 | complete -c mlterm -l depth --description 'Specify visual depth.' 93 | complete -c mlterm -l deffont --description 'DEFAULT in ~/. mlterm/*font.' 94 | complete -c mlterm -l emoji --description 'Specify path of a directory where emoji image files exist or a open type emoj…' 95 | complete -c mlterm -l exitbs --description 'Whether to exit backscroll mode on receiving data from pty.' 96 | complete -c mlterm -l fullwidth --description 'Force full width regardless of EastAsianWidth. txt. e. g.' 97 | complete -c mlterm -l halfwidth --description 'Force half width regardless of EastAsianWidth. txt. e. g.' 98 | complete -c mlterm -l ibc --description 'Whether to ignore broadcasted characters. The default is false.' 99 | complete -c mlterm -l iconpath --description 'Specify the file to be used as a window icon.' 100 | complete -c mlterm -l it --description 'Specify the color to use to display italic characters.' 101 | complete -c mlterm -l itfont --description 'Use italic font for characters with the italic attribute.' 102 | complete -c mlterm -l keepalive --description 'Specify interval seconds to send keepalive message to ssh server.' 103 | complete -c mlterm -l lborder --description 'Specify inner border width of a layout manager. The default is 0.' 104 | complete -c mlterm -l ldd --description 'Embold glyphs by drawing doubly at 1 pixel leftward instead of rightward.' 105 | complete -c mlterm -l locale --description 'Specify locale. The default is "".' 106 | complete -c mlterm -l logmsg --description 'Enable logging messages of mlterm to ~/. mlterm/msg. log.' 107 | complete -c mlterm -l loecho --description 'Whether to use local echo mode or not. The default is false.' 108 | complete -c mlterm -l maxptys --description 'Specify maximum number of ptys (sessions) to be opened simultaneously.' 109 | complete -c mlterm -l metaprefix --description 'Specify prefix characters in pressing meta key if mod_meta_mode = esc.' 110 | complete -c mlterm -l multivram --description 'Whether to draw the wall picture on Text VRAM instead of Graphic VRAM to impr…' 111 | complete -c mlterm -l noul --description 'Don\'t draw underline. The default is false.' 112 | complete -c mlterm -l oft --description 'Specify features of glyph substitution.' 113 | complete -c mlterm -l osc52 --description 'Allow access to clipboard(selection) by OSC 52 sequence.' 114 | complete -c mlterm -l ost --description 'Specify script of glyph substitution. The default is flatn.' 115 | complete -c mlterm -l otl --description 'Whether to show substituting glyphs in open type fonts with the use of libotf…' 116 | complete -c mlterm -l parent --description 'Specify parent Window ID. The default is 0.' 117 | complete -c mlterm -l point --description 'Treat the value of -w option as point instead of pixel.' 118 | complete -c mlterm -l pubkey --description 'Specify public key file for ssh connection. The default is ~/. ssh/id_rsa.' 119 | complete -c mlterm -l privkey --description 'Specify private key file for ssh connection. The default is ~/.' 120 | complete -c mlterm -l rcn --description 'Reconnect to ssh server automatically in unexpected disconnection.' 121 | complete -c mlterm -l restart --description 'Whether to restart mlterm with all opened ptys except ssh if SIGSEGV, SIGBUS,…' 122 | complete -c mlterm -l rv --description 'Specify the color to use to display reverse characters.' 123 | complete -c mlterm -l scp --description 'Allow OSC 5379 scp. The default is false.' 124 | complete -c mlterm -l seqfmt --description 'Specify the format of logging vt100 sequence.' 125 | complete -c mlterm -l serv --description 'Specify a host you want to connect via ssh etc.' 126 | complete -c mlterm -l shortcut --description 'Whether to allow dynamic change of shortcut keys by OSC 5379 set_shortcut seq…' 127 | complete -c mlterm -l slp --description 'Whether to start mlterm with local pty instead of ssh connection.' 128 | complete -c mlterm -l trim --description 'Whether to trim new line characters at the end in pasting text.' 129 | complete -c mlterm -l ul --description 'Specify the color to use to display underlined characters.' 130 | complete -c mlterm -l ulpos --description 'Specify the position (offset from the baseline) of underline.' 131 | complete -c mlterm -l ucsnoconv --description 'Use unicode fonts partially regardless of -n option. e. g.' 132 | complete -c mlterm -l urgent --description 'Draw the user\'s attention when making a bell sound in the unfocused window.' 133 | complete -c mlterm -l uriword --description 'Select URI by double clicking it regardless of -W option.' 134 | complete -c mlterm -l vtcolor --description 'Set vt color mode.' 135 | complete -c mlterm -l working-directory --description 'Working directory.' 136 | complete -c mlterm -l x11 --description 'Enable x11 forwarding for ssh connection. The default is false.' 137 | complete -c mlterm -o H/--bright --description '.' 138 | complete -c mlterm -l '>' --description '"ISO-2022-compliant" means that the encoding can be regarded as a subset of I…' 139 | complete -c mlterm -l ----------------------------------------------------- --description 'encoding received selection how to process?.' 140 | complete -c mlterm -o q/--extkey --description '.' 141 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "aho-corasick" 13 | version = "1.0.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" 16 | dependencies = [ 17 | "memchr", 18 | ] 19 | 20 | [[package]] 21 | name = "ansi_term" 22 | version = "0.12.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 25 | dependencies = [ 26 | "winapi", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.3.2" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is-terminal", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.1" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.1" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.0.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 64 | dependencies = [ 65 | "windows-sys 0.48.0", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "1.0.1" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" 73 | dependencies = [ 74 | "anstyle", 75 | "windows-sys 0.48.0", 76 | ] 77 | 78 | [[package]] 79 | name = "atty" 80 | version = "0.2.14" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 83 | dependencies = [ 84 | "hermit-abi 0.1.19", 85 | "libc", 86 | "winapi", 87 | ] 88 | 89 | [[package]] 90 | name = "autocfg" 91 | version = "1.1.0" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 94 | 95 | [[package]] 96 | name = "bitflags" 97 | version = "1.3.2" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 100 | 101 | [[package]] 102 | name = "bstr" 103 | version = "1.9.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" 106 | dependencies = [ 107 | "memchr", 108 | "regex-automata 0.4.3", 109 | "serde", 110 | ] 111 | 112 | [[package]] 113 | name = "bzip2" 114 | version = "0.4.4" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" 117 | dependencies = [ 118 | "bzip2-sys", 119 | "libc", 120 | ] 121 | 122 | [[package]] 123 | name = "bzip2-sys" 124 | version = "0.1.11+1.0.8" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" 127 | dependencies = [ 128 | "cc", 129 | "libc", 130 | "pkg-config", 131 | ] 132 | 133 | [[package]] 134 | name = "cc" 135 | version = "1.0.79" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 138 | 139 | [[package]] 140 | name = "cfg-if" 141 | version = "1.0.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 144 | 145 | [[package]] 146 | name = "clap" 147 | version = "2.34.0" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 150 | dependencies = [ 151 | "ansi_term", 152 | "atty", 153 | "bitflags", 154 | "strsim 0.8.0", 155 | "textwrap", 156 | "unicode-width", 157 | "vec_map", 158 | ] 159 | 160 | [[package]] 161 | name = "clap" 162 | version = "4.3.8" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "d9394150f5b4273a1763355bd1c2ec54cc5a2593f790587bcd6b2c947cfa9211" 165 | dependencies = [ 166 | "clap_builder", 167 | "clap_derive", 168 | "once_cell", 169 | ] 170 | 171 | [[package]] 172 | name = "clap_builder" 173 | version = "4.3.8" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "9a78fbdd3cc2914ddf37ba444114bc7765bbdcb55ec9cbe6fa054f0137400717" 176 | dependencies = [ 177 | "anstream", 178 | "anstyle", 179 | "bitflags", 180 | "clap_lex", 181 | "strsim 0.10.0", 182 | ] 183 | 184 | [[package]] 185 | name = "clap_complete" 186 | version = "4.1.5" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "37686beaba5ac9f3ab01ee3172f792fc6ffdd685bfb9e63cfef02c0571a4e8e1" 189 | dependencies = [ 190 | "clap 4.3.8", 191 | ] 192 | 193 | [[package]] 194 | name = "clap_derive" 195 | version = "4.3.2" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" 198 | dependencies = [ 199 | "heck 0.4.1", 200 | "proc-macro2", 201 | "quote", 202 | "syn 2.0.10", 203 | ] 204 | 205 | [[package]] 206 | name = "clap_lex" 207 | version = "0.5.0" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" 210 | 211 | [[package]] 212 | name = "colorchoice" 213 | version = "1.0.0" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 216 | 217 | [[package]] 218 | name = "console" 219 | version = "0.15.5" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" 222 | dependencies = [ 223 | "encode_unicode", 224 | "lazy_static", 225 | "libc", 226 | "unicode-width", 227 | "windows-sys 0.42.0", 228 | ] 229 | 230 | [[package]] 231 | name = "crc32fast" 232 | version = "1.3.2" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 235 | dependencies = [ 236 | "cfg-if", 237 | ] 238 | 239 | [[package]] 240 | name = "crossbeam-channel" 241 | version = "0.5.6" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" 244 | dependencies = [ 245 | "cfg-if", 246 | "crossbeam-utils", 247 | ] 248 | 249 | [[package]] 250 | name = "crossbeam-deque" 251 | version = "0.8.2" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" 254 | dependencies = [ 255 | "cfg-if", 256 | "crossbeam-epoch", 257 | "crossbeam-utils", 258 | ] 259 | 260 | [[package]] 261 | name = "crossbeam-epoch" 262 | version = "0.9.13" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" 265 | dependencies = [ 266 | "autocfg", 267 | "cfg-if", 268 | "crossbeam-utils", 269 | "memoffset", 270 | "scopeguard", 271 | ] 272 | 273 | [[package]] 274 | name = "crossbeam-utils" 275 | version = "0.8.14" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" 278 | dependencies = [ 279 | "cfg-if", 280 | ] 281 | 282 | [[package]] 283 | name = "diff" 284 | version = "0.1.13" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 287 | 288 | [[package]] 289 | name = "dirs" 290 | version = "5.0.1" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 293 | dependencies = [ 294 | "dirs-sys", 295 | ] 296 | 297 | [[package]] 298 | name = "dirs-sys" 299 | version = "0.4.1" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 302 | dependencies = [ 303 | "libc", 304 | "option-ext", 305 | "redox_users", 306 | "windows-sys 0.48.0", 307 | ] 308 | 309 | [[package]] 310 | name = "either" 311 | version = "1.8.1" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 314 | 315 | [[package]] 316 | name = "encode_unicode" 317 | version = "0.3.6" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 320 | 321 | [[package]] 322 | name = "errno" 323 | version = "0.3.1" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" 326 | dependencies = [ 327 | "errno-dragonfly", 328 | "libc", 329 | "windows-sys 0.48.0", 330 | ] 331 | 332 | [[package]] 333 | name = "errno-dragonfly" 334 | version = "0.1.2" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 337 | dependencies = [ 338 | "cc", 339 | "libc", 340 | ] 341 | 342 | [[package]] 343 | name = "fish-manpage-completions" 344 | version = "0.1.0" 345 | dependencies = [ 346 | "bstr", 347 | "bzip2", 348 | "clap 4.3.8", 349 | "clap_complete", 350 | "dirs", 351 | "flate2", 352 | "indicatif", 353 | "itertools", 354 | "lazy_static", 355 | "pretty_assertions", 356 | "rayon", 357 | "regex", 358 | "structopt", 359 | "tracing", 360 | "tracing-subscriber", 361 | "xz2", 362 | ] 363 | 364 | [[package]] 365 | name = "flate2" 366 | version = "1.0.26" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" 369 | dependencies = [ 370 | "crc32fast", 371 | "miniz_oxide", 372 | ] 373 | 374 | [[package]] 375 | name = "getrandom" 376 | version = "0.2.8" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" 379 | dependencies = [ 380 | "cfg-if", 381 | "libc", 382 | "wasi", 383 | ] 384 | 385 | [[package]] 386 | name = "heck" 387 | version = "0.3.3" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 390 | dependencies = [ 391 | "unicode-segmentation", 392 | ] 393 | 394 | [[package]] 395 | name = "heck" 396 | version = "0.4.1" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 399 | 400 | [[package]] 401 | name = "hermit-abi" 402 | version = "0.1.19" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 405 | dependencies = [ 406 | "libc", 407 | ] 408 | 409 | [[package]] 410 | name = "hermit-abi" 411 | version = "0.2.6" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 414 | dependencies = [ 415 | "libc", 416 | ] 417 | 418 | [[package]] 419 | name = "hermit-abi" 420 | version = "0.3.1" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 423 | 424 | [[package]] 425 | name = "indicatif" 426 | version = "0.17.3" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729" 429 | dependencies = [ 430 | "console", 431 | "number_prefix", 432 | "portable-atomic", 433 | "rayon", 434 | "unicode-width", 435 | ] 436 | 437 | [[package]] 438 | name = "io-lifetimes" 439 | version = "1.0.5" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" 442 | dependencies = [ 443 | "libc", 444 | "windows-sys 0.45.0", 445 | ] 446 | 447 | [[package]] 448 | name = "is-terminal" 449 | version = "0.4.7" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" 452 | dependencies = [ 453 | "hermit-abi 0.3.1", 454 | "io-lifetimes", 455 | "rustix", 456 | "windows-sys 0.48.0", 457 | ] 458 | 459 | [[package]] 460 | name = "itertools" 461 | version = "0.10.5" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 464 | dependencies = [ 465 | "either", 466 | ] 467 | 468 | [[package]] 469 | name = "lazy_static" 470 | version = "1.4.0" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 473 | 474 | [[package]] 475 | name = "libc" 476 | version = "0.2.147" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 479 | 480 | [[package]] 481 | name = "linux-raw-sys" 482 | version = "0.3.8" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" 485 | 486 | [[package]] 487 | name = "log" 488 | version = "0.4.17" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 491 | dependencies = [ 492 | "cfg-if", 493 | ] 494 | 495 | [[package]] 496 | name = "lzma-sys" 497 | version = "0.1.20" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" 500 | dependencies = [ 501 | "cc", 502 | "libc", 503 | "pkg-config", 504 | ] 505 | 506 | [[package]] 507 | name = "memchr" 508 | version = "2.7.1" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 511 | 512 | [[package]] 513 | name = "memoffset" 514 | version = "0.7.1" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" 517 | dependencies = [ 518 | "autocfg", 519 | ] 520 | 521 | [[package]] 522 | name = "miniz_oxide" 523 | version = "0.7.1" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 526 | dependencies = [ 527 | "adler", 528 | ] 529 | 530 | [[package]] 531 | name = "nu-ansi-term" 532 | version = "0.46.0" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 535 | dependencies = [ 536 | "overload", 537 | "winapi", 538 | ] 539 | 540 | [[package]] 541 | name = "num_cpus" 542 | version = "1.15.0" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" 545 | dependencies = [ 546 | "hermit-abi 0.2.6", 547 | "libc", 548 | ] 549 | 550 | [[package]] 551 | name = "number_prefix" 552 | version = "0.4.0" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 555 | 556 | [[package]] 557 | name = "once_cell" 558 | version = "1.17.1" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 561 | 562 | [[package]] 563 | name = "option-ext" 564 | version = "0.2.0" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 567 | 568 | [[package]] 569 | name = "overload" 570 | version = "0.1.1" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 573 | 574 | [[package]] 575 | name = "pin-project-lite" 576 | version = "0.2.9" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 579 | 580 | [[package]] 581 | name = "pkg-config" 582 | version = "0.3.26" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 585 | 586 | [[package]] 587 | name = "portable-atomic" 588 | version = "0.3.19" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" 591 | 592 | [[package]] 593 | name = "pretty_assertions" 594 | version = "1.4.0" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" 597 | dependencies = [ 598 | "diff", 599 | "yansi", 600 | ] 601 | 602 | [[package]] 603 | name = "proc-macro-error" 604 | version = "1.0.4" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 607 | dependencies = [ 608 | "proc-macro-error-attr", 609 | "proc-macro2", 610 | "quote", 611 | "syn 1.0.107", 612 | "version_check", 613 | ] 614 | 615 | [[package]] 616 | name = "proc-macro-error-attr" 617 | version = "1.0.4" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 620 | dependencies = [ 621 | "proc-macro2", 622 | "quote", 623 | "version_check", 624 | ] 625 | 626 | [[package]] 627 | name = "proc-macro2" 628 | version = "1.0.54" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534" 631 | dependencies = [ 632 | "unicode-ident", 633 | ] 634 | 635 | [[package]] 636 | name = "quote" 637 | version = "1.0.26" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" 640 | dependencies = [ 641 | "proc-macro2", 642 | ] 643 | 644 | [[package]] 645 | name = "rayon" 646 | version = "1.7.0" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" 649 | dependencies = [ 650 | "either", 651 | "rayon-core", 652 | ] 653 | 654 | [[package]] 655 | name = "rayon-core" 656 | version = "1.11.0" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" 659 | dependencies = [ 660 | "crossbeam-channel", 661 | "crossbeam-deque", 662 | "crossbeam-utils", 663 | "num_cpus", 664 | ] 665 | 666 | [[package]] 667 | name = "redox_syscall" 668 | version = "0.2.16" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 671 | dependencies = [ 672 | "bitflags", 673 | ] 674 | 675 | [[package]] 676 | name = "redox_users" 677 | version = "0.4.3" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" 680 | dependencies = [ 681 | "getrandom", 682 | "redox_syscall", 683 | "thiserror", 684 | ] 685 | 686 | [[package]] 687 | name = "regex" 688 | version = "1.9.3" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" 691 | dependencies = [ 692 | "aho-corasick", 693 | "memchr", 694 | "regex-automata 0.3.6", 695 | "regex-syntax", 696 | ] 697 | 698 | [[package]] 699 | name = "regex-automata" 700 | version = "0.3.6" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" 703 | dependencies = [ 704 | "aho-corasick", 705 | "memchr", 706 | "regex-syntax", 707 | ] 708 | 709 | [[package]] 710 | name = "regex-automata" 711 | version = "0.4.3" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" 714 | 715 | [[package]] 716 | name = "regex-syntax" 717 | version = "0.7.4" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" 720 | 721 | [[package]] 722 | name = "rustix" 723 | version = "0.37.7" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d" 726 | dependencies = [ 727 | "bitflags", 728 | "errno", 729 | "io-lifetimes", 730 | "libc", 731 | "linux-raw-sys", 732 | "windows-sys 0.45.0", 733 | ] 734 | 735 | [[package]] 736 | name = "scopeguard" 737 | version = "1.1.0" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 740 | 741 | [[package]] 742 | name = "serde" 743 | version = "1.0.152" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" 746 | 747 | [[package]] 748 | name = "sharded-slab" 749 | version = "0.1.4" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 752 | dependencies = [ 753 | "lazy_static", 754 | ] 755 | 756 | [[package]] 757 | name = "smallvec" 758 | version = "1.10.0" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 761 | 762 | [[package]] 763 | name = "strsim" 764 | version = "0.8.0" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 767 | 768 | [[package]] 769 | name = "strsim" 770 | version = "0.10.0" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 773 | 774 | [[package]] 775 | name = "structopt" 776 | version = "0.3.26" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" 779 | dependencies = [ 780 | "clap 2.34.0", 781 | "lazy_static", 782 | "structopt-derive", 783 | ] 784 | 785 | [[package]] 786 | name = "structopt-derive" 787 | version = "0.4.18" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" 790 | dependencies = [ 791 | "heck 0.3.3", 792 | "proc-macro-error", 793 | "proc-macro2", 794 | "quote", 795 | "syn 1.0.107", 796 | ] 797 | 798 | [[package]] 799 | name = "syn" 800 | version = "1.0.107" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 803 | dependencies = [ 804 | "proc-macro2", 805 | "quote", 806 | "unicode-ident", 807 | ] 808 | 809 | [[package]] 810 | name = "syn" 811 | version = "2.0.10" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "5aad1363ed6d37b84299588d62d3a7d95b5a5c2d9aad5c85609fda12afaa1f40" 814 | dependencies = [ 815 | "proc-macro2", 816 | "quote", 817 | "unicode-ident", 818 | ] 819 | 820 | [[package]] 821 | name = "textwrap" 822 | version = "0.11.0" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 825 | dependencies = [ 826 | "unicode-width", 827 | ] 828 | 829 | [[package]] 830 | name = "thiserror" 831 | version = "1.0.38" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" 834 | dependencies = [ 835 | "thiserror-impl", 836 | ] 837 | 838 | [[package]] 839 | name = "thiserror-impl" 840 | version = "1.0.38" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" 843 | dependencies = [ 844 | "proc-macro2", 845 | "quote", 846 | "syn 1.0.107", 847 | ] 848 | 849 | [[package]] 850 | name = "thread_local" 851 | version = "1.1.7" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" 854 | dependencies = [ 855 | "cfg-if", 856 | "once_cell", 857 | ] 858 | 859 | [[package]] 860 | name = "tracing" 861 | version = "0.1.37" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 864 | dependencies = [ 865 | "cfg-if", 866 | "pin-project-lite", 867 | "tracing-attributes", 868 | "tracing-core", 869 | ] 870 | 871 | [[package]] 872 | name = "tracing-attributes" 873 | version = "0.1.23" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" 876 | dependencies = [ 877 | "proc-macro2", 878 | "quote", 879 | "syn 1.0.107", 880 | ] 881 | 882 | [[package]] 883 | name = "tracing-core" 884 | version = "0.1.30" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" 887 | dependencies = [ 888 | "once_cell", 889 | "valuable", 890 | ] 891 | 892 | [[package]] 893 | name = "tracing-log" 894 | version = "0.1.3" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" 897 | dependencies = [ 898 | "lazy_static", 899 | "log", 900 | "tracing-core", 901 | ] 902 | 903 | [[package]] 904 | name = "tracing-subscriber" 905 | version = "0.3.17" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" 908 | dependencies = [ 909 | "nu-ansi-term", 910 | "sharded-slab", 911 | "smallvec", 912 | "thread_local", 913 | "tracing-core", 914 | "tracing-log", 915 | ] 916 | 917 | [[package]] 918 | name = "unicode-ident" 919 | version = "1.0.6" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 922 | 923 | [[package]] 924 | name = "unicode-segmentation" 925 | version = "1.10.1" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 928 | 929 | [[package]] 930 | name = "unicode-width" 931 | version = "0.1.10" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 934 | 935 | [[package]] 936 | name = "utf8parse" 937 | version = "0.2.1" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 940 | 941 | [[package]] 942 | name = "valuable" 943 | version = "0.1.0" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 946 | 947 | [[package]] 948 | name = "vec_map" 949 | version = "0.8.2" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 952 | 953 | [[package]] 954 | name = "version_check" 955 | version = "0.9.4" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 958 | 959 | [[package]] 960 | name = "wasi" 961 | version = "0.11.0+wasi-snapshot-preview1" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 964 | 965 | [[package]] 966 | name = "winapi" 967 | version = "0.3.9" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 970 | dependencies = [ 971 | "winapi-i686-pc-windows-gnu", 972 | "winapi-x86_64-pc-windows-gnu", 973 | ] 974 | 975 | [[package]] 976 | name = "winapi-i686-pc-windows-gnu" 977 | version = "0.4.0" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 980 | 981 | [[package]] 982 | name = "winapi-x86_64-pc-windows-gnu" 983 | version = "0.4.0" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 986 | 987 | [[package]] 988 | name = "windows-sys" 989 | version = "0.42.0" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 992 | dependencies = [ 993 | "windows_aarch64_gnullvm 0.42.1", 994 | "windows_aarch64_msvc 0.42.1", 995 | "windows_i686_gnu 0.42.1", 996 | "windows_i686_msvc 0.42.1", 997 | "windows_x86_64_gnu 0.42.1", 998 | "windows_x86_64_gnullvm 0.42.1", 999 | "windows_x86_64_msvc 0.42.1", 1000 | ] 1001 | 1002 | [[package]] 1003 | name = "windows-sys" 1004 | version = "0.45.0" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1007 | dependencies = [ 1008 | "windows-targets 0.42.1", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "windows-sys" 1013 | version = "0.48.0" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1016 | dependencies = [ 1017 | "windows-targets 0.48.0", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "windows-targets" 1022 | version = "0.42.1" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" 1025 | dependencies = [ 1026 | "windows_aarch64_gnullvm 0.42.1", 1027 | "windows_aarch64_msvc 0.42.1", 1028 | "windows_i686_gnu 0.42.1", 1029 | "windows_i686_msvc 0.42.1", 1030 | "windows_x86_64_gnu 0.42.1", 1031 | "windows_x86_64_gnullvm 0.42.1", 1032 | "windows_x86_64_msvc 0.42.1", 1033 | ] 1034 | 1035 | [[package]] 1036 | name = "windows-targets" 1037 | version = "0.48.0" 1038 | source = "registry+https://github.com/rust-lang/crates.io-index" 1039 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 1040 | dependencies = [ 1041 | "windows_aarch64_gnullvm 0.48.0", 1042 | "windows_aarch64_msvc 0.48.0", 1043 | "windows_i686_gnu 0.48.0", 1044 | "windows_i686_msvc 0.48.0", 1045 | "windows_x86_64_gnu 0.48.0", 1046 | "windows_x86_64_gnullvm 0.48.0", 1047 | "windows_x86_64_msvc 0.48.0", 1048 | ] 1049 | 1050 | [[package]] 1051 | name = "windows_aarch64_gnullvm" 1052 | version = "0.42.1" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" 1055 | 1056 | [[package]] 1057 | name = "windows_aarch64_gnullvm" 1058 | version = "0.48.0" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 1061 | 1062 | [[package]] 1063 | name = "windows_aarch64_msvc" 1064 | version = "0.42.1" 1065 | source = "registry+https://github.com/rust-lang/crates.io-index" 1066 | checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" 1067 | 1068 | [[package]] 1069 | name = "windows_aarch64_msvc" 1070 | version = "0.48.0" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 1073 | 1074 | [[package]] 1075 | name = "windows_i686_gnu" 1076 | version = "0.42.1" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" 1079 | 1080 | [[package]] 1081 | name = "windows_i686_gnu" 1082 | version = "0.48.0" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 1085 | 1086 | [[package]] 1087 | name = "windows_i686_msvc" 1088 | version = "0.42.1" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" 1091 | 1092 | [[package]] 1093 | name = "windows_i686_msvc" 1094 | version = "0.48.0" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 1097 | 1098 | [[package]] 1099 | name = "windows_x86_64_gnu" 1100 | version = "0.42.1" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" 1103 | 1104 | [[package]] 1105 | name = "windows_x86_64_gnu" 1106 | version = "0.48.0" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 1109 | 1110 | [[package]] 1111 | name = "windows_x86_64_gnullvm" 1112 | version = "0.42.1" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" 1115 | 1116 | [[package]] 1117 | name = "windows_x86_64_gnullvm" 1118 | version = "0.48.0" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 1121 | 1122 | [[package]] 1123 | name = "windows_x86_64_msvc" 1124 | version = "0.42.1" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" 1127 | 1128 | [[package]] 1129 | name = "windows_x86_64_msvc" 1130 | version = "0.48.0" 1131 | source = "registry+https://github.com/rust-lang/crates.io-index" 1132 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 1133 | 1134 | [[package]] 1135 | name = "xz2" 1136 | version = "0.1.7" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" 1139 | dependencies = [ 1140 | "lzma-sys", 1141 | ] 1142 | 1143 | [[package]] 1144 | name = "yansi" 1145 | version = "0.5.1" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" 1148 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! A translation of https://github.com/fish-shell/fish-shell/blob/e7bfd1d71ca54df726a4f1ea14bd6b0957b75752/share/tools/create_manpage_completions.py 2 | //! 3 | //! Copyright/license of original Python source: 4 | //! 5 | //! Copyright (c) 2012, Siteshwar Vashisht 6 | //! All rights reserved. 7 | //! 8 | //! Redistribution and use in source and binary forms, with or without 9 | //! modification, are permitted provided that the following conditions 10 | //! are met: 11 | //! 12 | //! Redistributions of source code must retain the above copyright 13 | //! notice, this list of conditions and the following disclaimer. 14 | //! 15 | //! Redistributions in binary form must reproduce the above copyright 16 | //! notice, this list of conditions and the following disclaimer in the 17 | //! documentation and/or other materials provided with the distribution. 18 | //! 19 | //! THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | //! "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | //! LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 22 | //! FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 23 | //! COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 24 | //! INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | //! BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | //! LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | //! CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 28 | //! LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 29 | //! ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | //! POSSIBILITY OF SUCH DAMAGE. 31 | 32 | use std::collections::HashSet; 33 | use std::fs::{self, File}; 34 | use std::io::{self, BufRead, BufReader, Read, Write}; 35 | use std::os::unix::ffi::OsStrExt; 36 | use std::path::{Path, PathBuf}; 37 | use std::process::Command; 38 | use std::{env, fmt}; 39 | 40 | use bzip2::read::BzDecoder; 41 | use clap::{CommandFactory, Parser}; 42 | use flate2::read::GzDecoder; 43 | use indicatif::ParallelProgressIterator; 44 | use itertools::Itertools; 45 | use rayon::prelude::*; 46 | use tracing_subscriber::filter::LevelFilter; 47 | use xz2::read::XzDecoder; 48 | 49 | #[cfg(test)] 50 | use pretty_assertions::assert_eq; 51 | 52 | mod util; 53 | 54 | macro_rules! regex { 55 | ($pattern: expr) => {{ 56 | lazy_static::lazy_static! { 57 | static ref REGEX: regex::Regex = regex::Regex::new($pattern).unwrap(); 58 | } 59 | ®EX 60 | }}; 61 | } 62 | 63 | // def unquote_double_quotes(data): 64 | // if (len(data) < 2): 65 | // return data 66 | // if data[0] == '"' and data[len(data)-1] == '"': 67 | // data = data[1:len(data)-1] 68 | // return data 69 | 70 | fn unquote_double_quotes(data: &str) -> &str { 71 | if data.len() > 2 && (data.as_bytes()[0], *data.as_bytes().last().unwrap()) == (b'"', b'"') { 72 | &data[1..(data.len() - 1)] 73 | } else { 74 | data 75 | } 76 | } 77 | 78 | // def unquote_single_quotes(data): 79 | // if (len(data) < 2): 80 | // return data 81 | // if data[0] == '`' and data[len(data)-1] == '\'': 82 | // data = data[1:len(data)-1] 83 | // return data 84 | 85 | fn unquote_single_quotes(data: &str) -> &str { 86 | if data.len() > 2 && (data.as_bytes()[0], *data.as_bytes().last().unwrap()) == (b'\'', b'\'') { 87 | &data[1..(data.len() - 1)] 88 | } else { 89 | data 90 | } 91 | } 92 | 93 | fn fish_escape_single_quote(s: &str) -> String { 94 | if s.chars() 95 | .all(|c| c.is_ascii_alphanumeric() || "_+-|/:=@~".contains(c)) 96 | { 97 | String::from(s) 98 | } else { 99 | format!("'{}'", s.replace(r"\", r"\\").replace(r"'", r"\'")) 100 | } 101 | } 102 | 103 | // Rust port replaced with String::from_utf8_lossy(s.as_bytes()) 104 | // 105 | // # Make a string Unicode by attempting to decode it as latin-1, or UTF8. See #658 106 | // def lossy_unicode(s): 107 | // # All strings are unicode in Python 3 108 | // if IS_PY3 or isinstance(s, unicode): return s 109 | // try: 110 | // return s.decode('latin-1') 111 | // except UnicodeEncodeError: 112 | // pass 113 | // try: 114 | // return s.decode('utf-8') 115 | // except UnicodeEncodeError: 116 | // pass 117 | // return s.decode('latin-1', 'ignore') 118 | 119 | // #[test] 120 | // fn test_lossy_unicode() { 121 | // let bytes = b"123 456"; 122 | // let s = lossy_unicode(bytes); 123 | // assert_eq!("123 456", s); 124 | // 125 | // let bad_bytes = &[255]; 126 | // let bad_s = lossy_unicode(bad_bytes); 127 | // assert_eq!("�", bad_s); 128 | // } 129 | 130 | const MAX_DESCRIPTION_WIDTH: usize = 78; 131 | const TRUNCATION_SUFFIX: char = '…'; 132 | 133 | fn char_len(string: &str) -> usize { 134 | string.chars().count() 135 | } 136 | 137 | fn fish_options(options: &str, existing_options: &mut HashSet) -> Vec { 138 | let mut out = vec![]; 139 | 140 | for option in regex!(r###"[ ,"=\[\]]"###).split(options) { 141 | let option = regex!(r###"\[.*\]"###).replace_all(option, ""); 142 | let option = regex!( 143 | r###"(?x) 144 | ^ [ \t\r\n\[\](){}.,:!] 145 | | [ \t\r\n\[\](){}.,:!] $ 146 | "### 147 | ) 148 | .replace_all(&option, ""); 149 | 150 | if option == "-" || option == "--" { 151 | continue; 152 | } 153 | 154 | if regex!(r###"[{}()]"###).is_match(&option) { 155 | continue; 156 | } 157 | 158 | let (fish_opt, num_dashes) = if option.starts_with("--") { 159 | ("l", 2) 160 | } else if option.starts_with("-") { 161 | (if option.len() == 2 { "s" } else { "o" }, 1) 162 | } else { 163 | continue; 164 | }; 165 | 166 | let option = format!( 167 | "-{} {}", 168 | fish_opt, 169 | // Direct indexing of `option` won't panic due to how `num_dashes` 170 | // is calculated. (I promise!) 171 | fish_escape_single_quote(&option[num_dashes..]) 172 | ); 173 | 174 | if existing_options.insert(option.clone()) { 175 | out.push(option); 176 | } 177 | } 178 | 179 | out 180 | } 181 | 182 | #[test] 183 | fn test_fish_options() { 184 | use std::iter::once; 185 | 186 | let expected_output: Vec = vec!["-s f".into(), "-l force".into()]; 187 | let options = "-f, --force[=false]"; 188 | let mut existing_options: HashSet = Default::default(); 189 | assert_eq!( 190 | fish_options(options, &mut existing_options), 191 | expected_output 192 | ); 193 | assert_eq!( 194 | expected_output.iter().cloned().collect::>(), 195 | existing_options 196 | ); 197 | 198 | let expected_output: Vec = vec!["-s \'\\\'\'".into()]; 199 | let options = "-'"; 200 | let mut existing_options: HashSet = Default::default(); 201 | assert_eq!( 202 | fish_options(options, &mut existing_options), 203 | expected_output 204 | ); 205 | assert_eq!( 206 | expected_output.iter().cloned().collect::>(), 207 | existing_options 208 | ); 209 | 210 | let expected_output: Vec = vec!["-l force".into()]; 211 | let options = "-f, --force[=false]"; 212 | let mut existing_options: HashSet = Default::default(); 213 | existing_options.insert("-s f".into()); 214 | existing_options.insert("-l something".into()); 215 | assert_eq!( 216 | fish_options(options, &mut existing_options), 217 | expected_output 218 | ); 219 | assert_eq!( 220 | expected_output 221 | .iter() 222 | .cloned() 223 | .chain(once("-s f".to_string())) 224 | .chain(once("-l something".to_string())) 225 | .collect::>(), 226 | existing_options, 227 | ); 228 | } 229 | 230 | /// # Panics 231 | /// If `max_length` is zero. 232 | fn char_truncate_string(string: &str, max_length: usize, truncator: char) -> Cow { 233 | let mut char_indices = string 234 | .char_indices() 235 | .skip(max_length.checked_sub(1).unwrap()) 236 | .map(|(idx, _)| idx); 237 | 238 | let penultimate = char_indices.next(); 239 | 240 | if char_indices.next().is_some() { 241 | // Okay to unwrap since the item element _following_ `penultimate` is 242 | // `Some`. 243 | format!("{}{}", &string[0..penultimate.unwrap()], truncator).into() 244 | } else { 245 | string.into() 246 | } 247 | } 248 | 249 | #[test] 250 | fn test_string_truncation() { 251 | assert_eq!(char_truncate_string("abc", 3, '…'), "abc"); 252 | 253 | assert_eq!(char_truncate_string("abcd", 3, '…'), "ab…"); 254 | 255 | assert_eq!("ßbc".len(), 4); 256 | assert_eq!(char_truncate_string("ßbc", 3, '…'), "ßbc"); 257 | 258 | assert_eq!(char_truncate_string("abß", 3, '…'), "abß"); 259 | 260 | assert_eq!("aßbc".len(), 5); 261 | assert_eq!("aß…".len(), 6); 262 | assert_eq!(char_truncate_string("aßbc", 3, '…'), "aß…"); 263 | } 264 | 265 | fn truncated_description(description: &str) -> String { 266 | let sentences = description.replace(r"\'", "'").replace(r"\.", "."); 267 | 268 | let mut sentences = sentences 269 | .split(".") 270 | .filter(|sentence| !sentence.trim().is_empty()); 271 | 272 | let out = sentences.next().unwrap_or_default(); 273 | let mut out = format!("{}.", String::from_utf8_lossy(out.as_bytes())); 274 | let mut out_len = char_len(&out); 275 | 276 | if out_len > MAX_DESCRIPTION_WIDTH { 277 | out = char_truncate_string(&out, MAX_DESCRIPTION_WIDTH, TRUNCATION_SUFFIX).into_owned(); 278 | } else { 279 | for line in sentences { 280 | out_len += 1 // space 281 | + char_len(&line) 282 | + 1; // period 283 | if out_len > MAX_DESCRIPTION_WIDTH { 284 | break; 285 | } 286 | out = format!("{} {}.", out, String::from_utf8_lossy(line.as_bytes())); 287 | } 288 | } 289 | 290 | fish_escape_single_quote(&out.trim_end_matches('.')) 291 | } 292 | 293 | #[test] 294 | fn test_truncated_description() { 295 | assert_eq!(truncated_description(r"\'\."), r"'\''"); 296 | 297 | assert_eq!( 298 | truncated_description(r"Don't use this command."), 299 | r"'Don\'t use this command'" 300 | ); 301 | 302 | assert_eq!( 303 | truncated_description(r"Don't use this command. It's really dumb."), 304 | r"'Don\'t use this command. It\'s really dumb'" 305 | ); 306 | 307 | assert_eq!( 308 | truncated_description( 309 | r"The description for the command is so long. This second sentence will be dropped, in fact, because it is too long to be displayed comfortably." 310 | ), 311 | r"'The description for the command is so long'" 312 | ); 313 | 314 | assert_eq!( 315 | truncated_description( 316 | r"This single, initial sentence exceeds the `MAX_DESCRIPTION_WIDTH` and so it will not be displayed in its entirety, which is a crying shame." 317 | ), 318 | r"'This single, initial sentence exceeds the `MAX_DESCRIPTION_WIDTH` and so it w…'" 319 | ); 320 | 321 | assert_eq!( 322 | // Note: This behavior seems wrong. Should probably change to remove extra spaces. 323 | truncated_description( 324 | r" Dumb command. \It's really dumb\. Extra spaces aren\'t removed. " 325 | ), 326 | r"' Dumb command. \\It\'s really dumb. Extra spaces aren\'t removed'" 327 | ); 328 | } 329 | 330 | struct Completions<'a> { 331 | cmdname: &'a str, 332 | // TODO should we store the whole built_command here? 333 | built_command_output: Vec, 334 | existing_options: HashSet, 335 | } 336 | 337 | impl<'a> Completions<'a> { 338 | fn new(cmdname: &'a str) -> Completions { 339 | Completions { 340 | cmdname, 341 | built_command_output: Vec::new(), 342 | existing_options: HashSet::new(), 343 | } 344 | } 345 | 346 | fn add(&mut self, option_name: &str, option_desc: &str) { 347 | let fish_options = fish_options(option_name, &mut self.existing_options); 348 | 349 | if fish_options.is_empty() { 350 | return; 351 | } 352 | 353 | self.built_command_output.push(complete_command( 354 | &fish_escape_single_quote(self.cmdname), 355 | fish_options, 356 | &truncated_description(option_desc), 357 | )); 358 | } 359 | 360 | fn build(self) -> Option { 361 | let mut s = self.built_command_output.join("\n"); 362 | if s.is_empty() { 363 | None 364 | } else { 365 | s.push('\n'); // add trailing whitespace 366 | Some(s) 367 | } 368 | } 369 | } 370 | 371 | /// Generate fish `complete` command. 372 | fn complete_command(cmdname: &str, args: Vec, description: &str) -> String { 373 | let mut out = format!("complete -c {} {}", cmdname, args.join(" ")); 374 | if !description.is_empty() { 375 | out.push_str(" --description "); 376 | out.push_str(description); 377 | } 378 | out 379 | } 380 | 381 | #[test] 382 | fn test_complete_command() { 383 | assert_eq!( 384 | complete_command( 385 | "tr".into(), 386 | vec![ 387 | "-s".into(), 388 | "s".into(), 389 | "-l".into(), 390 | "squeeze-repeats".into(), 391 | ], 392 | "'replace each input sequence of a repeated character that is listed in SET1 …'", 393 | ), 394 | "complete \ 395 | -c tr \ 396 | -s s \ 397 | -l squeeze-repeats \ 398 | --description 'replace each input sequence of a repeated character that is listed in SET1 …'" 399 | ); 400 | 401 | assert_eq!( 402 | complete_command( 403 | "tr".into(), 404 | vec![ 405 | "-s".into(), 406 | "s".into(), 407 | "-l".into(), 408 | "squeeze-repeats".into(), 409 | ], 410 | "", 411 | ), 412 | "complete \ 413 | -c tr \ 414 | -s s \ 415 | -l squeeze-repeats" 416 | ); 417 | } 418 | 419 | fn remove_groff_formatting(data: &str) -> Cow { 420 | // // TODO Can we remove all of these strings in one go? 421 | // let mut data = data.to_owned(); 422 | // for marker in &[ 423 | // r"\fI", r"\fP", r"\f1", r"\fB", r"\fR", r"\e", r".BI", r".BR", r"0.5i", r".rb", r"\^", 424 | // r"{ ", r" }", ".B", 425 | // ".I", 426 | // // The next ones are odd. Putting them into a python file makes my 427 | // // python linter warn about anomalous backslash and python2 vs python3 428 | // // seems to make no difference 429 | // // data = data.replace("\ ","") 430 | // // data = data.replace("\-","-") 431 | // //"\ ", 432 | // //"\&", 433 | // //"\f", 434 | // ] { 435 | // data = data.replace(marker, ""); 436 | // } 437 | // // See note above about anomalous backslash 438 | // //data = data.replace("\-", "-"); 439 | // let data = regex!(r##".PD( \d+)"##).replace_all(&data, ""); 440 | // data.to_string() 441 | // using regex is twice as fast as manual replace 442 | let re1 = regex!( 443 | r"\\fI|\\fP|\\f1|\\fB|\\fR|\\e|\.BI|\.BR|0\.5i|\.rb|\\\^|\{ | \}|\.B|\.I|\f|(.PD( \d+))" 444 | ); 445 | let re2 = regex!(r"\\-"); 446 | let re3 = regex!(r"\(cq"); 447 | match re1.replace_all(&data, "") { 448 | Cow::Borrowed(s) => match re2.replace_all(&s, "-") { 449 | Cow::Borrowed(s) => re3.replace_all(&s, "'"), 450 | Cow::Owned(s) => Cow::Owned(re3.replace_all(&s, "'").into_owned()), 451 | }, 452 | Cow::Owned(s) => Cow::Owned(re3.replace_all(&re2.replace_all(&s, "-"), "'").into_owned()), 453 | } 454 | } 455 | 456 | #[test] 457 | fn test_remove_groff_formatting() { 458 | assert_eq!( 459 | remove_groff_formatting(r#"Foo\fIbar\fP Zoom.PD 325 Zoom"#), 460 | "Foobar Zoom Zoom" 461 | ); 462 | assert_eq!( 463 | remove_groff_formatting( 464 | r#"\n\\fB\\-\\-working\\-directory\\fR=\\fIvalue\\fR\nWorking directory.\n"# 465 | ), 466 | "\\n\\\\-\\-working\\-directory\\=\\value\\\\nWorking directory.\\n" 467 | ); 468 | } 469 | 470 | trait ManParser { 471 | fn is_my_type(&self, manpage: &str) -> bool; 472 | 473 | fn parse_man_page(&self, _manpage: &str, _cmdname: &str) -> Option; 474 | } 475 | 476 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 477 | struct Type1; 478 | 479 | impl ManParser for Type1 { 480 | fn is_my_type(&self, manpage: &str) -> bool { 481 | manpage.contains(r#".SH "OPTIONS""#) 482 | } 483 | 484 | fn parse_man_page(&self, manpage: &str, cmdname: &str) -> Option { 485 | let options_section_re = regex!(r#"\.SH "OPTIONS"((?s:.)*?)(\.SH|\z)"#); 486 | let options_section_matched = options_section_re.find(manpage); 487 | let mut options_section = options_section_matched.unwrap().as_str(); 488 | 489 | let options_parts_re = regex!(r"\.PP((?s:.)*?)\.RE"); 490 | let mut options_matched = options_parts_re.captures(options_section); 491 | tracing::info!("Command is {}", cmdname); 492 | 493 | if options_matched.is_none() { 494 | tracing::info!("Unable to find options"); 495 | return self 496 | .fallback(options_section, cmdname) 497 | .or_else(|| self.fallback2(options_section, cmdname)); 498 | } 499 | 500 | let mut completions = Completions::new(cmdname); 501 | while let Some(mat) = options_matched { 502 | let mut data = mat.get(1).unwrap().as_str(); 503 | let last_dotpp_index = data.rfind(".PP"); 504 | if let Some(idx) = last_dotpp_index { 505 | data = &data[idx + 3..]; 506 | } 507 | 508 | let data = remove_groff_formatting(data); 509 | if let Some((option_name, option_desc)) = data.splitn(2, ".RS 4").next_tuple::<(_, _)>() 510 | { 511 | let option_name = option_name.trim(); 512 | if option_name.contains('-') { 513 | let option_name = unquote_double_quotes(option_name); 514 | let option_name = unquote_single_quotes(option_name); 515 | let option_desc = option_desc.trim().replace('\n', " "); 516 | completions.add(option_name, &option_desc); 517 | } else { 518 | tracing::info!("{:?} doesn't contain '-'", option_name); 519 | } 520 | } else { 521 | tracing::info!("Unable to split option from description"); 522 | return None; 523 | } 524 | 525 | options_section = &options_section[mat.get(0).unwrap().end() - 3..]; 526 | options_matched = options_parts_re.captures(options_section); 527 | } 528 | completions.build() 529 | } 530 | } 531 | 532 | impl Type1 { 533 | fn fallback(&self, mut options_section: &str, cmdname: &str) -> Option { 534 | tracing::info!("Trying fallback"); 535 | let options_parts_re = regex!(r"\.TP( \d+)?((?s:.)*?)\.TP"); 536 | let mut options_matched = options_parts_re.captures(options_section); 537 | if options_matched.is_none() { 538 | tracing::info!("Still not found"); 539 | return None; 540 | } 541 | let mut completions = Completions::new(cmdname); 542 | while let Some(mat) = options_matched { 543 | let data = mat.get(2).unwrap().as_str(); 544 | let data = remove_groff_formatting(data); 545 | let data = data.splitn(2, '\n').next_tuple::<(_, _)>(); 546 | if data.filter(|data| !data.1.trim().is_empty()).is_none() { 547 | tracing::info!("Unable to split option from description"); 548 | return None; 549 | } 550 | let option_name = data.unwrap().0.trim(); 551 | if option_name.contains('-') { 552 | let option_name = unquote_double_quotes(option_name); 553 | let option_name = unquote_single_quotes(option_name); 554 | let option_desc = data.unwrap().1.trim().replace('\n', " "); 555 | completions.add(option_name, &option_desc); 556 | } else { 557 | tracing::info!("{:?} does not contain '-'", option_name); 558 | } 559 | // XXX possible to add fallback2 here 560 | 561 | options_section = &options_section[mat.get(0).unwrap().end() - 3..]; 562 | options_matched = options_parts_re.captures(options_section); 563 | } 564 | completions.build() 565 | } 566 | 567 | fn fallback2(&self, options_section: &str, cmdname: &str) -> Option { 568 | tracing::info!("Trying last chance fallback"); 569 | let ix_remover_re = regex!(r"\.IX.*"); 570 | let trailing_num_re = regex!(r"\d+$"); 571 | let options_parts_re = regex!(r"\.IP ((?s:.)*?)\.IP"); 572 | 573 | let mut options_section = &*ix_remover_re.replace_all(options_section, ""); 574 | let mut options_matched = options_parts_re.captures(&options_section); 575 | if options_matched.is_none() { 576 | tracing::info!("Still (still!) not found"); 577 | return None; 578 | } 579 | let mut completions = Completions::new(cmdname); 580 | while let Some(mat) = options_matched { 581 | let data = mat.get(1).unwrap().as_str(); 582 | let data = remove_groff_formatting(data); 583 | let data: Vec<&str> = data.splitn(2, '\n').collect(); 584 | if data.len() < 2 || data[1].trim().is_empty() { 585 | tracing::info!("Unable to split option from description"); 586 | return None; 587 | } 588 | let option_name = trailing_num_re.replace_all(data[0].trim(), ""); 589 | if option_name.contains('-') { 590 | let option_name = option_name.trim(); 591 | let option_name = unquote_double_quotes(option_name); 592 | let option_name = unquote_single_quotes(option_name); 593 | let option_desc = data[1].trim().replace('\n', " "); 594 | completions.add(option_name, &option_desc); 595 | } else { 596 | tracing::info!("{:?} doesn't contain '-'", option_name); 597 | } 598 | 599 | options_section = &options_section[mat.get(0).unwrap().end() - 3..]; 600 | options_matched = options_parts_re.captures(&options_section); 601 | } 602 | completions.build() 603 | } 604 | } 605 | 606 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 607 | struct Type2; 608 | 609 | impl ManParser for Type2 { 610 | fn is_my_type(&self, manpage: &str) -> bool { 611 | manpage.contains(".SH OPTIONS") 612 | } 613 | 614 | fn parse_man_page(&self, manpage: &str, cmdname: &str) -> Option { 615 | let options_section_re = regex!(r#"\.SH OPTIONS((?s:.)*?)(\.SH|\z)"#); 616 | let options_section_matched = options_section_re.captures(manpage); 617 | let mut options_section = options_section_matched.unwrap().get(1).unwrap().as_str(); 618 | 619 | let options_parts_re = regex!(r#"\.[IT]P( \d+(\.\d)?i?)?((?s:.)*?)\.([IT]P|UNINDENT)"#); 620 | let mut options_matched = options_parts_re.captures(options_section); 621 | tracing::info!("Command is {}", cmdname); 622 | 623 | if options_matched.is_none() { 624 | tracing::info!("Unable to find options"); 625 | return None; 626 | } 627 | 628 | let mut completions = Completions::new(cmdname); 629 | while let Some(mat) = options_matched { 630 | let data = mat.get(3).unwrap().as_str(); 631 | let data = remove_groff_formatting(data); 632 | let data = data.trim().splitn(2, '\n').next_tuple::<(_, _)>(); 633 | if let Some((option_name, option_desc)) = 634 | data.filter(|(_, desc)| !desc.trim().is_empty()) 635 | { 636 | let option_name = option_name.trim(); 637 | if option_name.contains('-') { 638 | let option_name = unquote_double_quotes(option_name); 639 | let option_name = unquote_single_quotes(option_name); 640 | let option_desc = option_desc.trim().replace('\n', " "); 641 | completions.add(option_name, &option_desc); 642 | } else { 643 | tracing::info!("{:?} doesn't contain '-'", option_name); 644 | } 645 | } else { 646 | tracing::info!("Unable to split option from description"); 647 | } 648 | 649 | options_section = &options_section[mat.get(0).unwrap().end() - 3..]; 650 | options_matched = options_parts_re.captures(options_section); 651 | } 652 | completions.build() 653 | // TODO not sure why but the original version never succeed here 654 | } 655 | } 656 | 657 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 658 | struct Type3; 659 | 660 | impl ManParser for Type3 { 661 | fn is_my_type(&self, manpage: &str) -> bool { 662 | manpage.contains(".SH DESCRIPTION") 663 | } 664 | 665 | fn parse_man_page(&self, manpage: &str, cmdname: &str) -> Option { 666 | let options_section_re = regex!(r"\.SH DESCRIPTION((?s:.)*?)(\.SH|\z)"); 667 | let options_section_matched = options_section_re.find(manpage); 668 | let mut options_section = options_section_matched.unwrap().as_str(); 669 | 670 | let options_parts_re = regex!(r"\.TP((?s:.)*?)\.TP"); 671 | let mut options_matched = options_parts_re.captures(options_section); 672 | tracing::info!("Command is {}", cmdname); 673 | 674 | if options_matched.is_none() { 675 | tracing::info!("Unable to find options section"); 676 | return None; 677 | } 678 | 679 | let mut completions = Completions::new(cmdname); 680 | while let Some(mat) = options_matched { 681 | let data = mat.get(1).unwrap().as_str(); 682 | 683 | let data = remove_groff_formatting(data); 684 | let data = data.trim(); 685 | let (option_name, option_desc) = match data.splitn(2, '\n').next_tuple() { 686 | Some(tuple) => tuple, 687 | None => { 688 | tracing::info!("Unable to split option from description"); 689 | return None; 690 | } 691 | }; 692 | let option_name = option_name.trim(); 693 | if option_name.contains('-') { 694 | let option_name = unquote_double_quotes(option_name); 695 | let option_name = unquote_single_quotes(option_name); 696 | let option_desc = option_desc.trim().replace("\n", " "); 697 | completions.add(&option_name, &option_desc); 698 | } else { 699 | tracing::info!("{:?} doesn't contain '-'", option_name); 700 | } 701 | 702 | options_section = &options_section[mat.get(0).unwrap().end() - 3..]; 703 | options_matched = options_parts_re.captures(&options_section); 704 | } 705 | completions.build() 706 | } 707 | } 708 | 709 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 710 | struct Type4; 711 | 712 | impl ManParser for Type4 { 713 | fn is_my_type(&self, manpage: &str) -> bool { 714 | manpage.contains(".SH FUNCTION LETTERS") 715 | } 716 | 717 | fn parse_man_page(&self, manpage: &str, cmdname: &str) -> Option { 718 | let options_section_re = regex!(r"\.SH FUNCTION LETTERS((?s:.)*?)(\.SH|\z)"); 719 | let options_section_matched = options_section_re.captures(manpage); 720 | let mut options_section = options_section_matched.unwrap().get(1).unwrap().as_str(); 721 | 722 | let options_parts_re = regex!(r"\.TP((?s:.)*?)\.TP"); 723 | let mut options_matched = options_parts_re.captures(options_section); 724 | tracing::info!("Command is {}", cmdname); 725 | 726 | if options_matched.is_none() { 727 | tracing::info!("Unable to find options section"); 728 | return None; 729 | } 730 | 731 | let mut completions = Completions::new(cmdname); 732 | while let Some(mat) = options_matched { 733 | let data = mat.get(1).unwrap().as_str(); 734 | let data = remove_groff_formatting(data); 735 | if let Some((option_name, option_desc)) = data.trim().splitn(2, '\n').next_tuple() { 736 | let option_name = option_name.trim(); 737 | if option_name.contains('-') { 738 | let option_name = unquote_double_quotes(option_name); 739 | let option_name = unquote_single_quotes(option_name); 740 | let option_desc = option_desc.trim().replace('\n', " "); 741 | completions.add(option_name, &option_desc); 742 | } else { 743 | tracing::info!("{} doesn't contain '-'", option_name); 744 | } 745 | } else { 746 | tracing::info!("Unable to split option from description"); 747 | return None; 748 | } 749 | 750 | options_section = &options_section[mat.get(0).unwrap().end() - 3..]; 751 | options_matched = options_parts_re.captures(options_section); 752 | } 753 | completions.build() 754 | } 755 | } 756 | 757 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 758 | struct TypeScdoc; 759 | 760 | impl ManParser for TypeScdoc { 761 | fn is_my_type(&self, manpage: &str) -> bool { 762 | regex!(r#"\.\\" Generated by scdoc(?s:.)?*\.SH OPTIONS"#).is_match(manpage) 763 | } 764 | 765 | fn parse_man_page(&self, manpage: &str, cmdname: &str) -> Option { 766 | let options_section_re = regex!(r"\.SH OPTIONS((?s:.)*?)\.SH"); 767 | let options_section_matched = options_section_re.captures(manpage); 768 | let mut options_section = options_section_matched.unwrap().get(1)?.as_str(); 769 | 770 | let options_parts_re = regex!(r"((?s:.)*?)\.RE"); 771 | let mut options_matched = options_parts_re.captures(options_section); 772 | tracing::info!("Command is {}", cmdname); 773 | 774 | if options_matched.is_none() { 775 | tracing::info!("Unable to find options section"); 776 | return None; 777 | } 778 | 779 | let mut completions = Completions::new(cmdname); 780 | while let Some(mat) = options_matched { 781 | let data = mat.get(1).unwrap().as_str(); 782 | let data = remove_groff_formatting(data); 783 | 784 | // Should be at least two lines, split name and desc, other lines ignored 785 | let lines = data.split('\n'); 786 | let mut iter = lines.filter(|s| !["", ".P", ".RS 4"].contains(s)); 787 | if let Some((option_name, option_desc)) = iter.next_tuple() { 788 | let option_name = unquote_double_quotes(option_name); 789 | let option_name = unquote_single_quotes(option_name); 790 | if !option_name.contains('-') { 791 | tracing::info!("{} doesn't contain '-'", option_name); 792 | } 793 | completions.add(option_name, option_desc); 794 | } else { 795 | tracing::info!("Unable to split option from description"); 796 | } 797 | 798 | options_section = &options_section[mat.get(0).unwrap().end()..]; 799 | options_matched = options_parts_re.captures(options_section); 800 | } 801 | completions.build() 802 | } 803 | } 804 | 805 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 806 | struct TypeDarwin; 807 | 808 | impl ManParser for TypeDarwin { 809 | fn is_my_type(&self, manpage: &str) -> bool { 810 | regex!(r##"\.S[hH] DESCRIPTION"##).is_match(manpage) 811 | } 812 | 813 | fn parse_man_page(&self, manpage: &str, cmdname: &str) -> Option { 814 | let mut lines = manpage.split_terminator("\n").skip_while(|cond| { 815 | !cond.starts_with(".Sh DESCRIPTION") || !cond.starts_with(".SH DESCRIPTION") 816 | }); 817 | 818 | let mut completions = Completions::new(cmdname); 819 | while let Some(line) = lines.next() { 820 | if !Self::is_option(line) { 821 | continue; 822 | } 823 | 824 | // Try to guess how many dashes this argument has 825 | let dash_count = Self::count_argument_dashes(line); 826 | 827 | let line = Self::groff_replace_escapes(line); 828 | let line = Self::trim_groff(&line); 829 | let line = line.trim(); 830 | if line.is_empty() { 831 | continue; 832 | } 833 | 834 | // Extract the name 835 | let name = line.split_whitespace().next().unwrap(); 836 | 837 | // Extract the description 838 | let desc = lines 839 | .by_ref() 840 | .take_while(|line| Self::is_option(line)) 841 | .filter(|line| line.starts_with(".") && !line.starts_with(".\"")) // Ignore comments 842 | .map(Self::groff_replace_escapes) 843 | .map(|line| Self::trim_groff(&line)) 844 | .filter(|line| !line.is_empty()) 845 | .collect::>() 846 | .join(" "); 847 | 848 | if name == "-" { 849 | // Skip double -- arguments 850 | continue; 851 | } 852 | let name = if name.len() == 1 { 853 | format!("{}{}", "-".repeat(dash_count as usize), name) 854 | } else { 855 | format!("-{}", name) 856 | }; 857 | completions.add(&name, &desc); 858 | } 859 | completions.build() 860 | } 861 | } 862 | 863 | #[test] 864 | fn test_type_darwin_trim_groff() { 865 | assert_eq!(TypeDarwin::trim_groff(". Test"), "Test"); 866 | assert_eq!(TypeDarwin::trim_groff("..."), ".."); 867 | assert_eq!(TypeDarwin::trim_groff(" Test"), "Test"); 868 | assert_eq!(TypeDarwin::trim_groff("Test ."), "Test."); 869 | assert_eq!(TypeDarwin::trim_groff("Test ,"), "Test,"); 870 | assert_eq!(TypeDarwin::trim_groff("Ab "), ""); 871 | assert_eq!(TypeDarwin::trim_groff(".Ab Dd Fz ZZ ."), "ZZ."); 872 | assert_eq!(TypeDarwin::trim_groff("Test , ."), "Test ,."); 873 | assert_eq!(TypeDarwin::trim_groff("Test . ,"), "Test .,"); 874 | } 875 | 876 | impl TypeDarwin { 877 | fn trim_groff(line: &str) -> String { 878 | // Orig Python code would transform: 879 | // "This is a comment. An interesting example." 880 | // into " interesting example." 881 | // This port changes the regex to find the pattern at the start of the line 882 | // instead of anywhere in the line. 883 | 884 | // Remove initial period 885 | // Skip leading groff crud 886 | let line = regex!(r"^\.?([A-Z][a-z]\s)*").replace(&line, ""); 887 | // If the line ends with a space and then a period or comma, then erase the space 888 | regex!(r" ([.,])$").replace(&line, "$1").trim().to_string() 889 | } 890 | } 891 | 892 | #[test] 893 | fn test_replace_all() { 894 | let (string, num) = replace_all(""); 895 | assert_eq!(string, ""); 896 | assert_eq!(num, 0); 897 | 898 | let (string, num) = replace_all(".xyzppp"); 899 | assert_eq!(string, "ppp"); 900 | assert_eq!(num, 0); 901 | 902 | let (string, num) = replace_all("Fl jkl"); 903 | assert_eq!(string, "Fl jkl"); 904 | assert_eq!(num, 0); 905 | 906 | let (string, num) = replace_all(".xxxFl jkl"); 907 | assert_eq!(string, "jkl"); 908 | assert_eq!(num, 1); 909 | 910 | let (string, num) = replace_all(".Fl Fl Fl jkl"); 911 | assert_eq!(string, "jkl"); 912 | assert_eq!(num, 2); 913 | } 914 | 915 | use std::borrow::Cow; 916 | fn replace_all(line: &str) -> (Cow, u32) { 917 | let mut result = 0; 918 | ( 919 | regex!(r"^(?:\....)((?:Fl\s)*)").replace(&line, |captures: ®ex::Captures| { 920 | result = (captures[1].len() / 3) as u32; // Divide by 3 since there are 3 bytes per `Fl\s` pattern 921 | "" 922 | }), 923 | result, 924 | ) 925 | } 926 | 927 | #[test] 928 | fn test_type_darwin_count_argument_dashes() { 929 | assert_eq!(TypeDarwin::count_argument_dashes(".Fl Fl xx"), 1); 930 | assert_eq!(TypeDarwin::count_argument_dashes(".xxxFl Fl "), 2); 931 | assert_eq!(TypeDarwin::count_argument_dashes(".xxxFl FL "), 1); 932 | assert_eq!(TypeDarwin::count_argument_dashes("Fl Fl Fl "), 0); 933 | assert_eq!(TypeDarwin::count_argument_dashes(".Fl "), 0); 934 | } 935 | 936 | impl TypeDarwin { 937 | fn count_argument_dashes(line: &str) -> u32 { 938 | replace_all(&line).1 939 | } 940 | } 941 | 942 | #[test] 943 | fn test_type_darwin_groff_replace_escapes() { 944 | // tests for expected replacements 945 | assert!(TypeDarwin::groff_replace_escapes(".Nm") == "CMDNAME"); 946 | assert!(TypeDarwin::groff_replace_escapes("\\ ") == " "); 947 | assert!(TypeDarwin::groff_replace_escapes(r"& ") == ""); 948 | assert!(TypeDarwin::groff_replace_escapes(r"\ .Nm & ") == " CMDNAME "); 949 | // tests for no expected replacement 950 | assert!(TypeDarwin::groff_replace_escapes(".N") == ".N"); 951 | assert!(TypeDarwin::groff_replace_escapes(r"\x ") == r"\x "); 952 | assert!(TypeDarwin::groff_replace_escapes(r"&") == "&"); 953 | } 954 | 955 | impl TypeDarwin { 956 | fn groff_replace_escapes(line: &str) -> String { 957 | line.replace(".Nm", "CMDNAME") 958 | .replace("\\ ", " ") 959 | .replace(r"& ", "") 960 | } 961 | } 962 | 963 | #[test] 964 | fn test_type_darwin_is_option() { 965 | assert!(!TypeDarwin::is_option("Not an Option")); 966 | assert!(TypeDarwin::is_option(".It Fl Is an Option")); 967 | assert!(!TypeDarwin::is_option("")); 968 | } 969 | 970 | impl TypeDarwin { 971 | fn is_option(line: &str) -> bool { 972 | line.starts_with(".It Fl") 973 | } 974 | } 975 | 976 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 977 | struct TypeDeroff; 978 | 979 | impl ManParser for TypeDeroff { 980 | fn is_my_type(&self, _manpage: &str) -> bool { 981 | // TODO Revisit post-MVP 982 | // I think this is just to account for TypeDeroff being the last ManParser implementation 983 | // that is checked; it's the fallback. 984 | true // We're optimists 985 | } 986 | 987 | fn parse_man_page(&self, manpage: &str, cmdname: &str) -> Option { 988 | let mut deroffer = deroff::Deroffer::new(); 989 | deroffer.deroff(manpage.to_owned()); 990 | let output = deroffer.get_output(); 991 | let lines = output.lines(); 992 | 993 | let mut lines = lines 994 | // Discard lines until we get to DESCRIPTION or OPTIONS 995 | .skip_while(|line| { 996 | !(line.starts_with("DESCRIPTION") 997 | || line.starts_with("OPTIONS") 998 | || line.starts_with("COMMAND OPTIONS")) 999 | }) 1000 | // Look for BUGS and stop there 1001 | .take_while(|line| !line.starts_with("BUGS")) 1002 | .peekable(); 1003 | 1004 | let mut completions = Completions::new(cmdname); 1005 | while let Some(options) = lines.next() { 1006 | // Skip until we get to the next option 1007 | if !TypeDeroff::is_option(options) { 1008 | continue; 1009 | } 1010 | 1011 | // Pop until we get to either an empty line or a line starting with - 1012 | let description: Vec<_> = lines 1013 | .peeking_take_while(|line| TypeDeroff::could_be_description(line)) 1014 | .collect(); 1015 | let description = description.join(" "); 1016 | 1017 | completions.add(&options, &description); 1018 | } 1019 | completions.build() 1020 | } 1021 | } 1022 | 1023 | #[test] 1024 | fn test_type_deroff_is_option() { 1025 | assert!(!TypeDeroff::is_option("Not an Option")); 1026 | assert!(TypeDeroff::is_option("-Is an Option")); 1027 | assert!(!TypeDeroff::is_option("")); 1028 | } 1029 | 1030 | impl TypeDeroff { 1031 | fn is_option(line: &str) -> bool { 1032 | line.starts_with("-") 1033 | } 1034 | } 1035 | 1036 | #[test] 1037 | fn test_could_be_description() { 1038 | assert!(TypeDeroff::could_be_description("Test Pass Line")); 1039 | assert!(!TypeDeroff::could_be_description("-Test Fail Line")); 1040 | assert!(!TypeDeroff::could_be_description("")); 1041 | } 1042 | 1043 | impl TypeDeroff { 1044 | fn could_be_description(line: &str) -> bool { 1045 | line.len() > 0 && !line.starts_with("-") 1046 | } 1047 | } 1048 | 1049 | #[test] 1050 | fn test_file_is_overwritable() { 1051 | use std::path::Path; 1052 | use tests::FileKind; 1053 | 1054 | // Setup file paths 1055 | 1056 | // Good File Paths 1057 | let mut good_string = env::temp_dir(); 1058 | good_string.push("test_file_is_overwritable__good.txt"); 1059 | let good_path = Path::new(&good_string); 1060 | 1061 | // Bad File paths 1062 | let mut bad_string = env::temp_dir(); 1063 | bad_string.push("test_file_is_overwritable__bad.txt"); 1064 | let bad_path = Path::new(&bad_string); 1065 | 1066 | // Remove any leftover files if the already 1067 | // exist for a previous test failure 1068 | tests::remove_test_file(good_path).ok(); 1069 | tests::remove_test_file(bad_path).ok(); 1070 | 1071 | // Create good test file 1072 | tests::create_test_file(good_path, FileKind::Good).ok(); 1073 | 1074 | // Test for IO Error when File doesn't exist 1075 | let result = file_is_overwritable(bad_path); 1076 | assert!(result.is_err()); 1077 | 1078 | // Create bad test file 1079 | tests::create_test_file(bad_path, FileKind::Bad).ok(); 1080 | 1081 | // Tests 1082 | let result = file_is_overwritable(good_path); 1083 | assert_eq!(result, Ok(true)); 1084 | let result = file_is_overwritable(bad_path); 1085 | assert_eq!(result, Ok(false)); 1086 | 1087 | // Tear down 1088 | tests::remove_test_file(good_path).ok(); 1089 | tests::remove_test_file(bad_path).ok(); 1090 | } 1091 | 1092 | // Return whether the file at the given path is overwritable 1093 | // Raises IOError if it cannot be opened 1094 | fn file_is_overwritable(path: &Path) -> Result { 1095 | use bstr::ByteSlice; 1096 | let display = path.display(); 1097 | let f = File::open(path).map_err(|error| format!("{:?}", error))?; 1098 | let file = BufReader::new(&f); 1099 | Ok(file 1100 | .split(b'\n') 1101 | .map(|line| { 1102 | // Okay to panic via `expect` here since we've already verified 1103 | // that we can open the file for reading. 1104 | bstr::B(&line.expect(&format!("I/O error encountered reading {}", display))) 1105 | .trim() 1106 | .to_owned() 1107 | }) 1108 | .filter(|line| !line.is_empty()) 1109 | .take_while(|line| line.starts_with(b"#")) 1110 | .any(|line: Vec| line.contains_str("Autogenerated"))) 1111 | } 1112 | 1113 | /// Remove any and all autogenerated completions in the given directory 1114 | fn cleanup_autogenerated_completions_in_directory(dir: &Path) -> io::Result<()> { 1115 | Ok(for entry in fs::read_dir(dir)? { 1116 | let path = entry?.path(); 1117 | if path.extension().map_or(false, |ext| ext == "fish") { 1118 | cleanup_autogenerated_file(&path); 1119 | } 1120 | }) 1121 | } 1122 | 1123 | #[test] 1124 | fn test_cleanup_autogenerated_file_in_directory() { 1125 | use tests::FileKind; 1126 | 1127 | // Setup test dir 1128 | let test_dir = env::temp_dir().join("fish-manpage-completions-test"); 1129 | fs::create_dir(&test_dir).unwrap(); 1130 | let good_path = test_dir.join("good.fish"); 1131 | let bad1_path = test_dir.join("bad.fish"); 1132 | let bad2_path = test_dir.join("bad.txt"); 1133 | 1134 | // Remove leftovers from previous failed tests 1135 | tests::remove_test_file(&good_path).ok(); 1136 | tests::remove_test_file(&bad1_path).ok(); 1137 | tests::remove_test_file(&bad2_path).ok(); 1138 | 1139 | // Create files 1140 | tests::create_test_file(&good_path, FileKind::Good).ok(); 1141 | tests::create_test_file(&bad1_path, FileKind::Bad).ok(); 1142 | tests::create_test_file(&bad2_path, FileKind::Bad).ok(); 1143 | 1144 | // Tests 1145 | assert!(cleanup_autogenerated_completions_in_directory(&test_dir).is_ok()); 1146 | assert!(!good_path.exists()); 1147 | assert!(bad1_path.exists()); 1148 | assert!(bad2_path.exists()); 1149 | 1150 | // Tear down 1151 | tests::remove_test_file(&good_path).ok(); 1152 | tests::remove_test_file(&bad1_path).ok(); 1153 | tests::remove_test_file(&bad2_path).ok(); 1154 | fs::remove_dir(&test_dir).unwrap(); 1155 | 1156 | // Fail if dir not exist 1157 | assert!(cleanup_autogenerated_completions_in_directory(&test_dir).is_err()); 1158 | } 1159 | 1160 | /// Delete the file if it is autogenerated 1161 | fn cleanup_autogenerated_file(path: &Path) { 1162 | if file_is_overwritable(path) == Ok(true) { 1163 | // Original proceeds while ignoring errors 1164 | if let Err(err) = std::fs::remove_file(path) { 1165 | eprintln!( 1166 | "Error in cleaning up auto-generated file ({}): {}", 1167 | path.display(), 1168 | err.to_string(), 1169 | ); 1170 | } 1171 | } 1172 | } 1173 | 1174 | #[test] 1175 | fn test_cleanup_autogenerated_file() { 1176 | use std::path::Path; 1177 | use tests::FileKind; 1178 | 1179 | // Setup file paths 1180 | // Good File Paths 1181 | let mut good_string = env::temp_dir(); 1182 | good_string.push("test_cleanup_autogenerated_file__good.txt"); 1183 | let good_path = Path::new(&good_string); 1184 | 1185 | // Bad File paths 1186 | let mut bad_string = env::temp_dir(); 1187 | bad_string.push("test_cleanup_autogenerated_file__bad.txt"); 1188 | let bad_path = Path::new(&bad_string); 1189 | 1190 | // Remove any leftover files if the already 1191 | // exist for a previous test failure 1192 | tests::remove_test_file(good_path).ok(); 1193 | tests::remove_test_file(bad_path).ok(); 1194 | 1195 | // Create good test file 1196 | tests::create_test_file(good_path, FileKind::Good).ok(); 1197 | 1198 | // Test for IO Error when File doesn't exist 1199 | //let result = cleanup_autogenerated_file(bad_path); 1200 | // assert!(result.is_err()); 1201 | 1202 | // Create bad test file 1203 | tests::create_test_file(bad_path, FileKind::Bad).ok(); 1204 | 1205 | // Tests 1206 | cleanup_autogenerated_file(good_path); 1207 | assert_eq!(good_path.exists(), false); 1208 | cleanup_autogenerated_file(bad_path); 1209 | assert_eq!(bad_path.exists(), true); 1210 | 1211 | // Tear down 1212 | tests::remove_test_file(good_path).ok(); 1213 | tests::remove_test_file(bad_path).ok(); 1214 | } 1215 | 1216 | fn parse_manpage_at_path( 1217 | manpage_path: &Path, 1218 | output_directory: Option<&Path>, 1219 | deroff_only: bool, 1220 | ) -> io::Result { 1221 | // First level span 1222 | let span = tracing::info_span!("Considering", "{}", manpage_path.display()); 1223 | let _enter = span.enter(); 1224 | 1225 | // Get the "base" command, e.g. gcc.1.gz -> gcc 1226 | // These casts are safe as OsStr is internally a wrapper around [u8] on all 1227 | // platforms. Taken from libstd. 1228 | let cmdname = manpage_path 1229 | .file_name() 1230 | .and_then(|file| file.as_bytes().splitn(2, |b| *b == b'.').next()); 1231 | let cmdname = String::from_utf8_lossy(cmdname.unwrap()); 1232 | let ignored_commands = [ 1233 | "cc", "g++", "gcc", "c++", "cpp", "emacs", "gprof", "wget", "ld", "awk", 1234 | ]; 1235 | if ignored_commands.contains(&cmdname.as_ref()) { 1236 | return Ok(false); 1237 | } 1238 | 1239 | let mut manpage = String::new(); 1240 | let extension = manpage_path.extension().unwrap_or_default(); 1241 | let extension = extension.to_string_lossy(); 1242 | if extension.as_ref() == "gz" { 1243 | let mut gz = GzDecoder::new(File::open(manpage_path)?); 1244 | gz.read_to_string(&mut manpage)?; 1245 | } else if extension.as_ref() == "bz2" { 1246 | let mut bz = BzDecoder::new(File::open(manpage_path)?); 1247 | bz.read_to_string(&mut manpage)?; 1248 | } else if extension.as_ref() == "xz" || extension.as_ref() == "lzma" { 1249 | let mut xz = XzDecoder::new(File::open(manpage_path)?); 1250 | xz.read_to_string(&mut manpage)?; 1251 | } else if (1..=9).any(|suffix| suffix.to_string() == extension.as_ref()) { 1252 | File::open(manpage_path)?.read_to_string(&mut manpage)?; 1253 | } 1254 | 1255 | // Ignore perl's gazillion man pages 1256 | let ignored_prefixes = ["perl", "zsh"]; 1257 | if ignored_prefixes 1258 | .iter() 1259 | .any(|prefix| cmdname.starts_with(prefix)) 1260 | { 1261 | return Ok(false); 1262 | } 1263 | 1264 | // Ignore the millions of links to BUILTIN(1) 1265 | if manpage.contains("BUILTIN 1") || manpage.contains("builtin.1") { 1266 | return Ok(false); 1267 | } 1268 | 1269 | let parsers = if deroff_only { 1270 | &[ManType::TypeDeroff(TypeDeroff)] 1271 | } else { 1272 | ManType::ALL 1273 | }; 1274 | let parsers = parsers.iter().filter(|parser| parser.is_my_type(&manpage)); 1275 | let mut parsers = parsers.peekable(); 1276 | 1277 | if parsers.peek().is_none() { 1278 | tracing::info!("{}: Not supported", manpage_path.display()); 1279 | } 1280 | 1281 | if let Some(mut completions) = parsers.find_map(|parser| { 1282 | // Second (last) level span 1283 | let span = tracing::info_span!("Trying", "{}", parser); 1284 | let _enter = span.enter(); 1285 | parser.parse_man_page(&manpage, &cmdname) 1286 | }) { 1287 | let comments = format!( 1288 | "# {}\n# Autogenerated from man page {}\n", 1289 | &cmdname, 1290 | manpage_path.display() 1291 | ); 1292 | completions.insert_str(0, &comments); 1293 | 1294 | if let Some(output_directory) = output_directory { 1295 | let fullpath = output_directory 1296 | .join(cmdname.as_ref()) 1297 | .with_extension("fish"); 1298 | match File::create(&fullpath) { 1299 | Ok(mut file) => file.write_all(completions.as_bytes())?, 1300 | Err(err) => { 1301 | tracing::info!("Unable to open file '{}': {}", &fullpath.display(), err); 1302 | return Err(err); 1303 | } 1304 | } 1305 | } else { 1306 | io::stdout().lock().write_all(completions.as_bytes())?; 1307 | } 1308 | tracing::info!("{} parsed successfully", manpage_path.display()); 1309 | Ok(true) 1310 | } else { 1311 | let parser_names = parsers.join(", "); 1312 | tracing::warn!( 1313 | "{} contains no options or is unparsable (tried parser {})", 1314 | manpage_path.display(), 1315 | parser_names 1316 | ); 1317 | Ok(false) 1318 | } 1319 | } 1320 | 1321 | fn parse_and_output_man_pages( 1322 | paths: &mut [PathBuf], 1323 | output_directory: Option, 1324 | show_progress: bool, 1325 | deroff_only: bool, 1326 | ) { 1327 | paths.par_sort_unstable(); 1328 | 1329 | let paths_iter = paths.par_iter().map(|manpage_path| { 1330 | // We know the lifetime will always be the same as another 1331 | // painting thread but how to not clone this? 1332 | match parse_manpage_at_path(&manpage_path, output_directory.as_deref(), deroff_only) { 1333 | Ok(true) => 1, 1334 | Ok(false) => 0, 1335 | Err(_) => { 1336 | tracing::info!("Cannot open {}", manpage_path.display()); 1337 | 0 1338 | } 1339 | } 1340 | }); 1341 | let successful_count: u64 = if show_progress { 1342 | paths_iter.progress().sum() // may not be accurate since it paints after 1343 | } else { 1344 | paths_iter.sum() 1345 | }; 1346 | 1347 | tracing::info!( 1348 | "successfully parsed {} / {} pages", 1349 | successful_count, 1350 | paths.len() 1351 | ); 1352 | } 1353 | 1354 | macro_rules! mantypes { 1355 | ($($typ: tt),*) => { 1356 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 1357 | enum ManType { 1358 | $($typ($typ),)* 1359 | } 1360 | 1361 | impl ManType { 1362 | const ALL: &'static [ManType] = &[$(ManType::$typ($typ),)*]; 1363 | } 1364 | 1365 | $( 1366 | impl From<$typ> for ManType { 1367 | fn from(t: $typ) -> Self { 1368 | ManType::$typ(t) 1369 | } 1370 | } 1371 | )* 1372 | 1373 | impl fmt::Display for ManType { 1374 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1375 | write!(f, "{}", match self { 1376 | $(ManType::$typ($typ) => stringify!($typ),)* 1377 | }) 1378 | } 1379 | } 1380 | 1381 | impl ManParser for ManType { 1382 | fn is_my_type(&self, manpage: &str) -> bool { 1383 | match self {$( 1384 | ManType::$typ(t) => t.is_my_type(manpage), 1385 | )*} 1386 | } 1387 | 1388 | fn parse_man_page(&self, manpage: &str, cmdname: &str) -> Option { 1389 | match self {$( 1390 | ManType::$typ(t) => t.parse_man_page(manpage, cmdname), 1391 | )*} 1392 | } 1393 | } 1394 | }; 1395 | } 1396 | 1397 | mantypes![Type1, Type2, Type3, Type4, TypeScdoc, TypeDarwin, TypeDeroff]; 1398 | 1399 | /// Return all the paths to man(1) and man(8) files in the manpath. 1400 | fn get_paths_from_man_locations() -> Vec { 1401 | // $MANPATH take precedence, just like with `man` on the CLI. 1402 | let mut parent_paths: Vec<_> = if let Ok(output) = Command::new("manpath").output() { 1403 | let output = String::from_utf8(output.stdout).unwrap(); 1404 | env::split_paths(&output.trim()).collect() 1405 | } else if let Ok(output) = Command::new("man").arg("--path").output() { 1406 | let output = String::from_utf8(output.stdout).unwrap(); 1407 | env::split_paths(&output.trim()).collect() 1408 | } else if let Some(paths) = env::var_os("MANPATH") { 1409 | env::split_paths(&paths).collect() 1410 | // HACK: Use some fallbacks in case we can't get anything else. 1411 | // `mandoc` does not provide `manpath` or `man --path` and $MANPATH might not be set. 1412 | // The alternative is reading its config file (/etc/man.conf) 1413 | } else if let Ok(file) = File::open("/etc/man.conf") { 1414 | let lines_iter = BufReader::new(file).lines().map(|line| line.unwrap()); 1415 | lines_iter 1416 | .filter(|line| line.starts_with("manpath") || line.starts_with("MANPATH")) 1417 | .filter_map(|line| line.split_ascii_whitespace().nth(2).map(PathBuf::from)) 1418 | .collect() 1419 | } else { 1420 | Default::default() 1421 | }; 1422 | 1423 | if parent_paths.is_empty() { 1424 | const FALLBACK_PATHS: [&'static str; 3] = 1425 | ["/usr/share/man", "/usr/local/man", "/usr/local/share/man"]; 1426 | 1427 | eprintln!( 1428 | "Unable to get the manpath, falling back to {}. Please set \ 1429 | $MANPATH if that is not correct.", 1430 | FALLBACK_PATHS.join(":"), 1431 | ); 1432 | 1433 | parent_paths = FALLBACK_PATHS.map(PathBuf::from).into(); 1434 | } 1435 | 1436 | let mut paths = Vec::new(); 1437 | for parent_path in parent_paths { 1438 | for section in &["man1", "man6", "man8"] { 1439 | let section_path = parent_path.join(section); 1440 | if let Ok(dir) = fs::read_dir(§ion_path) { 1441 | for entry in dir { 1442 | paths.push(section_path.join(entry.unwrap().path())); 1443 | } 1444 | } 1445 | } 1446 | } 1447 | paths 1448 | } 1449 | 1450 | mod deroff; 1451 | 1452 | /// Generate fish completions from manpages. 1453 | #[derive(Parser, Debug)] 1454 | struct Args { 1455 | /// Level of debug output. 1456 | #[clap( 1457 | short, long, default_value = "0", 1458 | value_parser = clap::value_parser!(u8).range(0..3) 1459 | )] 1460 | verbose: u8, 1461 | /// Write the completions to stdout. 1462 | #[clap(short, long, conflicts_with = "directory")] 1463 | stdout: bool, 1464 | /// Use deroff parser only. 1465 | #[clap(short = 'z', long)] 1466 | deroff_only: bool, 1467 | /// Directory to save the completions in. 1468 | #[clap(short, long)] 1469 | directory: Option, 1470 | /// Use manpath from system and environment variable. 1471 | #[clap(short, long)] 1472 | manpath: bool, 1473 | /// Show progress bar. 1474 | #[clap(short, long)] 1475 | progress: bool, 1476 | /// Directory to clean up. 1477 | #[clap(short, long)] 1478 | cleanup_in: Option, 1479 | /// Keep files in target directory. 1480 | #[clap(short, long)] 1481 | keep: bool, 1482 | /// Generate fish completions. 1483 | // TODO generate this in build.rs and remove this option 1484 | #[clap(long)] 1485 | completions: bool, 1486 | /// Files to parse and generate completions. 1487 | files: Vec, 1488 | } 1489 | 1490 | fn program_name() -> String { 1491 | std::env::current_exe() 1492 | .unwrap() 1493 | .file_stem() 1494 | .map(|stem| stem.to_string_lossy().into_owned()) 1495 | .expect("No extractable program name.") 1496 | } 1497 | 1498 | fn main() -> Result<(), String> { 1499 | let args = Args::parse(); 1500 | 1501 | let level = match args.verbose { 1502 | 0 => LevelFilter::OFF, 1503 | 1 => LevelFilter::WARN, 1504 | 2 => LevelFilter::INFO, 1505 | n => unreachable!("{n}"), 1506 | }; 1507 | tracing_subscriber::fmt() 1508 | .with_writer(io::stderr) 1509 | .with_max_level(level) 1510 | .init(); 1511 | 1512 | if args.completions { 1513 | clap_complete::generate( 1514 | clap_complete::Shell::from_env().expect("Unsupported shell"), 1515 | &mut Args::command(), 1516 | program_name(), 1517 | &mut std::io::stdout(), 1518 | ); 1519 | return Ok(()); 1520 | } 1521 | 1522 | if let Some(cleanup_dir) = args.cleanup_in.as_ref() { 1523 | cleanup_autogenerated_completions_in_directory(cleanup_dir).ok(); 1524 | } 1525 | 1526 | let mut paths = args.files.clone(); 1527 | if args.manpath { 1528 | paths.extend(get_paths_from_man_locations()); 1529 | } 1530 | 1531 | if paths.is_empty() { 1532 | println!("No paths specified"); 1533 | return Ok(()); 1534 | } 1535 | 1536 | let output_directory = args.directory.clone().or_else(|| { 1537 | if args.stdout { 1538 | None 1539 | } else { 1540 | let mut xdg_data_home = dirs::data_dir().unwrap(); 1541 | xdg_data_home.push("fish/generated_completions/"); 1542 | if !xdg_data_home.is_dir() { 1543 | std::fs::create_dir_all(&xdg_data_home).expect("Failed to create directory"); 1544 | } 1545 | Some(xdg_data_home) 1546 | } 1547 | }); 1548 | 1549 | if let Some(output_directory) = output_directory.as_ref() { 1550 | if !args.keep { 1551 | cleanup_autogenerated_completions_in_directory(output_directory).ok(); 1552 | } 1553 | } 1554 | 1555 | parse_and_output_man_pages( 1556 | &mut paths, 1557 | output_directory, 1558 | args.progress, 1559 | args.deroff_only, 1560 | ); 1561 | 1562 | Ok(()) 1563 | } 1564 | 1565 | #[cfg(test)] 1566 | mod tests { 1567 | use super::*; 1568 | 1569 | #[derive(Copy, Clone, Debug)] 1570 | pub enum FileKind { 1571 | Good, 1572 | Bad, 1573 | } 1574 | 1575 | impl FileKind { 1576 | fn content(self) -> &'static [u8] { 1577 | match self { 1578 | FileKind::Good => { 1579 | b" \n#Hello, world!\ 1580 | \n#Hello, world! Autogenerated " 1581 | } 1582 | FileKind::Bad => b" Autogenerated ", 1583 | } 1584 | } 1585 | } 1586 | 1587 | pub fn create_test_file(path: &Path, kind: FileKind) -> std::io::Result<()> { 1588 | use std::io::prelude::*; 1589 | let mut file = File::create(path)?; 1590 | file.write_all(kind.content())?; 1591 | Ok(()) 1592 | } 1593 | 1594 | pub fn remove_test_file(path: &Path) -> std::io::Result<()> { 1595 | fs::remove_file(path)?; 1596 | Ok(()) 1597 | } 1598 | } 1599 | -------------------------------------------------------------------------------- /src/deroff.rs: -------------------------------------------------------------------------------- 1 | /// A translation of https://github.com/fish-shell/fish-shell/blob/e7bfd1d71ca54df726a4f1ea14bd6b0957b75752/share/tools/deroff.py 2 | /// Deroff, ported from deroff.py, which is ported from the venerable deroff.c 3 | use regex::Regex; 4 | 5 | use crate::util::TranslationTable; 6 | use std::borrow::Cow; 7 | use std::cell::Cell; 8 | use std::collections::HashMap; 9 | 10 | #[derive(PartialEq, Debug, Clone, Copy)] 11 | enum TblState { 12 | Options, 13 | Format, 14 | Data, 15 | } 16 | 17 | // class Deroffer: 18 | pub struct Deroffer { 19 | g_re_word: &'static Regex, 20 | g_re_number: &'static Regex, 21 | g_re_not_backslash_or_whitespace: &'static Regex, 22 | g_re_newline_collapse: &'static Regex, 23 | g_re_font: &'static Regex, 24 | 25 | reg_table: HashMap, 26 | tr_from: String, 27 | tr_to: String, 28 | tr: Option, 29 | specletter: bool, 30 | refer: bool, 31 | r#macro: u8, 32 | inlist: bool, 33 | inheader: bool, 34 | pic: bool, 35 | tbl: bool, 36 | tblstate: TblState, 37 | tbl_tab: String, 38 | eqn: bool, 39 | output: Cell, 40 | name: String, 41 | 42 | s: String, // This is not explicitly defined in python code 43 | } 44 | 45 | impl Deroffer { 46 | pub fn new() -> Deroffer { 47 | Deroffer { 48 | g_re_word: crate::regex!(r##"^[a-zA-Z_]+"##), 49 | g_re_number: crate::regex!(r##"^[+-]?\d+"##), 50 | // sequence of not backslash or whitespace 51 | g_re_not_backslash_or_whitespace: crate::regex!(r##"^[^ \t\n\r\f\v\\]+"##), 52 | g_re_newline_collapse: crate::regex!(r##"\n{3,}"##), 53 | g_re_font: crate::regex!( 54 | r##"(?x)\\f( # Starts with backslash f 55 | (\(\S{2}) | # Open paren, then two printable chars 56 | (\[\S*?\]) | # Open bracket, zero or more printable characters, then close bracket 57 | \S) # Any printable character 58 | "## 59 | ), 60 | 61 | reg_table: HashMap::new(), 62 | tr_from: String::new(), 63 | tr_to: String::new(), 64 | tr: None, 65 | specletter: false, 66 | refer: false, 67 | r#macro: 0, 68 | inlist: false, 69 | inheader: false, 70 | pic: false, 71 | tbl: false, 72 | tblstate: TblState::Options, 73 | tbl_tab: String::new(), 74 | eqn: false, 75 | output: Cell::new(String::new()), 76 | name: String::new(), 77 | 78 | s: String::new(), // This is not explicitly defined in python code 79 | } 80 | } 81 | 82 | /// Take the output, leaving the the default value. 83 | pub fn get_output(&self) -> String { 84 | let output = self.output.take(); 85 | match self.g_re_newline_collapse.replace_all(&output, "\n") { 86 | Cow::Borrowed(_) => output, 87 | Cow::Owned(result) => result, 88 | } 89 | } 90 | 91 | // for the moment, return small strings, until we figure out what 92 | // it should really be doing 93 | fn g_specs_specletter(key: &str) -> Option<&'static str> { 94 | Some(match key { 95 | // Output composed latin1 letters 96 | "-D" => &"Ð", 97 | "Sd" => &"ð", 98 | "Tp" => &"þ", 99 | "TP" => &"Þ", 100 | "AE" => &"Æ", 101 | "ae" => &"æ", 102 | "OE" => &"OE", 103 | "oe" => &"oe", 104 | ":a" => &"ä", 105 | ":A" => &"Ä", 106 | ":e" => &"ë", 107 | ":E" => &"Ë", 108 | ":i" => &"ï", 109 | ":I" => &"Ï", 110 | ":o" => &"ö", 111 | ":O" => &"Ö", 112 | ":u" => &"ü", 113 | ":U" => &"Ü", 114 | ":y" => &"ÿ", 115 | "ss" => &"ß", 116 | "'A" => &"Á", 117 | "'E" => &"É", 118 | "'I" => &"Í", 119 | "'O" => &"Ó", 120 | "'U" => &"Ú", 121 | "'Y" => &"Ý", 122 | "'a" => &"á", 123 | "'e" => &"é", 124 | "'i" => &"í", 125 | "'o" => &"ó", 126 | "'u" => &"ú", 127 | "'y" => &"ý", 128 | "^A" => &"Â", 129 | "^E" => &"Ê", 130 | "^I" => &"Î", 131 | "^O" => &"Ô", 132 | "^U" => &"Û", 133 | "^a" => &"â", 134 | "^e" => &"ê", 135 | "^i" => &"î", 136 | "^o" => &"ô", 137 | "^u" => &"û", 138 | "`A" => &"À", 139 | "`E" => &"È", 140 | "`I" => &"Ì", 141 | "`O" => &"Ò", 142 | "`U" => &"Ù", 143 | "`a" => &"à", 144 | "`e" => &"è", 145 | "`i" => &"ì", 146 | "`o" => &"ò", 147 | "`u" => &"ù", 148 | "~A" => &"Ã", 149 | "~N" => &"Ñ", 150 | "~O" => &"Õ", 151 | "~a" => &"ã", 152 | "~n" => &"ñ", 153 | "~o" => &"õ", 154 | ",C" => &"Ç", 155 | ",c" => &"ç", 156 | "/l" => &"/l", 157 | "/L" => &"/L", 158 | "/o" => &"ø", 159 | "/O" => &"Ø", 160 | "oA" => &"Å", 161 | "oa" => &"å", 162 | 163 | // Ligatures 164 | "fi" => &"fi", 165 | "ff" => &"ff", 166 | "fl" => &"fl", 167 | "Fi" => &"ffi", 168 | "Ff" => &"fff", 169 | "Fl" => &"ffl", 170 | _ => return None, 171 | }) 172 | } 173 | 174 | // Much like the above, return small strings for now until we know what 175 | // we might actually want to do 176 | fn g_specs(key: &str) -> Option<&'static str> { 177 | Some(match key { 178 | "mi" => &"-", 179 | "en" => &"-", 180 | "hy" => &"-", 181 | "em" => &"--", 182 | "lq" => &"“", 183 | "rq" => &"”", 184 | "Bq" => &",,", 185 | "oq" => &"`", 186 | "cq" => &"'", 187 | "aq" => &"'", 188 | "dq" => &"\"", 189 | "or" => &"|", 190 | "at" => &"@", 191 | "sh" => &"#", 192 | // For the moment, faithfully mimic the behavior of the Python script, 193 | // even though it might seem that &"€" is a more appropriate result here 194 | "Eu" => &"¤", 195 | "eu" => &"¤", 196 | "Do" => &"$", 197 | "ct" => &"¢", 198 | "Fo" => &"«", 199 | "Fc" => &"»", 200 | "fo" => &"<", 201 | "fc" => &">", 202 | "r!" => &"¡", 203 | "r?" => &"¿", 204 | "Of" => &"ª", 205 | "Om" => &"º", 206 | "pc" => &"·", 207 | "S1" => &"¹", 208 | "S2" => &"²", 209 | "S3" => &"³", 210 | "<-" => &"<-", 211 | "->" => &"->", 212 | "<>" => &"<->", 213 | "ua" => &"^", 214 | "da" => &"v", 215 | "lA" => &"<=", 216 | "rA" => &"=>", 217 | "hA" => &"<=>", 218 | "uA" => &"^^", 219 | "dA" => &"vv", 220 | "ba" => &"|", 221 | "bb" => &"|", 222 | "br" => &"|", 223 | "bv" => &"|", 224 | "ru" => &"_", 225 | "ul" => &"_", 226 | "ci" => &"O", 227 | "bu" => &"o", 228 | "co" => &"©", 229 | "rg" => &"®", 230 | "tm" => &"(TM)", 231 | "dd" => &"||", 232 | "dg" => &"|", 233 | "ps" => &"¶", 234 | "sc" => &"§", 235 | "de" => &"°", 236 | "%0" => &"0/00", 237 | "14" => &"¼", 238 | "12" => &"½", 239 | "34" => &"¾", 240 | "f/" => &"/", 241 | "sl" => &"/", 242 | "rs" => &"\\", 243 | "sq" => &"[]", 244 | "fm" => &"'", 245 | "ha" => &"^", 246 | "ti" => &"~", 247 | "lB" => &"[", 248 | "rB" => &"]", 249 | "lC" => &"{", 250 | "rC" => &"}", 251 | "la" => &"<", 252 | "ra" => &">", 253 | "lh" => &"<=", 254 | "rh" => &"=>", 255 | "tf" => &"therefore", 256 | "~~" => &"~~", 257 | "~=" => &"~=", 258 | "!=" => &"!=", 259 | "**" => &"*", 260 | "+-" => &"±", 261 | "<=" => &"<=", 262 | "==" => &"==", 263 | "=~" => &"=~", 264 | ">=" => &">=", 265 | "AN" => &"\\/", 266 | "OR" => &"/\\", 267 | "no" => &"¬", 268 | "te" => &"there exists", 269 | "fa" => &"for all", 270 | "Ah" => &"aleph", 271 | "Im" => &"imaginary", 272 | "Re" => &"real", 273 | "if" => &"infinity", 274 | "md" => &"·", 275 | "mo" => &"member of", 276 | "mu" => &"×", 277 | "nm" => &"not member of", 278 | "pl" => &"+", 279 | "eq" => &"=", 280 | "pt" => &"oc", 281 | "pp" => &"perpendicular", 282 | "sb" => &"(=", 283 | "sp" => &"=)", 284 | "ib" => &"(-", 285 | "ip" => &"-)", 286 | "ap" => &"~", 287 | "is" => &"I", 288 | "sr" => &"root", 289 | "pd" => &"d", 290 | "c*" => &"(x)", 291 | "c+" => &"(+)", 292 | "ca" => &"cap", 293 | "cu" => &"U", 294 | "di" => &"÷", 295 | "gr" => &"V", 296 | "es" => &"{}", 297 | "CR" => &"_|", 298 | "st" => &"such that", 299 | "/_" => &"/_", 300 | "lz" => &"<>", 301 | "an" => &"-", 302 | 303 | // Output Greek 304 | "*A" => &"Alpha", 305 | "*B" => &"Beta", 306 | "*C" => &"Xi", 307 | "*D" => &"Delta", 308 | "*E" => &"Epsilon", 309 | "*F" => &"Phi", 310 | "*G" => &"Gamma", 311 | "*H" => &"Theta", 312 | "*I" => &"Iota", 313 | "*K" => &"Kappa", 314 | "*L" => &"Lambda", 315 | "*M" => &"Mu", 316 | "*N" => &"Nu", 317 | "*O" => &"Omicron", 318 | "*P" => &"Pi", 319 | "*Q" => &"Psi", 320 | "*R" => &"Rho", 321 | "*S" => &"Sigma", 322 | "*T" => &"Tau", 323 | "*U" => &"Upsilon", 324 | "*W" => &"Omega", 325 | "*X" => &"Chi", 326 | "*Y" => &"Eta", 327 | "*Z" => &"Zeta", 328 | "*a" => &"alpha", 329 | "*b" => &"beta", 330 | "*c" => &"xi", 331 | "*d" => &"delta", 332 | "*e" => &"epsilon", 333 | "*f" => &"phi", 334 | "+f" => &"phi", 335 | "*g" => &"gamma", 336 | "*h" => &"theta", 337 | "+h" => &"theta", 338 | "*i" => &"iota", 339 | "*k" => &"kappa", 340 | "*l" => &"lambda", 341 | "*m" => &"µ", 342 | "*n" => &"nu", 343 | "*o" => &"omicron", 344 | "*p" => &"pi", 345 | "+p" => &"omega", 346 | "*q" => &"psi", 347 | "*r" => &"rho", 348 | "*s" => &"sigma", 349 | "*t" => &"tau", 350 | "*u" => &"upsilon", 351 | "*w" => &"omega", 352 | "*x" => &"chi", 353 | "*y" => &"eta", 354 | "*z" => &"zeta", 355 | "ts" => &"sigma", 356 | _ => return None, 357 | }) 358 | } 359 | 360 | fn skip_char(&mut self, amount: usize) { 361 | // drain takes in byte position but amount is character position 362 | match self.s.char_indices().nth(amount) { 363 | Some((pos, _)) => { 364 | self.s.drain(..pos); 365 | } 366 | None => { 367 | self.s.clear(); 368 | } 369 | } 370 | } 371 | 372 | fn skip_leading_whitespace(&mut self) { 373 | self.s = self.s.trim_start().to_owned(); 374 | } 375 | 376 | fn str_at(&self, idx: usize) -> &str { 377 | let s = &self.s; 378 | s.char_indices() 379 | .skip(idx) 380 | .next() 381 | .map(|(i, c)| &s[i..(i + c.len_utf8())]) 382 | .unwrap_or_default() 383 | } 384 | 385 | fn is_white(&self, idx: usize) -> bool { 386 | match self.str_at(idx) { 387 | "" => false, 388 | c => c.chars().all(|c| c.is_whitespace()), 389 | } 390 | } 391 | 392 | fn digit(&self, idx: usize) -> bool { 393 | match self.str_at(idx) { 394 | "" => false, 395 | c => c.chars().all(|c| c.is_digit(10)), 396 | } 397 | } 398 | 399 | fn comment(&mut self) -> bool { 400 | let mut s = self.str_at(0); 401 | while !s.is_empty() && s != "\n" { 402 | self.skip_char(1); 403 | s = self.str_at(0); 404 | } 405 | 406 | true 407 | } 408 | 409 | fn text_arg(&mut self) -> bool { 410 | let mut got_something = false; 411 | loop { 412 | let possible = self.g_re_not_backslash_or_whitespace.find(&self.s); 413 | if let Some(m) = possible { 414 | // Output the characters in the match 415 | self.condputs(m.as_str()); 416 | let end = m.end(); 417 | self.skip_char(end); 418 | got_something = true; 419 | } 420 | 421 | if self.s.is_empty() || self.is_white(0) { 422 | return got_something; 423 | } 424 | 425 | if !self.esc_char() { 426 | self.condputs(self.str_at(0)); 427 | self.skip_char(1); 428 | got_something = true; 429 | } 430 | } 431 | } 432 | 433 | // Replaces the g_macro_dict lookup in the Python code 434 | fn g_macro_dispatch(&mut self, s: &str) -> bool { 435 | match s { 436 | "SH" => self.macro_sh(), 437 | "SS" => self.macro_ss_ip(), 438 | "IP" => self.macro_ss_ip(), 439 | "H " => self.macro_ss_ip(), 440 | "I " => self.macro_i_ir(), 441 | "IR" => self.macro_i_ir(), 442 | "IB" => self.macro_i_ir(), 443 | "B " => self.macro_i_ir(), 444 | "BR" => self.macro_i_ir(), 445 | "BI" => self.macro_i_ir(), 446 | "R " => self.macro_i_ir(), 447 | "RB" => self.macro_i_ir(), 448 | "RI" => self.macro_i_ir(), 449 | "AB" => self.macro_i_ir(), 450 | "Nm" => self.macro_nm(), 451 | "] " => self.macro_close_bracket(), 452 | "PS" => self.macro_ps(), 453 | "PE" => self.macro_pe(), 454 | "TS" => self.macro_ts(), 455 | "T&" => self.macro_t_and(), 456 | "TE" => self.macro_te(), 457 | "EQ" => self.macro_eq(), 458 | "EN" => self.macro_en(), 459 | "R1" => self.macro_r1(), 460 | "R2" => self.macro_r2(), 461 | "de" => self.macro_de(), 462 | "BL" => self.macro_bl_vl(), 463 | "VL" => self.macro_bl_vl(), 464 | "AL" => self.macro_bl_vl(), 465 | "LB" => self.macro_bl_vl(), 466 | "RL" => self.macro_bl_vl(), 467 | "ML" => self.macro_bl_vl(), 468 | "DL" => self.macro_bl_vl(), 469 | "BV" => self.macro_bv(), 470 | "LE" => self.macro_le(), 471 | "LP" => self.macro_lp_pp(), 472 | "PP" => self.macro_lp_pp(), 473 | "P\n" => self.macro_lp_pp(), 474 | "ds" => self.macro_ds(), 475 | "so" => self.macro_so_nx(), 476 | "nx" => self.macro_so_nx(), 477 | "tr" => self.macro_tr(), 478 | "sp" => self.macro_sp(), 479 | _ => self.macro_other(), 480 | } 481 | } 482 | 483 | fn macro_sh(&mut self) -> bool { 484 | let headers = [" SYNOPSIS", " \"SYNOPSIS", " ‹BERSICHT", " \"‹BERSICHT"]; 485 | // @TODO: In the future s[2..] should care about UTF-8 486 | if headers.iter().any(|header| self.s[2..].starts_with(header)) { 487 | self.inheader = true; 488 | } else { 489 | self.inheader = false; 490 | } 491 | false 492 | } 493 | 494 | fn macro_ss_ip(&mut self) -> bool { 495 | false 496 | } 497 | 498 | fn macro_i_ir(&mut self) -> bool { 499 | false 500 | } 501 | 502 | fn macro_nm(&mut self) -> bool { 503 | if self.s == "Nm\n" { 504 | self.condputs(&self.name); 505 | } else { 506 | self.name = self.s.get(3..).unwrap_or_default().trim().into(); 507 | self.name.push(' '); 508 | } 509 | true 510 | } 511 | 512 | fn macro_close_bracket(&mut self) -> bool { 513 | self.refer = false; 514 | false 515 | } 516 | 517 | fn macro_ps(&mut self) -> bool { 518 | if self.is_white(2) { 519 | self.pic = true; 520 | } 521 | self.condputs("\n"); 522 | true 523 | } 524 | 525 | fn macro_pe(&mut self) -> bool { 526 | if self.is_white(2) { 527 | self.pic = false 528 | } 529 | self.condputs("\n"); 530 | true 531 | } 532 | 533 | fn macro_ts(&mut self) -> bool { 534 | if self.is_white(2) { 535 | self.tbl = true; 536 | self.tblstate = TblState::Options; 537 | } 538 | 539 | self.condputs("\n"); 540 | true 541 | } 542 | 543 | fn macro_t_and(&mut self) -> bool { 544 | if self.is_white(2) { 545 | self.tbl = true; 546 | self.tblstate = TblState::Format; 547 | } 548 | 549 | self.condputs("\n"); 550 | true 551 | } 552 | 553 | fn macro_te(&mut self) -> bool { 554 | if self.is_white(2) { 555 | self.tbl = false 556 | } 557 | 558 | self.condputs("\n"); 559 | true 560 | } 561 | 562 | fn macro_eq(&mut self) -> bool { 563 | if self.is_white(2) { 564 | self.eqn = true 565 | } 566 | 567 | self.condputs("\n"); 568 | true 569 | } 570 | 571 | fn macro_en(&mut self) -> bool { 572 | if self.is_white(2) { 573 | self.eqn = false 574 | } 575 | 576 | self.condputs("\n"); 577 | true 578 | } 579 | 580 | fn macro_r1(&mut self) -> bool { 581 | // NOTE: self.refer2 is never used in the python source, so this and macro_r2 are 582 | // pretty much worthless 583 | // if self.is_white(2) { 584 | // self.refer2 = true; 585 | // } 586 | self.condputs("\n"); 587 | true 588 | } 589 | 590 | fn macro_r2(&mut self) -> bool { 591 | // if self.is_white(2) { 592 | // NOTE: See macro_r1 593 | // self.refer2 = false; 594 | // } 595 | self.condputs("\n"); 596 | true 597 | } 598 | 599 | fn macro_de(&mut self) -> bool { 600 | self.r#macro = 1; 601 | self.condputs("\n"); 602 | true 603 | } 604 | 605 | fn macro_bl_vl(&mut self) -> bool { 606 | if self.is_white(2) { 607 | self.inlist = true 608 | } 609 | self.condputs("\n"); 610 | true 611 | } 612 | 613 | fn macro_bv(&mut self) -> bool { 614 | // TODO: Determine whether `self.white` is a bastardization of 615 | // `self.is_white`. (Was self.white converted to self.is_white 616 | // but this call site was missed?) 617 | // 618 | // If it _were_ a valid function, the original Python source 619 | // would translate roughly to: 620 | // 621 | // for `self.is_white`, so I don't know what function its supposed to be 622 | // if self.str_at(2) == "L" and self.white(self.str_at(3)): 623 | // self.inlist = true 624 | // } 625 | self.condputs("\n"); 626 | true 627 | } 628 | 629 | fn macro_le(&mut self) -> bool { 630 | if self.is_white(2) { 631 | self.inlist = false; 632 | } 633 | self.condputs("\n"); 634 | true 635 | } 636 | 637 | fn macro_lp_pp(&mut self) -> bool { 638 | self.condputs("\n"); 639 | true 640 | } 641 | 642 | fn macro_ds(&mut self) -> bool { 643 | // Yuck 644 | self.skip_char(2); 645 | self.skip_leading_whitespace(); 646 | 647 | if !self.str_at(0).is_empty() { 648 | let comps: Vec = self.s.splitn(2, " ").map(|s| s.into()).collect(); 649 | 650 | if comps.len() == 2 { 651 | let name: String = comps.get(0).unwrap().into(); 652 | let value = comps.get(1).unwrap().trim_end().into(); 653 | self.reg_table.insert(name, value); 654 | } 655 | } 656 | 657 | self.condputs("\n"); 658 | true 659 | } 660 | 661 | fn macro_so_nx(&mut self) -> bool { 662 | true 663 | } 664 | 665 | fn macro_tr(&mut self) -> bool { 666 | self.skip_char(2); 667 | self.skip_leading_whitespace(); 668 | 669 | while !self.s.is_empty() && &self.s[0..=0] != "\n" { 670 | self.tr_from.push_str(&self.s[0..=0]); 671 | 672 | let ns = &self.s[1..=1]; 673 | self.tr_to 674 | .push_str(if ns.is_empty() || ns == "\n" { " " } else { ns }); 675 | 676 | self.skip_char(2); 677 | } 678 | 679 | // Update our table, then swap in the slower tr-savvy condputs 680 | self.tr = match TranslationTable::new(&self.tr_from, &self.tr_to) { 681 | Ok(table) => Some(table), 682 | Err(e) => panic!( 683 | "Encountered an error creating a new translation table from {}, {}: {}", 684 | self.tr_from, self.tr_to, e 685 | ), 686 | }; 687 | true 688 | } 689 | 690 | fn macro_sp(&mut self) -> bool { 691 | self.condputs("\n"); 692 | true 693 | } 694 | 695 | fn macro_other(&mut self) -> bool { 696 | self.condputs("\n"); 697 | true 698 | } 699 | 700 | fn request_or_macro(&mut self) -> bool { 701 | // self.s[0] is a period or open single quote 702 | self.skip_char(1); 703 | 704 | match self.s.chars().nth(1) { 705 | Some('[') => { 706 | self.refer = true; 707 | self.condputs("\n"); 708 | return true; 709 | } 710 | Some(']') => { 711 | self.refer = false; 712 | self.skip_char(1); 713 | return self.text(); 714 | } 715 | Some('.') => { 716 | self.r#macro = 0; 717 | self.condputs("\n"); 718 | return true; 719 | } 720 | _ => {} 721 | } 722 | 723 | let s0s1 = self.s.chars().take(2).collect::(); 724 | 725 | if self.g_macro_dispatch(&s0s1) { 726 | return true; 727 | } 728 | 729 | self.skip_leading_whitespace(); 730 | while !self.s.is_empty() && !self.is_white(0) { 731 | self.skip_char(1); 732 | } 733 | self.skip_leading_whitespace(); 734 | loop { 735 | if !self.quoted_arg() && !self.text_arg() { 736 | if !self.s.is_empty() { 737 | self.condputs(self.str_at(0)); 738 | self.skip_char(1); 739 | } else { 740 | return true; 741 | } 742 | } 743 | } 744 | } 745 | 746 | fn font(&mut self) -> bool { 747 | if let Some(m) = self.g_re_font.find(&self.s) { 748 | let end = m.end(); 749 | self.skip_char(end); 750 | true 751 | } else { 752 | false 753 | } 754 | } 755 | 756 | fn numreq(&mut self) -> bool { 757 | if !"hvwud".contains(self.str_at(1)) || self.str_at(2) != "'" { 758 | return false; 759 | } 760 | 761 | self.r#macro += 1; 762 | 763 | self.skip_char(3); 764 | 765 | // This is weird, but it was in the source so it's here now. 766 | // I think this skips characters until we hit a ' 767 | while self.str_at(0) != "'" && self.esc_char() {} 768 | 769 | if self.str_at(0) == "'" { 770 | self.skip_char(1); 771 | } 772 | 773 | self.r#macro -= 1; 774 | 775 | true 776 | } 777 | 778 | // This function is the worst, there are a few comments explaining some of it in the test (test_var) 779 | // its so hard to briefly put into words what this function does, basically depending on the state 780 | // of self.s, it will either, change self.s to "", a part of self.s, or a value in self.reg_table 781 | // which corresponds to a key that is part of self.s. 782 | // This should be like 2 or 3 functions, but it's only one. So there's that. :-) 783 | // NOTE: there is a call to text_arg that is commented out because it's not implemented, so the 784 | // tests will need revised when it gets implemented 785 | fn var(&mut self) -> bool { 786 | let s0s1 = self.s.chars().take(2).collect::(); 787 | 788 | if s0s1 == "\\n" { 789 | if "dy" == self.s.chars().skip(3).take(2).collect::() 790 | || (self.str_at(2) == "(" && self.not_whitespace(3) && self.not_whitespace(4)) 791 | { 792 | self.skip_char(5); 793 | return true; 794 | } else if self.str_at(2) == "[" && self.not_whitespace(3) { 795 | self.skip_char(3); 796 | while !self.str_at(0).is_empty() && self.str_at(0) != "]" { 797 | self.skip_char(1); 798 | } 799 | return true; 800 | } else if self.not_whitespace(2) { 801 | self.skip_char(3); 802 | return true; 803 | } else { 804 | return false; 805 | } 806 | } else if s0s1 == "\\*" { 807 | let mut reg = String::new(); 808 | if self.str_at(2) == "(" && self.not_whitespace(3) && self.not_whitespace(4) { 809 | reg = self.s[3..5].to_owned(); 810 | self.skip_char(5); 811 | } else if self.str_at(2) == "[" && self.not_whitespace(3) { 812 | self.skip_char(3); 813 | while !self.str_at(0).is_empty() && self.str_at(0) != "]" { 814 | reg.push_str(self.str_at(0)); 815 | self.skip_char(1); 816 | } 817 | if let Some(']') = self.s.chars().next() { 818 | self.skip_char(1); 819 | } else { 820 | return false; 821 | } 822 | } else if self.not_whitespace(2) { 823 | reg = self.str_at(2).to_owned(); 824 | self.skip_char(3); 825 | } else { 826 | return false; 827 | } 828 | 829 | if self.reg_table.contains_key(®) { 830 | // This unwrap is safe because of the if 831 | self.s = self.reg_table.get(®).unwrap().to_owned(); 832 | self.text_arg(); 833 | true 834 | } else { 835 | false 836 | } 837 | } else { 838 | false 839 | } 840 | } 841 | 842 | fn size(&mut self) -> bool { 843 | // We require that the string starts with \s 844 | if self.digit(2) || ("-+".contains(self.str_at(2)) && self.digit(3)) { 845 | self.skip_char(3); 846 | while self.digit(0) { 847 | self.skip_char(1); 848 | } 849 | true 850 | } else { 851 | false 852 | } 853 | } 854 | 855 | fn esc(&mut self) -> bool { 856 | // We require that the string start with backslash 857 | if let Some(c) = self.s.chars().nth(1) { 858 | match c { 859 | 'e' | 'E' => self.condputs("\\"), 860 | 't' => self.condputs("\t"), 861 | '0' | '~' => self.condputs(" "), 862 | '|' | '^' | '&' | ':' => (), 863 | _ => self.condputs(c.to_string()), 864 | }; 865 | self.skip_char(2); 866 | true 867 | } else { 868 | false 869 | } 870 | } 871 | 872 | fn word(&mut self) -> bool { 873 | let mut got_something = false; 874 | while let Some(m) = self.g_re_word.find(&self.s) { 875 | got_something = true; 876 | self.condputs(m.as_str()); 877 | let end = m.end(); 878 | self.skip_char(end); 879 | 880 | while self.spec() { 881 | if !self.specletter { 882 | break; 883 | } 884 | } 885 | } 886 | got_something 887 | } 888 | 889 | fn text(&mut self) -> bool { 890 | loop { 891 | if let Some(idx) = self.s.find("\\") { 892 | self.condputs(&self.s[..idx]); 893 | self.skip_char(idx); 894 | if !self.esc_char_backslash() { 895 | self.condputs(self.str_at(0)); 896 | self.skip_char(1); 897 | } 898 | } else { 899 | self.condputs(&self.s); 900 | self.s = String::new(); 901 | return true; 902 | } 903 | } 904 | } 905 | 906 | fn spec(&mut self) -> bool { 907 | self.specletter = false; 908 | 909 | if self.s.get(..2) == Some("\\(") && self.not_whitespace(2) && self.not_whitespace(3) { 910 | let key = self.s.get(2..4).unwrap_or_default(); 911 | 912 | if let Some(value) = Self::g_specs_specletter(key) { 913 | self.condputs(value); 914 | self.specletter = true; 915 | } else if let Some(value) = Self::g_specs(key) { 916 | self.condputs(value); 917 | } 918 | 919 | self.skip_char(4); 920 | true 921 | } else if self.s.starts_with("\\%") { 922 | self.specletter = true; 923 | self.skip_char(2); 924 | true 925 | } else { 926 | false 927 | } 928 | } 929 | 930 | fn esc_char_backslash(&mut self) -> bool { 931 | if let Some(c) = self.s.chars().nth(1) { 932 | match c { 933 | '"' => self.comment(), 934 | 'f' => self.font(), 935 | 's' => self.size(), 936 | 'h' | 'v' | 'w' | 'u' | 'd' => self.numreq(), 937 | 'n' | '*' => self.var(), 938 | '(' => self.spec(), 939 | _ => self.esc(), 940 | } 941 | } else { 942 | false 943 | } 944 | } 945 | 946 | /// AKA `prch` 947 | fn not_whitespace(&self, idx: usize) -> bool { 948 | // # Note that this return False for the empty string (idx >= len(self.s)) 949 | // ch = self.s[idx:idx+1] 950 | // return ch not in ' \t\n' 951 | // TODO Investigate checking for ASCII whitespace after mvp 952 | self.s 953 | .chars() 954 | .nth(idx) 955 | .map(|op| !op.is_whitespace()) 956 | .unwrap_or_default() 957 | } 958 | /// `condputs` (cond)itionally (puts) `s` into `self.output` 959 | /// if `self.tr` is set, instead of putting `s` into `self.output` directly, 960 | /// it `translate`s it using the set translation table and puts the result 961 | /// into `self.output` 962 | fn condputs>(&self, s: S) { 963 | let s = s.as_ref(); 964 | let is_special = { 965 | self.pic || self.eqn || self.refer || self.r#macro != 0 || self.inlist || self.inheader 966 | }; 967 | 968 | if !is_special { 969 | let mut o = self.output.take(); 970 | if let Some(table) = &self.tr { 971 | o.push_str(&table.translate(s)); 972 | } else { 973 | o.push_str(s); 974 | } 975 | self.output.set(o); 976 | } 977 | } 978 | 979 | fn number(&mut self) -> bool { 980 | if let Some(mat) = self.g_re_number.find(&self.s) { 981 | self.condputs(mat.as_str()); 982 | let end = mat.end(); 983 | self.skip_char(end); 984 | true 985 | } else { 986 | false 987 | } 988 | } 989 | 990 | fn esc_char(&mut self) -> bool { 991 | if self.s.chars().next() == Some('\\') { 992 | self.esc_char_backslash() 993 | } else { 994 | self.word() || self.number() 995 | } 996 | } 997 | 998 | fn quoted_arg(&mut self) -> bool { 999 | if self.str_at(0) == "\"" { 1000 | // We've now entered a portion of the source that should be 1001 | // surrounded by double quotes. (We've found the first one—really 1002 | // hoping we find its mate later). 1003 | self.skip_char(1); 1004 | while !self.s.is_empty() && self.str_at(0) != "\"" { 1005 | if !self.esc_char() { 1006 | self.condputs(self.str_at(0)); 1007 | self.skip_char(1); 1008 | } 1009 | } 1010 | // We've run past the end of the string OR we've found the closing 1011 | // double-quote to match the initial one we found at the start of 1012 | // the function. 1013 | true 1014 | } else { 1015 | // We don't start with quotes! 1016 | false 1017 | } 1018 | } 1019 | 1020 | fn do_tbl(&mut self) -> bool { 1021 | match self.tblstate { 1022 | TblState::Options => { 1023 | while !self.s.is_empty() && !"\n;".contains(self.str_at(0)) { 1024 | self.skip_leading_whitespace(); 1025 | 1026 | if !self.str_at(0).chars().all(|c| c.is_alphabetic()) { 1027 | self.skip_char(1); 1028 | } else { 1029 | // Parse option 1030 | 1031 | // find first non-alphabetic character 1032 | match self.s.char_indices().find(|(_, c)| !c.is_alphabetic()) { 1033 | Some((idx, '(')) => { 1034 | // self.s -> option '(' arg ')' rest 1035 | let option = &self.s[..idx]; 1036 | let mut iter = self.s[idx + 1..].splitn(2, ')'); 1037 | let arg = iter.next().unwrap_or_default(); 1038 | let rest = iter.next().unwrap_or_default(); 1039 | 1040 | if option.to_lowercase() == "tab" { 1041 | self.tbl_tab = arg.get(0..1).unwrap_or_default().to_owned(); 1042 | } 1043 | 1044 | self.s = rest.to_owned(); 1045 | } 1046 | _ => self.s.clear(), 1047 | } 1048 | } 1049 | } 1050 | self.tblstate = TblState::Format; 1051 | self.condputs("\n"); 1052 | } 1053 | TblState::Format => { 1054 | while !self.s.is_empty() && !".\n".contains(self.str_at(0)) { 1055 | self.skip_leading_whitespace(); 1056 | if !self.str_at(0).is_empty() { 1057 | self.skip_char(1); 1058 | } 1059 | } 1060 | 1061 | if self.str_at(0) == "." { 1062 | self.tblstate = TblState::Data; 1063 | } 1064 | self.condputs("\n"); 1065 | } 1066 | TblState::Data => { 1067 | if !self.tbl_tab.is_empty() { 1068 | self.s = self.s.replace(&self.tbl_tab, "\t"); 1069 | } 1070 | 1071 | self.text(); 1072 | } 1073 | } 1074 | 1075 | true 1076 | } 1077 | 1078 | fn do_line(&mut self) -> bool { 1079 | match self 1080 | .s 1081 | .bytes() 1082 | .next() 1083 | .expect("`do_line` called when `self.s` was empty") 1084 | { 1085 | b'.' | b'\'' => self.request_or_macro(), 1086 | _ => { 1087 | if self.tbl { 1088 | self.do_tbl() 1089 | } else { 1090 | self.text() 1091 | } 1092 | } 1093 | } 1094 | } 1095 | 1096 | pub fn deroff(&mut self, s: String) { 1097 | let lines = s.split('\n'); 1098 | for line in lines { 1099 | self.s = line.to_owned() + "\n"; 1100 | if !self.do_line() { 1101 | break; 1102 | } 1103 | } 1104 | } 1105 | } 1106 | 1107 | #[test] 1108 | fn test_comment() { 1109 | let mut deroffer = Deroffer::new(); 1110 | 1111 | deroffer.s = "\n".to_owned(); 1112 | deroffer.comment(); 1113 | assert_eq!(deroffer.s, "\n".to_owned()); 1114 | 1115 | deroffer.s = "hello\n".to_owned(); 1116 | deroffer.comment(); 1117 | assert_eq!(deroffer.s, "\n".to_owned()); 1118 | 1119 | deroffer.s = "hello\nworld".to_owned(); 1120 | deroffer.comment(); 1121 | assert_eq!(deroffer.s, "\nworld".to_owned()); 1122 | } 1123 | 1124 | #[test] 1125 | fn test_text_arg() { 1126 | let mut deroffer = Deroffer::new(); 1127 | deroffer.s = "Hello World!".into(); 1128 | assert!(deroffer.text_arg()); 1129 | assert_eq!(deroffer.s, " World!"); 1130 | assert_eq!(deroffer.output.take(), "Hello"); 1131 | 1132 | let mut deroffer = Deroffer::new(); 1133 | assert!(!deroffer.text_arg()); 1134 | assert!(deroffer.s.is_empty()); 1135 | assert!(deroffer.output.take().is_empty()); 1136 | 1137 | let mut deroffer = Deroffer::new(); 1138 | deroffer.s = "\t\n\t \t\n".into(); 1139 | assert!(!deroffer.text_arg()); 1140 | assert_eq!(deroffer.s, "\t\n\t \t\n"); 1141 | assert!(deroffer.output.take().is_empty()); 1142 | 1143 | let mut deroffer = Deroffer::new(); 1144 | deroffer.s = r"\r".into(); 1145 | assert!(!deroffer.text_arg()); 1146 | assert!(deroffer.s.is_empty()); 1147 | assert_eq!(deroffer.output.take(), "r"); 1148 | 1149 | let mut deroffer = Deroffer::new(); 1150 | deroffer.s = "Applebees".into(); 1151 | assert!(deroffer.text_arg()); 1152 | assert_eq!(deroffer.s, ""); 1153 | assert_eq!(deroffer.output.take(), "Applebees"); 1154 | 1155 | let mut deroffer = Deroffer::new(); 1156 | deroffer.s = "忍一时风平浪静,退一步海阔天空。".into(); 1157 | assert!(deroffer.text_arg()); 1158 | assert_eq!(deroffer.s, ""); 1159 | assert_eq!(deroffer.output.take(), "忍一时风平浪静,退一步海阔天空。"); 1160 | } 1161 | 1162 | #[test] 1163 | fn test_font() { 1164 | let mut deroffer = Deroffer::new(); 1165 | deroffer.s = r"\f(aa)lemon".into(); 1166 | assert!(deroffer.font()); 1167 | assert_eq!(deroffer.s, ")lemon"); 1168 | assert!(!deroffer.font()); 1169 | assert_eq!(deroffer.s, ")lemon"); 1170 | } 1171 | 1172 | #[test] 1173 | fn test_numreq() { 1174 | let mut deroffer = Deroffer::new(); 1175 | deroffer.s = "Hello World!".into(); 1176 | assert!(!deroffer.numreq()); 1177 | assert_eq!(deroffer.s, "Hello World!"); 1178 | assert!(deroffer.output.take().is_empty()); 1179 | 1180 | deroffer.s = r"\w'Apple'".into(); 1181 | assert!(deroffer.numreq()); 1182 | assert!(deroffer.s.is_empty()); 1183 | assert!(deroffer.output.take().is_empty()); 1184 | 1185 | deroffer.s = r"\w'Hello\tWorld!'".into(); 1186 | assert!(deroffer.numreq()); 1187 | assert_eq!(deroffer.s, "!'"); 1188 | assert!(deroffer.output.take().is_empty()); 1189 | } 1190 | 1191 | #[test] 1192 | fn test_size() { 1193 | let mut deroffer = Deroffer::new(); 1194 | deroffer.s = "Hello World!".into(); 1195 | assert!(!deroffer.size()); 1196 | 1197 | deroffer.s = r"\s10Hello World!".into(); 1198 | assert!(deroffer.size()); 1199 | assert_eq!(deroffer.s, "Hello World!"); 1200 | 1201 | deroffer.s = r"\s-11 ignore me".into(); 1202 | assert!(deroffer.size()); 1203 | assert_eq!(deroffer.s, " ignore me"); 1204 | 1205 | assert!(deroffer.output.take().is_empty()); 1206 | } 1207 | 1208 | #[test] 1209 | fn test_esc() { 1210 | let mut deroffer = Deroffer::new(); 1211 | assert!(!deroffer.esc()); 1212 | 1213 | // This is UB, but it's the same UB as the python 1214 | deroffer.s = "Hello World!".into(); 1215 | assert!(deroffer.esc()); 1216 | assert_eq!(deroffer.output.take(), "\\"); 1217 | 1218 | deroffer.s = r"\E".into(); 1219 | assert!(deroffer.esc()); 1220 | assert_eq!(deroffer.output.take(), "\\"); 1221 | deroffer.s = r"\t".into(); 1222 | assert!(deroffer.esc()); 1223 | assert_eq!(deroffer.output.take(), "\t"); 1224 | 1225 | deroffer.s = r"\~".into(); 1226 | assert!(deroffer.esc()); 1227 | assert_eq!(deroffer.output.take(), " "); 1228 | 1229 | deroffer.s = r"\|".into(); 1230 | assert!(deroffer.esc()); 1231 | assert!(deroffer.output.take().is_empty()); 1232 | 1233 | deroffer.s = r"\apple".into(); 1234 | assert!(deroffer.esc()); 1235 | assert_eq!(deroffer.output.take(), "a"); 1236 | assert_eq!(deroffer.s, "pple"); 1237 | } 1238 | 1239 | #[test] 1240 | fn test_word() { 1241 | let mut deroffer = Deroffer::new(); 1242 | 1243 | deroffer.s = "Hello World!".into(); 1244 | assert!(deroffer.word()); 1245 | assert_eq!(deroffer.s, " World!"); 1246 | assert_eq!(deroffer.output.take(), "Hello"); 1247 | 1248 | deroffer.s = "Hello\\(ps".into(); 1249 | assert!(deroffer.word()); 1250 | assert_eq!(deroffer.s, ""); 1251 | assert_eq!(deroffer.output.take(), "Hello¶"); 1252 | 1253 | deroffer.s = "100 thousand".into(); 1254 | assert!(!deroffer.word()); 1255 | assert_eq!(deroffer.s, "100 thousand"); 1256 | assert_eq!(deroffer.output.take(), ""); 1257 | } 1258 | 1259 | #[test] 1260 | fn test_text() { 1261 | let mut deroffer = Deroffer::new(); 1262 | 1263 | deroffer.s = "Hello World!".into(); 1264 | assert!(deroffer.text()); 1265 | assert_eq!(deroffer.s, ""); 1266 | assert_eq!(deroffer.output.take(), "Hello World!"); 1267 | 1268 | deroffer.s = "Hello\tWorld!".into(); 1269 | assert!(deroffer.text()); 1270 | assert_eq!(deroffer.s, ""); 1271 | assert_eq!(deroffer.output.take(), "Hello\tWorld!"); 1272 | 1273 | deroffer.s = "Hello\\(psWorld!".into(); 1274 | assert!(deroffer.text()); 1275 | assert_eq!(deroffer.s, ""); 1276 | assert_eq!(deroffer.output.take(), "Hello¶World!"); 1277 | 1278 | deroffer.s = "Hello 10 World!".into(); 1279 | assert!(deroffer.text()); 1280 | assert_eq!(deroffer.s, ""); 1281 | assert_eq!(deroffer.output.take(), "Hello 10 World!"); 1282 | 1283 | deroffer.s = "你好世界!".into(); 1284 | assert!(deroffer.text()); 1285 | assert_eq!(deroffer.s, ""); 1286 | assert_eq!(deroffer.output.take(), "你好世界!"); 1287 | } 1288 | 1289 | #[test] 1290 | fn test_esc_char_backslash() { 1291 | let mut deroffer = Deroffer::new(); 1292 | 1293 | deroffer.s = r#"\"This is a comment, it will be ignored"#.into(); 1294 | assert!(deroffer.esc_char_backslash()); 1295 | assert!(deroffer.s.is_empty()); 1296 | 1297 | // This gets passed to `font`, so i stole a test from there 1298 | deroffer.s = r"\f(aa)lemon".into(); 1299 | assert!(deroffer.esc_char_backslash()); 1300 | assert_eq!(deroffer.s, ")lemon"); 1301 | 1302 | // This gets passed to `size` 1303 | deroffer.s = r"\s-11 ignore me".into(); 1304 | assert!(deroffer.esc_char_backslash()); 1305 | assert_eq!(deroffer.s, " ignore me"); 1306 | 1307 | // You get the idea 1308 | // Taken from numreq 1309 | deroffer.s = r"\w'Apple'".into(); 1310 | assert!(deroffer.esc_char_backslash()); 1311 | assert!(deroffer.s.is_empty()); 1312 | assert!(deroffer.output.take().is_empty()); 1313 | 1314 | // Taken from var 1315 | deroffer.s = "\\*[test_reg]".to_owned(); 1316 | deroffer 1317 | .reg_table 1318 | .insert("test_reg".to_owned(), "It me!".to_owned()); 1319 | assert!(deroffer.esc_char_backslash()); 1320 | assert_eq!(deroffer.s, " me!"); 1321 | assert!(deroffer.output.take().contains("It")); 1322 | 1323 | // Taken from spec 1324 | deroffer.s = "\\(Sdaaaa".into(); // `ð` 1325 | assert!(deroffer.esc_char_backslash()); 1326 | assert!(deroffer.specletter); 1327 | assert_eq!(deroffer.output.take(), "ð"); 1328 | assert_eq!(deroffer.s, "aaaa"); 1329 | 1330 | // Taken from esc 1331 | deroffer.s = r"\E".into(); 1332 | assert!(deroffer.esc_char_backslash()); 1333 | assert_eq!(deroffer.output.take(), "\\"); 1334 | 1335 | // This is UB, but it's the same UB as the python 1336 | deroffer.s = "Hello World!".into(); 1337 | assert!(deroffer.esc_char_backslash()); 1338 | assert_eq!(deroffer.output.take(), "\\"); 1339 | } 1340 | 1341 | #[test] 1342 | fn test_esc_char() { 1343 | // Gets passed to esc_char_backslash, stealing one test to make sure it works 1344 | let mut deroffer = Deroffer::new(); 1345 | deroffer.s = r#"\"This is a comment, it will be ignored"#.into(); 1346 | assert!(deroffer.esc_char()); 1347 | assert!(deroffer.s.is_empty()); 1348 | 1349 | // Will get passed to word, stealing a test 1350 | deroffer.s = "Hello\\(ps".into(); 1351 | assert!(deroffer.esc_char()); 1352 | assert_eq!(deroffer.s, ""); 1353 | assert_eq!(deroffer.output.take(), "Hello¶"); 1354 | 1355 | // Will get passed to number, stealing a test 1356 | deroffer.s = String::from("4343xx7"); 1357 | assert_eq!(deroffer.number(), true); 1358 | assert_eq!(deroffer.output.take(), "4343".to_string()); 1359 | } 1360 | 1361 | #[test] 1362 | fn test_quoted_arg() { 1363 | let mut deroffer = Deroffer::new(); 1364 | deroffer.s = r#""Hello World!""#.into(); 1365 | assert!(deroffer.quoted_arg()); 1366 | assert_eq!(deroffer.s, "\""); 1367 | assert_eq!(deroffer.output.take(), "Hello World!"); 1368 | 1369 | deroffer.s = r#""Hello\(psWorld""#.into(); 1370 | assert!(deroffer.quoted_arg()); 1371 | assert_eq!(deroffer.s, "\""); 1372 | assert_eq!(deroffer.output.take(), "Hello¶World"); 1373 | } 1374 | 1375 | #[test] 1376 | fn test_do_line() { 1377 | let mut deroffer = Deroffer::new(); 1378 | 1379 | // Gets passed to request_or_macro, stealing a test 1380 | deroffer.s = ".SH".into(); 1381 | assert!(deroffer.do_line()); 1382 | assert!(deroffer.s.is_empty()); 1383 | assert!(deroffer.output.take().is_empty()); 1384 | 1385 | // same, to_tbl 1386 | deroffer.s = "aaa(bbb);Hello World!".into(); 1387 | assert!(deroffer.do_tbl()); 1388 | assert!(deroffer.tbl_tab.is_empty()); 1389 | assert_eq!(deroffer.s, ";Hello World!"); 1390 | assert_eq!(deroffer.output.take(), "\n"); 1391 | assert_eq!(deroffer.tblstate, TblState::Format); 1392 | 1393 | // same, text 1394 | deroffer.s = "Hello\\(psWorld!".into(); 1395 | assert!(deroffer.text()); 1396 | assert_eq!(deroffer.s, ""); 1397 | assert_eq!(deroffer.output.take(), "Hello¶World!"); 1398 | } 1399 | 1400 | #[test] 1401 | fn test_request_or_macro() { 1402 | let mut deroffer = Deroffer::new(); 1403 | 1404 | deroffer.s = "'_[Hello".into(); 1405 | assert!(deroffer.request_or_macro()); 1406 | assert!(deroffer.refer); 1407 | assert_eq!(deroffer.s, "_[Hello"); 1408 | assert!(deroffer.output.take().is_empty()); 1409 | 1410 | deroffer.s = "'_]Hello".into(); 1411 | assert!(deroffer.request_or_macro()); 1412 | assert!(!deroffer.refer); 1413 | assert!(deroffer.s.is_empty()); 1414 | assert_eq!(deroffer.output.take(), "]Hello"); 1415 | 1416 | deroffer.s = "'_.Hello".into(); 1417 | assert!(deroffer.request_or_macro()); 1418 | assert!(deroffer.r#macro == 0); 1419 | assert_eq!(deroffer.s, "_.Hello"); 1420 | assert_eq!(deroffer.output.take(), "\n"); 1421 | 1422 | deroffer.s = ".SH".into(); 1423 | assert!(deroffer.request_or_macro()); 1424 | assert!(deroffer.s.is_empty()); 1425 | assert!(deroffer.output.take().is_empty()); 1426 | 1427 | deroffer.s = ".] Hello World".into(); 1428 | assert!(deroffer.request_or_macro()); 1429 | assert!(deroffer.s.is_empty()); 1430 | assert_eq!(deroffer.output.take(), "Hello World"); 1431 | } 1432 | 1433 | #[test] 1434 | fn test_deroff() {} 1435 | 1436 | #[test] 1437 | fn test_deroff_files() {} 1438 | 1439 | #[test] 1440 | fn test_do_tbl() { 1441 | // I made this based on the python source to make sure it's doing the right things 1442 | // also, I create new deroffers for each test to reset the shared state 1443 | 1444 | // 1445 | 1446 | let mut deroffer = Deroffer::new(); 1447 | deroffer.tblstate = TblState::Options; 1448 | 1449 | deroffer.s = "aaa(bbb);Hello World!".into(); 1450 | assert!(deroffer.do_tbl()); 1451 | assert!(deroffer.tbl_tab.is_empty()); 1452 | assert_eq!(deroffer.s, ";Hello World!"); 1453 | assert_eq!(deroffer.output.take(), "\n"); 1454 | assert_eq!(deroffer.tblstate, TblState::Format); 1455 | 1456 | let mut deroffer = Deroffer::new(); 1457 | deroffer.tblstate = TblState::Options; 1458 | deroffer.s = "aaa(bbb;Hello World!".into(); 1459 | assert!(deroffer.do_tbl()); 1460 | assert!(deroffer.tbl_tab.is_empty()); 1461 | assert!(deroffer.s.is_empty()); 1462 | assert_eq!(deroffer.tblstate, TblState::Format); 1463 | 1464 | let mut deroffer = Deroffer::new(); 1465 | deroffer.tblstate = TblState::Options; 1466 | deroffer.s = ";".into(); 1467 | assert!(deroffer.do_tbl()); 1468 | assert!(deroffer.tbl_tab.is_empty()); 1469 | assert_eq!(deroffer.s, ";"); 1470 | assert_eq!(deroffer.tblstate, TblState::Format); 1471 | 1472 | let mut deroffer = Deroffer::new(); 1473 | deroffer.tblstate = TblState::Options; 1474 | deroffer.s = "\n".into(); 1475 | assert!(deroffer.do_tbl()); 1476 | assert!(deroffer.tbl_tab.is_empty()); 1477 | assert_eq!(deroffer.s, "\n"); 1478 | assert_eq!(deroffer.tblstate, TblState::Format); 1479 | 1480 | let mut deroffer = Deroffer::new(); 1481 | deroffer.tblstate = TblState::Options; 1482 | assert!(deroffer.do_tbl()); 1483 | assert!(deroffer.tbl_tab.is_empty()); 1484 | assert!(deroffer.s.is_empty()); 1485 | 1486 | let mut deroffer = Deroffer::new(); 1487 | deroffer.tblstate = TblState::Options; 1488 | deroffer.s = "Tab(arg);".into(); 1489 | assert!(deroffer.do_tbl()); 1490 | assert_eq!(deroffer.tbl_tab, "a"); 1491 | assert_eq!(deroffer.s, ";"); 1492 | assert_eq!(deroffer.tblstate, TblState::Format); 1493 | 1494 | // 1495 | 1496 | // 1497 | 1498 | let mut deroffer = Deroffer::new(); 1499 | deroffer.tblstate = TblState::Format; 1500 | deroffer.s = "Hello World!".into(); 1501 | assert!(deroffer.do_tbl()); 1502 | assert_ne!(deroffer.tblstate, TblState::Data); 1503 | assert_eq!(deroffer.output.take(), "\n"); 1504 | assert!(deroffer.s.is_empty()); 1505 | 1506 | let mut deroffer = Deroffer::new(); 1507 | deroffer.tblstate = TblState::Format; 1508 | deroffer.s = "".into(); 1509 | assert!(deroffer.do_tbl()); 1510 | assert_ne!(deroffer.tblstate, TblState::Data); 1511 | assert_eq!(deroffer.output.take(), "\n"); 1512 | assert!(deroffer.s.is_empty()); 1513 | 1514 | let mut deroffer = Deroffer::new(); 1515 | deroffer.tblstate = TblState::Format; 1516 | deroffer.s = "Hello World!.foo bar!".into(); 1517 | assert!(deroffer.do_tbl()); 1518 | assert_eq!(deroffer.tblstate, TblState::Data); 1519 | assert_eq!(deroffer.s, ".foo bar!"); 1520 | assert_eq!(deroffer.output.take(), "\n"); 1521 | 1522 | let mut deroffer = Deroffer::new(); 1523 | deroffer.tblstate = TblState::Format; 1524 | deroffer.s = "\n".into(); 1525 | assert!(deroffer.do_tbl()); 1526 | assert_ne!(deroffer.tblstate, TblState::Data); 1527 | assert_eq!(deroffer.s, "\n"); 1528 | assert_eq!(deroffer.output.take(), "\n"); 1529 | 1530 | let mut deroffer = Deroffer::new(); 1531 | deroffer.tblstate = TblState::Format; 1532 | deroffer.s = ".".into(); 1533 | assert!(deroffer.do_tbl()); 1534 | assert_eq!(deroffer.tblstate, TblState::Data); 1535 | assert_eq!(deroffer.s, "."); 1536 | assert_eq!(deroffer.output.take(), "\n"); 1537 | 1538 | // 1539 | // 1540 | 1541 | let mut deroffer = Deroffer::new(); 1542 | deroffer.tblstate = TblState::Data; 1543 | deroffer.tbl_tab = "a".into(); 1544 | 1545 | deroffer.s = "HelloaWorld!".into(); 1546 | assert!(deroffer.do_tbl()); 1547 | assert_eq!(deroffer.tblstate, TblState::Data); 1548 | assert_eq!(deroffer.s, ""); 1549 | assert_eq!(deroffer.output.take(), "Hello\tWorld!"); 1550 | 1551 | // 1552 | } 1553 | 1554 | #[test] 1555 | fn test_spec() { 1556 | // I create new deroffers for each test to reset the shared state 1557 | 1558 | let mut deroffer = Deroffer::new(); 1559 | deroffer.s = "\\(Sdaaaa".into(); // `ð` 1560 | assert!(deroffer.spec()); 1561 | assert!(deroffer.specletter); 1562 | assert_eq!(deroffer.output.take(), "ð"); 1563 | assert_eq!(deroffer.s, "aaaa"); 1564 | 1565 | let mut deroffer = Deroffer::new(); 1566 | deroffer.s = "\\(miaaaa".into(); // `-` 1567 | assert!(deroffer.spec()); 1568 | assert!(!deroffer.specletter); 1569 | assert_eq!(deroffer.output.take(), "-"); 1570 | assert_eq!(deroffer.s, "aaaa"); 1571 | 1572 | let mut deroffer = Deroffer::new(); 1573 | deroffer.s = "\\(aaaaaa".into(); 1574 | assert!(deroffer.spec()); 1575 | assert!(!deroffer.specletter); 1576 | assert!(deroffer.output.take().is_empty()); 1577 | assert_eq!(deroffer.s, "aaaa"); 1578 | 1579 | let mut deroffer = Deroffer::new(); 1580 | deroffer.s = "\\%asdasdasd".into(); 1581 | assert!(deroffer.spec()); 1582 | assert!(deroffer.specletter); 1583 | assert!(deroffer.output.take().is_empty()); 1584 | assert_eq!(deroffer.s, "asdasdasd"); 1585 | 1586 | let mut deroffer = Deroffer::new(); 1587 | deroffer.s = "Hello World!".into(); 1588 | assert!(!deroffer.spec()); 1589 | assert_eq!(deroffer.s, "Hello World!"); 1590 | assert!(deroffer.output.take().is_empty()); 1591 | } 1592 | 1593 | #[test] 1594 | fn test_get_output() { 1595 | let deroffer = Deroffer::new(); 1596 | deroffer.output.set("foo\n\nbar".to_string()); 1597 | assert_eq!(&deroffer.get_output(), "foo\n\nbar"); 1598 | deroffer.output.set("foo\n\n\nbar".to_string()); 1599 | assert_eq!(&deroffer.get_output(), "foo\nbar"); 1600 | } 1601 | 1602 | #[test] 1603 | fn test_not_whitespace() { 1604 | let mut deroffer = Deroffer::new(); 1605 | 1606 | deroffer.s = "".to_owned(); 1607 | assert_eq!(deroffer.not_whitespace(0), false); 1608 | assert_eq!(deroffer.not_whitespace(9), false); 1609 | 1610 | deroffer.s = "ab d".to_owned(); 1611 | // idx 2 = " ", should be false 1612 | assert_eq!(deroffer.not_whitespace(2), false); 1613 | assert_eq!(deroffer.not_whitespace(3), true); 1614 | } 1615 | 1616 | #[test] 1617 | fn test_str_at() { 1618 | let mut deroffer = Deroffer::new(); 1619 | 1620 | assert_eq!(deroffer.str_at(1), ""); 1621 | 1622 | deroffer.s = "ab cd".to_owned(); 1623 | assert_eq!(deroffer.str_at(42), ""); 1624 | assert_eq!(deroffer.str_at(1), "b"); 1625 | 1626 | deroffer.s = "🗻".to_owned(); 1627 | assert_eq!(deroffer.str_at(0), "🗻"); 1628 | assert_eq!(deroffer.str_at(1), ""); 1629 | } 1630 | 1631 | #[test] 1632 | fn test_is_white() { 1633 | let mut deroffer = Deroffer::new(); 1634 | 1635 | assert_eq!(deroffer.is_white(1), false); 1636 | 1637 | deroffer.s = "ab cd".to_owned(); 1638 | assert_eq!(deroffer.is_white(42), false); 1639 | assert_eq!(deroffer.is_white(1), false); 1640 | assert_eq!(deroffer.is_white(2), true); 1641 | assert_eq!(deroffer.is_white(3), false); 1642 | } 1643 | 1644 | #[test] 1645 | fn test_var() { 1646 | let mut d = Deroffer::new(); 1647 | 1648 | // "\n" successes 1649 | d.s = "\\n dyHello".to_owned(); 1650 | assert_eq!(d.var(), true); 1651 | assert_eq!(d.s, "Hello"); 1652 | 1653 | d.s = "\\n(aaHello".to_owned(); 1654 | assert_eq!(d.var(), true); 1655 | assert_eq!(d.s, "Hello"); 1656 | 1657 | d.s = "\\n[skipme] Hello".to_owned(); 1658 | assert_eq!(d.var(), true); 1659 | assert_eq!(d.s, "] Hello"); 1660 | 1661 | d.s = "\\naHello".to_owned(); 1662 | assert_eq!(d.var(), true); 1663 | assert_eq!(d.s, "Hello"); 1664 | 1665 | // "\n" errors 1666 | d.s = "\\n".to_owned(); 1667 | assert_eq!(d.var(), false); 1668 | assert_eq!(d.s, "\\n"); 1669 | 1670 | d.s = "\\n a".to_owned(); 1671 | assert_eq!(d.var(), false); 1672 | assert_eq!(d.s, "\\n a"); 1673 | 1674 | d.s = "\\n da".to_owned(); 1675 | assert_eq!(d.var(), false); 1676 | assert_eq!(d.s, "\\n da"); 1677 | 1678 | // "\*" successes 1679 | 1680 | d.s = "\\*(traaaaaaaaaaaaa".to_owned(); 1681 | d.reg_table 1682 | .insert("tr".to_owned(), "Hello World!".to_owned()); 1683 | assert_eq!(d.var(), true); 1684 | assert_eq!(d.s, " World!"); 1685 | let o = d.output.take(); 1686 | assert!(o.contains("Hello")); 1687 | d.output.set(o); 1688 | 1689 | d.s = "\\*(aaHello World!".to_owned(); 1690 | assert_eq!(d.var(), false); 1691 | assert_eq!(d.s, "Hello World!"); 1692 | 1693 | // ideal case, B is in reg_table 1694 | d.s = "\\*[test_reg]".to_owned(); 1695 | d.reg_table 1696 | .insert("test_reg".to_owned(), "It me!".to_owned()); 1697 | assert_eq!(d.var(), true); 1698 | assert_eq!(d.s, " me!"); 1699 | let o = d.output.take(); 1700 | assert!(o.contains("It")); 1701 | d.output.set(o); 1702 | 1703 | // no "]" 1704 | d.s = "\\*[foo bar :)".to_owned(); 1705 | assert_eq!(d.var(), false); 1706 | assert_eq!(d.s, ""); 1707 | 1708 | // B not in reg_table 1709 | d.s = "\\*[foo bar]abcd".to_owned(); 1710 | assert_eq!(d.var(), false); 1711 | assert_eq!(d.s, "abcd"); 1712 | } 1713 | 1714 | #[test] 1715 | fn test_condputs() { 1716 | let mut d = Deroffer::new(); 1717 | 1718 | let o = d.output.take(); 1719 | assert_eq!(o, String::new()); 1720 | d.output.set(o); 1721 | 1722 | d.condputs("Hello World!\n"); 1723 | 1724 | let o = d.output.take(); 1725 | assert_eq!(o, "Hello World!\n".to_owned()); 1726 | d.output.set(o); 1727 | 1728 | d.pic = true; 1729 | d.condputs("This won't go to output"); 1730 | 1731 | let o = d.output.take(); 1732 | assert_eq!(o, "Hello World!\n".to_owned()); 1733 | d.output.set(o); 1734 | 1735 | d.pic = false; 1736 | d.condputs("This will go to output :)"); 1737 | let o = d.output.take(); 1738 | assert_eq!(o, "Hello World!\nThis will go to output :)".to_owned()); 1739 | d.output.set(o); 1740 | 1741 | // Test the translation check 1742 | d.tr = TranslationTable::new("Ttr", "AAA").ok(); 1743 | d.condputs("Translate test"); 1744 | 1745 | let o = d.output.take(); 1746 | assert_eq!( 1747 | o, 1748 | "Hello World!\nThis will go to output :)AAanslaAe AesA".to_owned() 1749 | ); 1750 | d.output.set(o); 1751 | } 1752 | 1753 | #[test] 1754 | fn test_digit() { 1755 | let mut deroffer = Deroffer::new(); 1756 | 1757 | deroffer.s = "0".to_owned(); 1758 | assert_eq!(deroffer.digit(0), true); 1759 | 1760 | deroffer.s = "9".to_owned(); 1761 | assert_eq!(deroffer.digit(0), true); 1762 | 1763 | deroffer.s = "".to_owned(); 1764 | assert_eq!(deroffer.digit(1), false); 1765 | 1766 | deroffer.s = "1".to_owned(); 1767 | assert_eq!(deroffer.digit(1), false); 1768 | 1769 | deroffer.s = "a".to_owned(); 1770 | assert_eq!(deroffer.digit(0), false); 1771 | 1772 | deroffer.s = " ".to_owned(); 1773 | assert_eq!(deroffer.digit(0), false); 1774 | } 1775 | 1776 | #[test] 1777 | fn test_skip_char() { 1778 | let mut d = Deroffer::new(); 1779 | d.s = String::from(" Hello World一"); 1780 | d.skip_char(6); 1781 | assert_eq!(&d.s, "Hello World一"); 1782 | d.skip_char(5); 1783 | assert_eq!(&d.s, " World一"); 1784 | d.skip_char(9); 1785 | assert_eq!(&d.s, "World一"); 1786 | d.skip_char(5); 1787 | assert_eq!(&d.s, "一"); 1788 | d.skip_char(1); 1789 | assert_eq!(&d.s, ""); 1790 | d.skip_char(1); 1791 | assert_eq!(&d.s, ""); 1792 | } 1793 | 1794 | #[test] 1795 | fn test_skip_leading_whitespace() { 1796 | let mut d = Deroffer::new(); 1797 | d.s = String::from(" Hello World"); 1798 | d.skip_leading_whitespace(); 1799 | assert_eq!(&d.s, "Hello World"); 1800 | d.skip_char(5); 1801 | assert_eq!(&d.s, " World"); 1802 | d.skip_leading_whitespace(); 1803 | assert_eq!(&d.s, "World"); 1804 | d.skip_leading_whitespace(); 1805 | assert_eq!(&d.s, "World"); 1806 | } 1807 | 1808 | #[test] 1809 | fn test_number() { 1810 | let mut d = Deroffer::new(); 1811 | 1812 | d.s = String::from("4343xx7"); 1813 | assert_eq!(d.number(), true); 1814 | let o = d.output.take(); 1815 | assert_eq!(o, "4343".to_string()); 1816 | d.output.set(o); 1817 | 1818 | d.s = String::from("__23"); 1819 | assert_eq!(d.number(), false); 1820 | 1821 | d.s = String::from("-18.5"); 1822 | assert_eq!(d.number(), true); 1823 | let o = d.output.take(); 1824 | assert_eq!(o, "4343-18".to_string()); 1825 | d.output.set(o); 1826 | 1827 | d.s = String::from("+078t"); 1828 | assert_eq!(d.number(), true); 1829 | } 1830 | 1831 | // if __name__ == "__main__": 1832 | // import gzip 1833 | // paths = sys.argv[1:] 1834 | // if True: 1835 | // deroff_files(paths) 1836 | // else: 1837 | // import cProfile, profile, pstats 1838 | // profile.run('deroff_files(paths)', 'fooprof') 1839 | // p = pstats.Stats('fooprof') 1840 | // p.sort_stats('time').print_stats(100) 1841 | // #p.sort_stats('calls').print_callers(.5, 'startswith') 1842 | --------------------------------------------------------------------------------