varcon-core-5.0.7/.cargo_vcs_info.json0000644000000001601046102023000133020ustar { "git": { "sha1": "b859c0df7f391deba73030f79b957e62b4d81dc6" }, "path_in_vcs": "crates/varcon-core" }varcon-core-5.0.7/Cargo.lock0000644000000124561046102023000112700ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "anstream" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys", ] [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "enumflags2" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", ] [[package]] name = "enumflags2_derive" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "normalize-line-endings" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "snapbox" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d9a4b69d5b4116a26b662c0bb38895261da3f3bfc171657d66a2a3c7a41e929" dependencies = [ "anstream", "anstyle", "normalize-line-endings", "similar", "snapbox-macros", ] [[package]] name = "snapbox-macros" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed4a172e483585ebbc7c7f7d1705ca7e3f94f606ed78caa14805673189fd5455" dependencies = [ "anstream", ] [[package]] name = "syn" version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "varcon-core" version = "5.0.7" dependencies = [ "enumflags2", "snapbox", "winnow", ] [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "winnow" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" dependencies = [ "memchr", ] varcon-core-5.0.7/Cargo.toml0000644000000063311046102023000113060ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2024" rust-version = "1.91" name = "varcon-core" version = "5.0.7" build = false include = [ "build.rs", "src/**/*", "Cargo.toml", "Cargo.lock", "LICENSE*", "README.md", "examples/**/*", ] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Varcon-relevant data structures" readme = "README.md" categories = ["text-processing"] license = "MIT OR Apache-2.0" repository = "https://github.com/crate-ci/typos" [package.metadata.docs.rs] all-features = true rustdoc-args = ["--generate-link-to-definition"] [features] default = [] flags = ["dep:enumflags2"] parser = ["dep:winnow"] [lib] name = "varcon_core" path = "src/lib.rs" [dependencies.enumflags2] version = "0.7" optional = true [dependencies.winnow] version = "1.0.0" optional = true [dev-dependencies.snapbox] version = "1.1.0" [lints.clippy] bool_assert_comparison = "allow" branches_sharing_code = "allow" checked_conversions = "warn" collapsible_else_if = "allow" collapsible_if = "allow" create_dir = "warn" dbg_macro = "warn" debug_assert_with_mut_call = "warn" doc_markdown = "warn" empty_enums = "warn" enum_glob_use = "warn" expl_impl_clone_on_copy = "warn" explicit_deref_methods = "warn" explicit_into_iter_loop = "warn" fallible_impl_from = "warn" filter_map_next = "warn" flat_map_option = "warn" float_cmp_const = "warn" fn_params_excessive_bools = "warn" from_iter_instead_of_collect = "warn" if_same_then_else = "allow" implicit_clone = "warn" imprecise_flops = "warn" inconsistent_struct_constructor = "warn" inefficient_to_string = "warn" infinite_loop = "warn" invalid_upcast_comparisons = "warn" large_digit_groups = "warn" large_stack_arrays = "warn" large_types_passed_by_value = "warn" let_and_return = "allow" linkedlist = "warn" lossy_float_literal = "warn" macro_use_imports = "warn" mem_forget = "warn" mutex_integer = "warn" needless_continue = "allow" needless_for_each = "warn" negative_feature_names = "warn" path_buf_push_overwrite = "warn" ptr_as_ptr = "warn" rc_mutex = "warn" redundant_feature_names = "warn" ref_option_ref = "warn" rest_pat_in_fully_bound_structs = "warn" result_large_err = "allow" same_functions_in_if_condition = "warn" self_named_module_files = "warn" semicolon_if_nothing_returned = "warn" str_to_string = "warn" string_add = "warn" string_add_assign = "warn" string_lit_as_bytes = "warn" todo = "warn" trait_duplication_in_bounds = "warn" uninlined_format_args = "warn" verbose_file_reads = "warn" wildcard_imports = "warn" zero_sized_map_values = "warn" [lints.rust] unnameable_types = "warn" unreachable_pub = "warn" unsafe_op_in_unsafe_fn = "warn" unused_lifetimes = "warn" unused_macro_rules = "warn" unused_qualifications = "warn" [lints.rust.rust_2018_idioms] level = "warn" priority = -1 varcon-core-5.0.7/Cargo.toml.orig000064400000000000000000000012011046102023000147340ustar 00000000000000[package] name = "varcon-core" version = "5.0.7" description = "Varcon-relevant data structures" readme = "../../README.md" categories = ["text-processing"] repository.workspace = true license.workspace = true edition.workspace = true rust-version.workspace = true include.workspace = true [package.metadata.docs.rs] all-features = true rustdoc-args = ["--generate-link-to-definition"] [features] default = [] parser = ["dep:winnow"] flags = ["dep:enumflags2"] [dependencies] winnow = { version = "1.0.0", optional = true } enumflags2 = { version = "0.7", optional = true } [lints] workspace = true [dev-dependencies] snapbox = "1.1.0" varcon-core-5.0.7/README.md000064400000000000000000000140171046102023000133350ustar 00000000000000# typos > **Source code spell checker** Finds and corrects spelling mistakes among source code: - Fast enough to run on monorepos - Low false positives so you can run on PRs ![Screenshot](./docs/screenshot.png) [![Downloads](https://img.shields.io/github/downloads/crate-ci/typos/total.svg)](https://github.com/crate-ci/typos/releases) [![codecov](https://codecov.io/gh/crate-ci/typos/branch/master/graph/badge.svg)](https://codecov.io/gh/crate-ci/typos) [![Documentation](https://img.shields.io/badge/docs-master-blue.svg)][Documentation] ![License](https://img.shields.io/crates/l/typos.svg) [![Crates Status](https://img.shields.io/crates/v/typos.svg)][Crates.io] Dual-licensed under [MIT](LICENSE-MIT) or [Apache 2.0](LICENSE-APACHE) ## Documentation - [Installation](#install) - [Getting Started](#getting-started) - [False Positives](#false-positives) - [Integrations](#integrations) - [GitHub Action](docs/github-action.md) - [pre-commit](docs/pre-commit.md) - [Custom](#custom) - [Debugging](#debugging) - [Reference](docs/reference.md) - [FAQ](#faq) - [Comparison with other spell checkers](docs/comparison.md) - [Projects using typos](https://github.com/crate-ci/typos/wiki) - [Benchmarks](benchsuite/runs) - [Design](docs/design.md) - [Contribute](CONTRIBUTING.md) - [CHANGELOG](CHANGELOG.md) ## Install [Download](https://github.com/crate-ci/typos/releases) a pre-built binary (installable via [gh-install](https://github.com/crate-ci/gh-install)). Or use rust to install: ```console $ cargo install typos-cli --locked ``` Or use [Homebrew](https://brew.sh/) to install: ```console $ brew install typos-cli ``` Or use [Conda](https://conda.io/) to install: ```console $ conda install typos ``` Or use [Pacman](https://wiki.archlinux.org/title/pacman) to install: ```console $ sudo pacman -S typos ``` ## Getting Started Most commonly, you'll either want to see what typos are available with ```console $ typos ``` Or have them fixed ```console $ typos --write-changes $ typos -w ``` If there is any ambiguity (multiple possible corrections), `typos` will just report it to the user and move on. ### False Positives Sometimes, what looks like a typo is intentional, like with people's names, acronyms, or localized content. To mark a word or an identifier (grouping of words) as valid, add it to your [`_typos.toml`](docs/reference.md) by declaring itself as the valid spelling: ```toml [default] extend-ignore-identifiers-re = [ # *sigh* this just isn't worth the cost of fixing "AttributeID.*Supress.*", ] [default.extend-identifiers] # *sigh* this just isn't worth the cost of fixing AttributeIDSupressMenu = "AttributeIDSupressMenu" [default.extend-words] # Don't correct the surname "Teh" teh = "teh" ``` For more ways to ignore or extend the dictionary with examples, see the [config reference](docs/reference.md). For cases like localized content, you can disable spell checking of file contents while still checking the file name: ```toml [type.po] extend-glob = ["*.po"] check-file = false ``` (run `typos --type-list` to see configured file types) If you need some more flexibility, you can completely exclude some files from consideration: ```toml [files] extend-exclude = ["localized/*.po"] ``` ### Integrations - [GitHub Actions](docs/github-action.md) - [pre-commit](docs/pre-commit.md) - [🐊Putout Processor](https://github.com/putoutjs/putout-processor-typos) - [Visual Studio Code](https://github.com/tekumara/typos-vscode) - [typos-lsp (Language Server Protocol server)](https://github.com/tekumara/typos-vscode) - [GitLab Code Quality](https://github.com/tahv/typos-gitlab-code-quality) #### Custom `typos` provides several building blocks for custom native integrations - `-` reads from `stdin`, `--write-changes` will be written to `stdout` - `--diff` to provide a diff - `--format json` to get jsonlines with exit code 0 on no errors, code 2 on typos, anything else is an error. Examples: ```console $ # Read file from stdin, write corrected version to stdout $ typos - --write-changes $ # Creates a diff of what would change $ typos dir/file --diff $ # Fully programmatic control $ typos dir/file --format json ``` ### Debugging You can see what the effective config looks like by running ```console $ typos --dump-config - ``` You can then see how typos is processing your project with ```console $ typos --files $ typos --identifiers $ typos --words ``` If you need to dig in more, you can enable debug logging with `-v` ## FAQ ### Why was ... not corrected? **Does the file show up in `typos --files`?** If not, check your config with `typos --dump-config -`. The `[files]` table controls how we walk files. If you are using `files.extend-exclude`, are you running into [#593](https://github.com/crate-ci/typos/issues/593)? If you are using `files.ignore-vcs = true`, is the file in your `.gitignore` but git tracks it anyways? Prefer allowing the file explicitly (see [#909](https://github.com/crate-ci/typos/issues/909)). **Does the identifier show up in `typos --identifiers` or the word show up in `typos --words`?** If not, it might be subject to one of typos' heuristics for detecting non-words (like hashes) or unambiguous words (like words after a `\` escape). If it is showing up, likely `typos` doesn't know about it yet. `typos` maintains a list of known typo corrections to keep the false positive count low so it can safely run unassisted. This is in contrast to most spell checking UIs people use where there is a known list of valid words. In this case, the spell checker tries to guess your intent by finding the closest-looking word. It then has a gauge for when a word isn't close enough and assumes you know best. The user has the opportunity to verify these corrections and explicitly allow or reject them. For more on the trade offs of these approaches, see [Design](docs/design.md). - To correct it locally, see also our [False Positives documentation](#false-positives). - To contribute your correction, see [Contribute](CONTRIBUTING.md) [Crates.io]: https://crates.io/crates/typos-cli [Documentation]: https://docs.rs/typos varcon-core-5.0.7/src/borrowed.rs000064400000000000000000000031251046102023000150340ustar 00000000000000#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct Cluster { pub header: &'static str, pub verified: bool, pub level: usize, pub entries: &'static [Entry], pub notes: &'static [&'static str], } impl Cluster { pub fn into_owned(self) -> crate::Cluster { crate::Cluster { header: self.header.to_owned(), verified: self.verified, level: self.level, entries: self.entries.iter().map(|s| s.into_owned()).collect(), notes: self.notes.iter().map(|s| (*s).to_owned()).collect(), } } } #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct Entry { pub variants: &'static [Variant], pub pos: Option, pub archaic: bool, pub description: Option<&'static str>, pub note: Option<&'static str>, pub comment: Option<&'static str>, } impl Entry { pub fn into_owned(self) -> crate::Entry { crate::Entry { variants: self.variants.iter().map(|v| v.into_owned()).collect(), pos: self.pos, archaic: self.archaic, description: self.description.map(|s| s.to_owned()), note: self.note.map(|s| s.to_owned()), comment: self.comment.map(|s| s.to_owned()), } } } #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct Variant { pub types: &'static [crate::Type], pub word: &'static str, } impl Variant { pub fn into_owned(self) -> crate::Variant { crate::Variant { types: self.types.to_vec(), word: self.word.to_owned(), } } } varcon-core-5.0.7/src/lib.rs000064400000000000000000000061051046102023000137600ustar 00000000000000#![cfg_attr(docsrs, feature(doc_cfg))] #![warn(clippy::print_stderr)] #![warn(clippy::print_stdout)] pub mod borrowed; #[cfg(feature = "parser")] mod parser; #[cfg(feature = "parser")] pub use crate::parser::ClusterIter; #[cfg(feature = "parser")] pub use crate::parser::ParseError; #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub struct Cluster { pub header: String, pub verified: bool, pub level: usize, pub entries: Vec, pub notes: Vec, } impl Cluster { pub fn infer(&mut self) { for entry in self.entries.iter_mut() { entry.infer(); } } } #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub struct Entry { pub variants: Vec, pub pos: Option, pub archaic: bool, pub description: Option, pub note: Option, pub comment: Option, } impl Entry { pub fn infer(&mut self) { imply( &mut self.variants, Category::BritishIse, Category::BritishIze, ); imply(&mut self.variants, Category::BritishIze, Category::Canadian); imply( &mut self.variants, Category::BritishIse, Category::Australian, ); } } fn imply(variants: &mut [Variant], required: Category, missing: Category) { let missing_exists = variants .iter() .any(|v| v.types.iter().any(|t| t.category == missing)); if missing_exists { return; } for variant in variants.iter_mut() { let types: Vec<_> = variant .types .iter() .filter(|t| t.category == required) .cloned() .map(|mut t| { t.category = missing; t }) .collect(); variant.types.extend(types); } } #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub struct Variant { pub types: Vec, pub word: String, } #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] pub struct Type { pub category: Category, pub tag: Option, pub num: Option, } #[cfg_attr(feature = "flags", enumflags2::bitflags)] #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] #[repr(u8)] pub enum Category { American = 0x01, BritishIse = 0x02, BritishIze = 0x04, Canadian = 0x08, Australian = 0x10, Other = 0x20, } #[cfg(feature = "flags")] pub type CategorySet = enumflags2::BitFlags; #[cfg_attr(feature = "flags", enumflags2::bitflags)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] #[repr(u8)] pub enum Tag { Eq = 0x01, Variant = 0x02, Seldom = 0x04, Possible = 0x08, Improper = 0x10, } #[cfg(feature = "flags")] pub type TagSet = enumflags2::BitFlags; #[cfg_attr(feature = "flags", enumflags2::bitflags)] #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] #[repr(u8)] pub enum Pos { Noun = 0x01, Verb = 0x02, Adjective = 0x04, Adverb = 0x08, AdjectiveOrAdverb = 0x10, Interjection = 0x20, Preposition = 0x40, } #[cfg(feature = "flags")] pub type PosSet = enumflags2::BitFlags; varcon-core-5.0.7/src/parser.rs000064400000000000000000001547771046102023000145310ustar 00000000000000use winnow::ascii::space1; use winnow::combinator::alt; use winnow::combinator::cut_err; use winnow::combinator::delimited; use winnow::combinator::opt; use winnow::combinator::preceded; use winnow::combinator::terminated; use winnow::combinator::trace; use winnow::prelude::*; use winnow::token::one_of; use crate::{Category, Cluster, Entry, Pos, Tag, Type, Variant}; #[derive(Clone, PartialEq, Eq, Debug)] pub struct ClusterIter<'i> { input: &'i str, } impl<'i> ClusterIter<'i> { pub fn new(input: &'i str) -> Self { Self { input } } } impl Iterator for ClusterIter<'_> { type Item = Cluster; fn next(&mut self) -> Option { self.input = self.input.trim_start(); Cluster::parse_.parse_next(&mut self.input).ok() } } #[cfg(test)] mod test_cluster_iter { use super::*; use snapbox::ToDebug; use snapbox::assert_data_eq; use snapbox::str; #[test] fn test_single() { let actual = ClusterIter::new( "# acknowledgment (level 35) A Cv: acknowledgment / Av B C: acknowledgement A Cv: acknowledgments / Av B C: acknowledgements A Cv: acknowledgment's / Av B C: acknowledgement's ", ); assert_data_eq!( actual.collect::>().to_debug(), str![[r#" [ Cluster { header: "acknowledgment ", verified: true, level: 35, entries: [ Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "acknowledgment", }, Variant { types: [ Type { category: American, tag: Some( Variant, ), num: None, }, Type { category: BritishIse, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "acknowledgement", }, ], pos: None, archaic: false, description: None, note: None, comment: None, }, Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "acknowledgments", }, Variant { types: [ Type { category: American, tag: Some( Variant, ), num: None, }, Type { category: BritishIse, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "acknowledgements", }, ], pos: None, archaic: false, description: None, note: None, comment: None, }, Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "acknowledgment's", }, Variant { types: [ Type { category: American, tag: Some( Variant, ), num: None, }, Type { category: BritishIse, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "acknowledgement's", }, ], pos: None, archaic: false, description: None, note: None, comment: None, }, ], notes: [], }, ] "#]] ); } #[test] fn test_multiple() { let actual = ClusterIter::new( "# acknowledgment (level 35) A Cv: acknowledgment / Av B C: acknowledgement A Cv: acknowledgments / Av B C: acknowledgements A Cv: acknowledgment's / Av B C: acknowledgement's # acknowledgment (level 35) A Cv: acknowledgment / Av B C: acknowledgement A Cv: acknowledgments / Av B C: acknowledgements A Cv: acknowledgment's / Av B C: acknowledgement's ", ); assert_data_eq!( actual.collect::>().to_debug(), str![[r#" [ Cluster { header: "acknowledgment ", verified: true, level: 35, entries: [ Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "acknowledgment", }, Variant { types: [ Type { category: American, tag: Some( Variant, ), num: None, }, Type { category: BritishIse, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "acknowledgement", }, ], pos: None, archaic: false, description: None, note: None, comment: None, }, Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "acknowledgments", }, Variant { types: [ Type { category: American, tag: Some( Variant, ), num: None, }, Type { category: BritishIse, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "acknowledgements", }, ], pos: None, archaic: false, description: None, note: None, comment: None, }, Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "acknowledgment's", }, Variant { types: [ Type { category: American, tag: Some( Variant, ), num: None, }, Type { category: BritishIse, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "acknowledgement's", }, ], pos: None, archaic: false, description: None, note: None, comment: None, }, ], notes: [], }, Cluster { header: "acknowledgment ", verified: true, level: 35, entries: [ Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "acknowledgment", }, Variant { types: [ Type { category: American, tag: Some( Variant, ), num: None, }, Type { category: BritishIse, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "acknowledgement", }, ], pos: None, archaic: false, description: None, note: None, comment: None, }, Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "acknowledgments", }, Variant { types: [ Type { category: American, tag: Some( Variant, ), num: None, }, Type { category: BritishIse, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "acknowledgements", }, ], pos: None, archaic: false, description: None, note: None, comment: None, }, Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "acknowledgment's", }, Variant { types: [ Type { category: American, tag: Some( Variant, ), num: None, }, Type { category: BritishIse, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "acknowledgement's", }, ], pos: None, archaic: false, description: None, note: None, comment: None, }, ], notes: [], }, ] "#]] ); } } impl Cluster { pub fn parse(input: &str) -> Result { Self::parse_.parse(input).map_err(|_err| ParseError) } fn parse_(input: &mut &str) -> ModalResult { trace("cluster", move |input: &mut &str| { let header = ( "#", winnow::ascii::space0, winnow::token::take_till(1.., ('\r', '\n', '<', '(')), winnow::ascii::space0, opt(("", winnow::ascii::space0)), delimited("(level ", winnow::ascii::digit1, ')').parse_to::(), winnow::ascii::space0, winnow::ascii::line_ending, ); let note = preceded( ("##", winnow::ascii::space0), terminated(winnow::ascii::till_line_ending, winnow::ascii::line_ending), ); let mut cluster = ( header, winnow::combinator::repeat( 1.., terminated(Entry::parse_, winnow::ascii::line_ending), ), winnow::combinator::repeat(0.., note), ); let (header, entries, notes): (_, _, Vec<_>) = cluster.parse_next(input)?; let verified = header.4.is_some(); let level = header.5; let header = header.2.to_owned(); let notes = notes.into_iter().map(|s| s.to_owned()).collect(); let c = Self { header, verified, level, entries, notes, }; Ok(c) }) .parse_next(input) } } #[cfg(test)] mod test_cluster { use super::*; use snapbox::ToDebug; use snapbox::assert_data_eq; use snapbox::str; #[test] fn test_basic() { let (input, actual) = Cluster::parse_ .parse_peek( "# acknowledgment (level 35) A Cv: acknowledgment / Av B C: acknowledgement A Cv: acknowledgments / Av B C: acknowledgements A Cv: acknowledgment's / Av B C: acknowledgement's ", ) .unwrap(); assert_data_eq!( input, str![[r#" "#]] ); assert_data_eq!( actual.to_debug(), str![[r#" Cluster { header: "acknowledgment ", verified: true, level: 35, entries: [ Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "acknowledgment", }, Variant { types: [ Type { category: American, tag: Some( Variant, ), num: None, }, Type { category: BritishIse, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "acknowledgement", }, ], pos: None, archaic: false, description: None, note: None, comment: None, }, Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "acknowledgments", }, Variant { types: [ Type { category: American, tag: Some( Variant, ), num: None, }, Type { category: BritishIse, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "acknowledgements", }, ], pos: None, archaic: false, description: None, note: None, comment: None, }, Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "acknowledgment's", }, Variant { types: [ Type { category: American, tag: Some( Variant, ), num: None, }, Type { category: BritishIse, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "acknowledgement's", }, ], pos: None, archaic: false, description: None, note: None, comment: None, }, ], notes: [], } "#]] ); } #[test] fn test_notes() { let (input, actual) = Cluster::parse_ .parse_peek( "# coloration (level 50) A B C: coloration / B. Cv: colouration A B C: colorations / B. Cv: colourations A B C: coloration's / B. Cv: colouration's ## OED has coloration as the preferred spelling and discolouration as a ## variant for British Engl or some reason ", ) .unwrap(); assert_data_eq!( input, str![[r#" "#]] ); assert_data_eq!( actual.to_debug(), str![[r#" Cluster { header: "coloration ", verified: true, level: 50, entries: [ Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: BritishIse, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "coloration", }, Variant { types: [ Type { category: BritishIse, tag: Some( Eq, ), num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "colouration", }, ], pos: None, archaic: false, description: None, note: None, comment: None, }, Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: BritishIse, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "colorations", }, Variant { types: [ Type { category: BritishIse, tag: Some( Eq, ), num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "colourations", }, ], pos: None, archaic: false, description: None, note: None, comment: None, }, Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: BritishIse, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "coloration's", }, Variant { types: [ Type { category: BritishIse, tag: Some( Eq, ), num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "colouration's", }, ], pos: None, archaic: false, description: None, note: None, comment: None, }, ], notes: [ "OED has coloration as the preferred spelling and discolouration as a", "variant for British Engl or some reason", ], } "#]] ); } } impl Entry { pub fn parse(input: &str) -> Result { Self::parse_.parse(input).map_err(|_err| ParseError) } fn parse_(input: &mut &str) -> ModalResult { trace("entry", move |input: &mut &str| { let var_sep = (winnow::ascii::space0, '/', winnow::ascii::space0); let variants = winnow::combinator::separated(1.., Variant::parse_, var_sep).parse_next(input)?; let mut e = Self::parse_description.parse_next(input)?; let comment_sep = (winnow::ascii::space0, '#'); let comment = opt((comment_sep, space1, winnow::ascii::till_line_ending)).parse_next(input)?; let _ = winnow::ascii::space0.parse_next(input)?; e.variants = variants; e.comment = comment.map(|c| c.2.to_owned()); Ok(e) }) .parse_next(input) } fn parse_description(input: &mut &str) -> ModalResult { trace("description", move |input: &mut &str| { let mut entry = Self { variants: Vec::new(), pos: None, archaic: false, description: None, note: None, comment: None, }; if opt((winnow::ascii::space0, '|')) .parse_next(input)? .is_some() { let _ = opt((space1, "")).parse_next(input)?; let _ = opt((space1, "")).parse_next(input)?; entry.pos = opt(delimited((space1, '<'), cut_err(Pos::parse_), cut_err('>'))) .parse_next(input)?; entry.archaic = opt(preceded(space1, archaic)).parse_next(input)?.is_some(); entry.note = opt(preceded(space1, note)).parse_next(input)?; entry.description = opt(preceded(space1, description)).parse_next(input)?; if opt((winnow::ascii::space0, '|')) .parse_next(input)? .is_some() { entry.note = opt(preceded(space1, note)).parse_next(input)?; } } Ok(entry) }) .parse_next(input) } } fn note(input: &mut &str) -> ModalResult { let (_, _, note) = (NOTE_PREFIX, space1, description).parse_next(input)?; Ok(note) } const NOTE_PREFIX: &str = "--"; fn archaic(input: &mut &str) -> ModalResult<(), ()> { "(-)".void().parse_next(input) } fn description(input: &mut &str) -> ModalResult { let description = winnow::token::take_till(0.., ('\n', '\r', '#', '|')).parse_next(input)?; Ok(description.to_owned()) } #[cfg(test)] mod test_entry { #![allow(clippy::bool_assert_comparison)] use super::*; use snapbox::ToDebug; use snapbox::assert_data_eq; use snapbox::str; #[test] fn test_variant_only() { // Having nothing after `A` causes an incomplete parse. Shouldn't be a problem for my use // cases. let (input, actual) = Entry::parse_ .parse_peek("A Cv: acknowledgment's / Av B C: acknowledgement's\n") .unwrap(); assert_data_eq!( input, str![[r#" "#]] ); assert_data_eq!( actual.to_debug(), str![[r#" Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "acknowledgment's", }, Variant { types: [ Type { category: American, tag: Some( Variant, ), num: None, }, Type { category: BritishIse, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "acknowledgement's", }, ], pos: None, archaic: false, description: None, note: None, comment: None, } "#]] ); } #[test] fn test_description() { // Having nothing after `A` causes an incomplete parse. Shouldn't be a problem for my use // cases. let (input, actual) = Entry::parse_ .parse_peek("A C: prize / B: prise | otherwise\n") .unwrap(); assert_data_eq!( input, str![[r#" "#]] ); assert_data_eq!( actual.to_debug(), str![[r#" Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "prize", }, Variant { types: [ Type { category: BritishIse, tag: None, num: None, }, ], word: "prise", }, ], pos: None, archaic: false, description: Some( "otherwise", ), note: None, comment: None, } "#]] ); } #[test] fn test_pos() { // Having nothing after `A` causes an incomplete parse. Shouldn't be a problem for my use // cases. let (input, actual) = Entry::parse_ .parse_peek("A B C: practice / AV Cv: practise | \n") .unwrap(); assert_data_eq!( input, str![[r#" "#]] ); assert_data_eq!( actual.to_debug(), str![[r#" Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: BritishIse, tag: None, num: None, }, Type { category: Canadian, tag: None, num: None, }, ], word: "practice", }, Variant { types: [ Type { category: American, tag: Some( Seldom, ), num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "practise", }, ], pos: Some( Noun, ), archaic: false, description: None, note: None, comment: None, } "#]] ); } #[test] fn test_pos_bad() { // Having nothing after `A` causes an incomplete parse. Shouldn't be a problem for my use // cases. let err = Entry::parse_ .parse_peek("A B C: practice / AV Cv: practise | \n") .unwrap_err(); assert_data_eq!(err.to_string(), str!["Parsing Failure: ()"]); } #[test] fn test_plural() { // Having nothing after `A` causes an incomplete parse. Shouldn't be a problem for my use // cases. let (input, actual) = Entry::parse_.parse_peek("_ _-: dogies | \n").unwrap(); assert_data_eq!( input, str![[r#" "#]] ); assert_data_eq!( actual.to_debug(), str![[r#" Entry { variants: [ Variant { types: [ Type { category: Other, tag: None, num: None, }, Type { category: Other, tag: Some( Possible, ), num: None, }, ], word: "dogies", }, ], pos: None, archaic: false, description: None, note: None, comment: None, } "#]] ); } #[test] fn test_abbr() { // Having nothing after `A` causes an incomplete parse. Shouldn't be a problem for my use // cases. let (input, actual) = Entry::parse_.parse_peek("A B: ha | \n").unwrap(); assert_data_eq!( input, str![[r#" "#]] ); assert_data_eq!( actual.to_debug(), str![[r#" Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: BritishIse, tag: None, num: None, }, ], word: "ha", }, ], pos: None, archaic: false, description: None, note: None, comment: None, } "#]] ); } #[test] fn test_archaic() { // Having nothing after `A` causes an incomplete parse. Shouldn't be a problem for my use // cases. let (input, actual) = Entry::parse_ .parse_peek("A: bark / Av B: barque | (-) ship\n") .unwrap(); assert_data_eq!( input, str![[r#" "#]] ); assert_data_eq!( actual.to_debug(), str![[r#" Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, ], word: "bark", }, Variant { types: [ Type { category: American, tag: Some( Variant, ), num: None, }, Type { category: BritishIse, tag: None, num: None, }, ], word: "barque", }, ], pos: None, archaic: true, description: Some( "ship", ), note: None, comment: None, } "#]] ); } #[test] fn test_note() { // Having nothing after `A` causes an incomplete parse. Shouldn't be a problem for my use // cases. let (input, actual) = Entry::parse_ .parse_peek("_: cabbies | -- plural\n") .unwrap(); assert_data_eq!( input, str![[r#" "#]] ); assert_data_eq!( actual.to_debug(), str![[r#" Entry { variants: [ Variant { types: [ Type { category: Other, tag: None, num: None, }, ], word: "cabbies", }, ], pos: None, archaic: false, description: None, note: Some( "plural", ), comment: None, } "#]] ); } #[test] fn test_description_and_note() { // Having nothing after `A` causes an incomplete parse. Shouldn't be a problem for my use // cases. let (input, actual) = Entry::parse_ .parse_peek("A B: wizz | as in \"gee whiz\" | -- Ox: informal, chiefly N. Amer.\n") .unwrap(); assert_data_eq!( input, str![[r#" "#]] ); assert_data_eq!( actual.to_debug(), str![[r#" Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: BritishIse, tag: None, num: None, }, ], word: "wizz", }, ], pos: None, archaic: false, description: Some( "as in /"gee whiz/" ", ), note: Some( "Ox: informal, chiefly N. Amer.", ), comment: None, } "#]] ); } #[test] fn test_trailing_comment() { let (input, actual) = Entry::parse_.parse_peek( "A B: accursed / AV B-: accurst # ODE: archaic, M-W: 'or' but can find little evidence of use\n", ) .unwrap(); assert_data_eq!( input, str![[r#" "#]] ); assert_data_eq!( actual.to_debug(), str![[r#" Entry { variants: [ Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: BritishIse, tag: None, num: None, }, ], word: "accursed", }, Variant { types: [ Type { category: American, tag: Some( Seldom, ), num: None, }, Type { category: BritishIse, tag: Some( Possible, ), num: None, }, ], word: "accurst", }, ], pos: None, archaic: false, description: None, note: None, comment: Some( "ODE: archaic, M-W: 'or' but can find little evidence of use", ), } "#]] ); } } impl Variant { pub fn parse(input: &str) -> Result { Self::parse_.parse(input).map_err(|_err| ParseError) } fn parse_(input: &mut &str) -> ModalResult { trace("variant", move |input: &mut &str| { let types = winnow::combinator::separated(1.., Type::parse_, space1); let columns = winnow::combinator::separated(0.., winnow::ascii::digit1, space1).map(|()| ()); let sep = (":", winnow::ascii::space0); let ((types, _, _columns), word) = winnow::combinator::separated_pair( (types, winnow::ascii::space0, columns), sep, word, ) .parse_next(input)?; let v = Self { types, word }; Ok(v) }) .parse_next(input) } } fn word(input: &mut &str) -> ModalResult { trace("word", move |input: &mut &str| { winnow::token::take_till(1.., |item: char| item.is_ascii_whitespace()) .map(|s: &str| s.to_owned().replace('_', " ")) .parse_next(input) }) .parse_next(input) } #[cfg(test)] mod test_variant { use super::*; use snapbox::ToDebug; use snapbox::assert_data_eq; use snapbox::str; #[test] fn test_valid() { // Having nothing after `A` causes an incomplete parse. Shouldn't be a problem for my use // cases. let (input, actual) = Variant::parse_.parse_peek("A Cv: acknowledgment ").unwrap(); assert_data_eq!(input, str![" "]); assert_data_eq!( actual.to_debug(), str![[r#" Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "acknowledgment", } "#]] ); } #[test] fn test_extra() { let (input, actual) = Variant::parse_ .parse_peek("A Cv: acknowledgment's / Av B C: acknowledgement's") .unwrap(); assert_data_eq!(input, str![" / Av B C: acknowledgement's"]); assert_data_eq!( actual.to_debug(), str![[r#" Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: Canadian, tag: Some( Variant, ), num: None, }, ], word: "acknowledgment's", } "#]] ); } #[test] fn test_underscore() { let (input, actual) = Variant::parse_.parse_peek("_: air_gun\n").unwrap(); assert_data_eq!( input, str![[r#" "#]] ); assert_data_eq!( actual.to_debug(), str![[r#" Variant { types: [ Type { category: Other, tag: None, num: None, }, ], word: "air gun", } "#]] ); } #[test] fn test_columns() { // Having nothing after `A` causes an incomplete parse. Shouldn't be a problem for my use // cases. let (input, actual) = Variant::parse_.parse_peek("A B 1 2: aeries").unwrap(); assert_data_eq!(input, str![""]); assert_data_eq!( actual.to_debug(), str![[r#" Variant { types: [ Type { category: American, tag: None, num: None, }, Type { category: BritishIse, tag: None, num: None, }, ], word: "aeries", } "#]] ); } } impl Type { pub fn parse(input: &str) -> Result { Self::parse_.parse(input).map_err(|_err| ParseError) } fn parse_(input: &mut &str) -> ModalResult { trace("type", move |input: &mut &str| { let category = Category::parse_(input)?; let tag = opt(Tag::parse_).parse_next(input)?; let num = opt(winnow::ascii::digit1).parse_next(input)?; let num = num.map(|s| s.parse().expect("parser ensured it's a number")); let t = Type { category, tag, num }; Ok(t) }) .parse_next(input) } } #[cfg(test)] mod test_type { use super::*; use snapbox::ToDebug; use snapbox::assert_data_eq; use snapbox::str; #[test] fn test_valid() { // Having nothing after `A` causes an incomplete parse. Shouldn't be a problem for my use // cases. let (input, actual) = Type::parse_.parse_peek("A ").unwrap(); assert_data_eq!(input, str![" "]); assert_data_eq!( actual.to_debug(), str![[r#" Type { category: American, tag: None, num: None, } "#]] ); let (input, actual) = Type::parse_.parse_peek("Bv ").unwrap(); assert_data_eq!(input, str![" "]); assert_data_eq!( actual.to_debug(), str![[r#" Type { category: BritishIse, tag: Some( Variant, ), num: None, } "#]] ); } #[test] fn test_extra() { let (input, actual) = Type::parse_.parse_peek("Z foobar").unwrap(); assert_data_eq!(input, str![" foobar"]); assert_data_eq!( actual.to_debug(), str![[r#" Type { category: BritishIze, tag: None, num: None, } "#]] ); let (input, actual) = Type::parse_.parse_peek("C- foobar").unwrap(); assert_data_eq!(input, str![" foobar"]); assert_data_eq!( actual.to_debug(), str![[r#" Type { category: Canadian, tag: Some( Possible, ), num: None, } "#]] ); } #[test] fn test_num() { let (input, actual) = Type::parse_.parse_peek("Av1 ").unwrap(); assert_data_eq!(input, str![" "]); assert_data_eq!( actual.to_debug(), str![[r#" Type { category: American, tag: Some( Variant, ), num: Some( 1, ), } "#]] ); } } impl Category { pub fn parse(input: &str) -> Result { Self::parse_.parse(input).map_err(|_err| ParseError) } fn parse_(input: &mut &str) -> ModalResult { trace("category", move |input: &mut &str| { let symbols = one_of(['A', 'B', 'Z', 'C', 'D', '_']); symbols .map(|c| match c { 'A' => Category::American, 'B' => Category::BritishIse, 'Z' => Category::BritishIze, 'C' => Category::Canadian, 'D' => Category::Australian, '_' => Category::Other, _ => unreachable!("parser won't select this option"), }) .parse_next(input) }) .parse_next(input) } } #[cfg(test)] mod test_category { use super::*; use snapbox::ToDebug; use snapbox::assert_data_eq; use snapbox::str; #[test] fn test_valid() { let (input, actual) = Category::parse_.parse_peek("A").unwrap(); assert_data_eq!(input, str![]); assert_data_eq!( actual.to_debug(), str![[r#" American "#]] ); } #[test] fn test_extra() { let (input, actual) = Category::parse_.parse_peek("_ foobar").unwrap(); assert_data_eq!(input, str![" foobar"]); assert_data_eq!( actual.to_debug(), str![[r#" Other "#]] ); } } impl Tag { pub fn parse(input: &str) -> Result { Self::parse_.parse(input).map_err(|_err| ParseError) } fn parse_(input: &mut &str) -> ModalResult { trace("tag", move |input: &mut &str| { let symbols = one_of(['.', 'v', 'V', '-', 'x']); symbols .map(|c| match c { '.' => Tag::Eq, 'v' => Tag::Variant, 'V' => Tag::Seldom, '-' => Tag::Possible, 'x' => Tag::Improper, _ => unreachable!("parser won't select this option"), }) .parse_next(input) }) .parse_next(input) } } #[cfg(test)] mod test_tag { use super::*; use snapbox::ToDebug; use snapbox::assert_data_eq; use snapbox::str; #[test] fn test_valid() { let (input, actual) = Tag::parse_.parse_peek(".").unwrap(); assert_data_eq!(input, str![]); assert_data_eq!( actual.to_debug(), str![[r#" Eq "#]] ); } #[test] fn test_extra() { let (input, actual) = Tag::parse_.parse_peek("x foobar").unwrap(); assert_data_eq!(input, str![" foobar"]); assert_data_eq!( actual.to_debug(), str![[r#" Improper "#]] ); } } impl Pos { pub fn parse(input: &str) -> Result { Self::parse_.parse(input).map_err(|_err| ParseError) } fn parse_(input: &mut &str) -> ModalResult { trace("pos", move |input: &mut &str| { alt(( "N".value(Pos::Noun), "V".value(Pos::Verb), "Adj".value(Pos::Adjective), "Adv".value(Pos::Adverb), "A".value(Pos::AdjectiveOrAdverb), "Inj".value(Pos::Interjection), "Prep".value(Pos::Preposition), )) .parse_next(input) }) .parse_next(input) } } #[cfg(test)] mod test_pos { use super::*; use snapbox::ToDebug; use snapbox::assert_data_eq; use snapbox::str; #[test] fn test_valid() { let (input, actual) = Pos::parse_.parse_peek("N>").unwrap(); assert_data_eq!(input, str![">"]); assert_data_eq!( actual.to_debug(), str![[r#" Noun "#]] ); } #[test] fn test_extra() { let (input, actual) = Pos::parse_.parse_peek("Adj> foobar").unwrap(); assert_data_eq!(input, str!["> foobar"]); assert_data_eq!( actual.to_debug(), str![[r#" Adjective "#]] ); } } #[derive(Debug)] pub struct ParseError; impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "invalid") } } impl std::error::Error for ParseError {}