titlecase-2.2.1/.cargo/config000064400000000000000000000002050072674642500141710ustar 00000000000000[target.x86_64-unknown-freebsd] linker = "x86_64-pc-freebsd12-gcc" [target.x86_64-pc-windows-gnu] linker = "x86_64-w64-mingw32-gcc" titlecase-2.2.1/.cargo_vcs_info.json0000644000000001360000000000100130130ustar { "git": { "sha1": "b6674fe4f4b7e7f4be1ba964ebf6da595af66157" }, "path_in_vcs": "" }titlecase-2.2.1/.cirrus.yml000064400000000000000000000011730072674642500137450ustar 00000000000000task: name: Build (Alpine Linux) container: image: alpine:3.15 cpu: 8 matrix: - environment: RUST_VERSION: "1.40.0" - environment: RUST_VERSION: "stable" environment: RUSTFLAGS: "-C target-feature=-crt-static" PATH: "$HOME/.cargo/bin:$PATH" cargo_cache: folder: $HOME/.cargo/registry fingerprint_script: cat Cargo.toml install_script: - apk --update add curl git gcc musl-dev - curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal --default-toolchain ${RUST_VERSION} test_script: - cargo test before_cache_script: rm -rf $HOME/.cargo/registry/index titlecase-2.2.1/.gitignore000064400000000000000000000000230072674642500136160ustar 00000000000000target/ **/*.rs.bk titlecase-2.2.1/Cargo.lock0000644000000026520000000000100107730ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] name = "aho-corasick" version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] [[package]] name = "joinery" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72167d68f5fce3b8655487b8038691a3c9984ee769590f93f2a631f4ad64e4f5" [[package]] name = "lazy_static" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "regex" version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" [[package]] name = "titlecase" version = "2.2.1" dependencies = [ "joinery", "lazy_static", "regex", ] titlecase-2.2.1/Cargo.toml0000644000000021270000000000100110130ustar # 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 = "2018" name = "titlecase" version = "2.2.1" authors = ["Wesley Moore "] description = "A tool and library that capitalizes text according to a style defined by John Gruber for post titles on his website Daring Fireball." documentation = "https://docs.rs/titlecase" readme = "README.md" keywords = [ "title", "case", "capitalization", "capitalisation", "capitalize", ] categories = ["text-processing"] license = "MIT" repository = "https://github.com/wezm/titlecase" [dependencies.joinery] version = "< 3.0" [dependencies.lazy_static] version = "1.0" [dependencies.regex] version = "1.5" titlecase-2.2.1/Cargo.toml.orig000064400000000000000000000011660072674642500145260ustar 00000000000000[package] name = "titlecase" description = "A tool and library that capitalizes text according to a style defined by John Gruber for post titles on his website Daring Fireball." version = "2.2.1" edition = "2018" authors = ["Wesley Moore "] documentation = "https://docs.rs/titlecase" repository = "https://github.com/wezm/titlecase" readme = "README.md" license = "MIT" keywords = ["title", "case", "capitalization", "capitalisation", "capitalize"] categories = ["text-processing"] [dependencies] lazy_static = "1.0" regex = "1.5" joinery = "< 3.0" # 3.0+ requires 2021 edition, which our MSRV does not support titlecase-2.2.1/Changelog.md000064400000000000000000000016720072674642500140520ustar 00000000000000Changelog ========= ## [2.2.0](https://github.com/wezm/titlecase/releases/tag/v2.2.0) - Further reduce allocations and optimise regex use ## [2.1.0](https://github.com/wezm/titlecase/releases/tag/v2.1.0) - Lowercase small words that are uppercase #7 - Clean up and reduce intermediate allocations #8 ## [2.0.0](https://github.com/wezm/titlecase/releases/tag/v2.0.0) - Update dependencies - Minimum Supported Rust Version is now 1.40.0 ## [1.1.0](https://github.com/wezm/titlecase/releases/tag/v1.1.0) - Update dependencies - Add help and version flags to CLI ## [0.10.0](https://github.com/wezm/titlecase/releases/tag/v0.10.0) - Improve documentation - Make use of regular expressions more efficient - Errors encountered by the titlecase tool are now written to stderr ## [0.9.2](https://github.com/wezm/titlecase/releases/tag/v0.9.2) Fix typos in Cargo.toml ## [0.9.1](https://github.com/wezm/titlecase/releases/tag/0.9.1) Initial release titlecase-2.2.1/LICENSE000064400000000000000000000022750072674642500126460ustar 00000000000000This software is Copyright (c) 2015-2019 by John Gruber, Aristotle Pagaltzis, David Gouch, Wesley Moore. This is free software, licensed under: The MIT (X11) License The MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. titlecase-2.2.1/README.md000064400000000000000000000044170072674642500131200ustar 00000000000000# Title Case (titlecase) `titlecase` is a small tool and library (crate) that capitalizes English text [according to a style][style] defined by John Gruber for post titles on his website [Daring Fireball]. `titlecase` should run on all platforms supported by Rust including Linux, macOS, FreeBSD, NetBSD, OpenBSD, and Windows. [![Build Status](https://api.cirrus-ci.com/github/wezm/titlecase.svg)](https://cirrus-ci.com/github/wezm/titlecase) [![crates.io](https://img.shields.io/crates/v/titlecase.svg)](https://crates.io/crates/titlecase) [![Documentation](https://docs.rs/titlecase/badge.svg)][crate-docs] `titlecase` is licensed under the [MIT license][MIT]. ## Examples ``` % echo 'Being productive on linux' | titlecase Being Productive on Linux % echo 'Finding an alternative to Mac OS X — part 2' | titlecase Finding an Alternative to Mac OS X — Part 2 % echo 'an example with small words and sub-phrases: "the example"' | titlecase An Example With Small Words and Sub-Phrases: "The Example" ``` ## Command Line Usage `titlecase` reads lines of text from **stdin** and prints title cased versions to **stdout**. ## Usage as a Rust Crate See the [crate documentation][crate-docs]. ## Building **Minimum Supported Rust Version:** 1.40.0 If you have a stable Rust compiler toolchain installed you can install the most recently released `titlecase` with cargo: ``` % cargo install titlecase ``` ## Style Instead of simply capitalizing each word `titlecase` does the following ([amongst other things][style]): * Lower case small words like an, of, or in. * Don't capitalize words like iPhone. * Don't interfere with file paths, URLs, domains, and email addresses. * Always capitalize the first and last words, even if they are small words or surrounded by quotes. * Don't interfere with terms like "Q&A", or "AT&T". * Capitalize small words after a colon. ## Credits This tool makes use of prior work by [John Gruber][style], [Aristotle Pagaltzis], and [David Gouch]. [Daring Fireball]: https://daringfireball.net/ [style]: https://daringfireball.net/2008/05/title_case [Aristotle Pagaltzis]: http://plasmasturm.org/code/titlecase/ [David Gouch]: http://individed.com/code/to-title-case/ [MIT]: https://github.com/wezm/titlecase/blob/master/LICENSE [crate-docs]: https://docs.rs/titlecase titlecase-2.2.1/src/lib.rs000064400000000000000000000272420072674642500135450ustar 00000000000000//! `titlecase` capitlizes English text according to [a style][style] defined by John //! Gruber for post titles on his website [Daring Fireball]. //! //! [Daring Fireball]: https://daringfireball.net/ //! [style]: https://daringfireball.net/2008/05/title_case //! //! ## Example //! //! ``` //! use titlecase::titlecase; //! //! let text = "a sample title to capitalize: an example"; //! assert_eq!(titlecase(text), "A Sample Title to Capitalize: An Example"); //! ``` //! //! ## Style //! //! Instead of simply capitalizing each word it does the following ([amongst other //! things][style]): //! //! * Lower case small words like an, of, or in. //! * Don't capitalize words like iPhone. //! * Don't interfere with file paths, URLs, domains, and email addresses. //! * Always capitalize the first and last words, even if they are small words //! or surrounded by quotes. //! * Don't interfere with terms like "Q&A", or "AT&T". //! * Capitalize small words after a colon. #[macro_use] extern crate lazy_static; use std::borrow::Cow; use joinery::JoinableIterator; use regex::{Captures, Regex}; #[rustfmt::skip] const SMALL_WORDS: &[&str] = &[ "a", "an", "and", "as", "at", "but", "by", "en", "for", "if", "in", "of", "on", "or", "the", "to", "v[.]?", "via", "vs[.]?", ]; lazy_static! { static ref SMALL_WORDS_PIPE: String = SMALL_WORDS.join("|"); } /// Returns `input` in title case. /// /// ### Example /// /// ``` /// use titlecase::titlecase; /// /// let text = "a sample title to capitalize: an example"; /// assert_eq!(titlecase(text), "A Sample Title to Capitalize: An Example"); /// ``` pub fn titlecase(input: &str) -> String { lazy_static! { static ref WORDS: Regex = Regex::new( r"(?x) (_*) ([\w'’.:/@\[\]/()&]+) (_*)", ) .expect("unable to compile regex"); } // If input is yelling (all uppercase) make lowercase let trimmed_input = input.trim(); let trimmed_input = if trimmed_input.chars().any(|ch| ch.is_lowercase()) { Cow::from(trimmed_input) } else { Cow::from(trimmed_input.to_lowercase()) }; let result = WORDS.replace_all(&trimmed_input, |captures: &Captures| { let mut result = captures.get(1).map_or("", |cap| cap.as_str()).to_owned(); let word = &captures[2]; result.push_str(&process_word(word)); result.push_str(captures.get(3).map_or("", |cap| cap.as_str())); result }); // Now deal with small words at the start and end of the text fix_small_word_at_end(&fix_small_word_at_start(&result)).into_owned() } fn process_word(word: &str) -> Cow<'_, str> { lazy_static! { static ref SMALL_RE: Regex = Regex::new(&format!(r"\A(?:{})\z", *SMALL_WORDS_PIPE)) .expect("unable to compile small words regex"); } if is_digital_resource(word) { // pass through return Cow::from(word); } let lower_word = word.to_lowercase(); if SMALL_RE.is_match(&lower_word) { Cow::from(lower_word) } else if starts_with_bracket(word) { let rest = titlecase(&word[1..]); Cow::from(format!("({}", rest)) } else if has_internal_slashes(word) { Cow::from(word.split('/').map(titlecase).join_with('/').to_string()) } else if has_internal_caps(word) { // Preserve internal caps like iPhone or DuBois Cow::from(word) } else { Cow::from(ucfirst(word)) } } // https://stackoverflow.com/a/38406885 fn ucfirst(input: &str) -> String { let mut chars = input.chars(); match chars.next() { None => String::new(), Some(f) => f.to_uppercase().chain(chars).collect(), } } fn is_digital_resource(word: &str) -> bool { lazy_static! { static ref RE: Regex = Regex::new( r"(?x) \A (?: [/\\] [[:alpha:]]+ [-_[:alpha:]/\\]+ | # file path or [-_[:alpha:]]+ [@.:] [-_[:alpha:]@.:/]+ ) # URL, domain, or email", ) .expect("unable to compile file/url regex"); } RE.is_match(word) } // E.g. iPhone or DuBois fn has_internal_caps(word: &str) -> bool { word.chars().skip(1).any(|chr| chr.is_uppercase()) } fn has_internal_slashes(word: &str) -> bool { !word.is_empty() && word.chars().skip(1).any(|chr| chr == '/') } fn starts_with_bracket(word: &str) -> bool { word.starts_with('(') } fn fix_small_word_at_start(text: &str) -> Cow<'_, str> { lazy_static! { static ref RE: Regex = Regex::new(&format!( r#"(?x) ( \A [[:punct:]]* # start of title... | [:.;?!]\x20+ # or of subsentence... | \x20['"“‘(\[]\x20* ) # or of inserted subphrase... ( {small_re} ) \b # ... followed by small word "#, small_re = *SMALL_WORDS_PIPE )) .expect("unable to compile fix_small_word_at_start regex"); } RE.replace_all(text, |captures: &Captures| { let mut result = captures[1].to_owned(); result.push_str(&ucfirst(&captures[2])); result }) } fn fix_small_word_at_end(text: &str) -> Cow<'_, str> { lazy_static! { static ref RE: Regex = Regex::new(&format!( r#"(?x) \b ( {small_re} ) # small word... ( [[:punct:]]* \z # ... at the end of the title... | ['"’”)\]] \x20 ) # ... or of an inserted subphrase? "#, small_re = *SMALL_WORDS_PIPE )) .expect("unable to compile fix_small_word_at_end regex"); } RE.replace_all(text, |captures: &Captures| { let mut result = ucfirst(&captures[1]); result.push_str(&captures[2]); result }) } #[cfg(test)] mod tests { use super::titlecase; macro_rules! testcase { ($name:ident, $input:expr, $expected:expr) => { #[test] fn $name() { assert_eq!(titlecase($input), $expected); } }; } testcase!( email, "For step-by-step directions email someone@gmail.com", "For Step-by-Step Directions Email someone@gmail.com" ); testcase!( subphrase_in_single_quotes, "2lmc Spool: 'Gruber on OmniFocus and Vapo(u)rware'", "2lmc Spool: 'Gruber on OmniFocus and Vapo(u)rware'" ); testcase!( subphrase_in_double_quotes, r#"2lmc Spool: "Gruber on OmniFocus and Vapo(u)rware""#, r#"2lmc Spool: "Gruber on OmniFocus and Vapo(u)rware""# ); testcase!( curly_double_quotes, "Have you read “the lottery”?", "Have You Read “The Lottery”?" ); testcase!( brackets, "your hair[cut] looks (nice)", "Your Hair[cut] Looks (Nice)" ); testcase!( multiple_brackets, "your hair[cut] looks ((Very Nice))", "Your Hair[cut] Looks ((Very Nice))" ); testcase!( url, "People probably won't put http://foo.com/bar/ in titles", "People Probably Won't Put http://foo.com/bar/ in Titles" ); testcase!( name_url, "Scott Moritz and TheStreet.com’s million iPhone la‑la land", "Scott Moritz and TheStreet.com’s Million iPhone La‑La Land" ); testcase!(iphone, "BlackBerry vs. iPhone", "BlackBerry vs. iPhone"); testcase!( curly_single_quotes, "Notes and observations regarding Apple’s announcements from ‘The Beat Goes On’ special event", "Notes and Observations Regarding Apple’s Announcements From ‘The Beat Goes On’ Special Event" ); testcase!( markdown, "Read markdown_rules.txt to find out how _underscores around words_ will be interpreted", "Read markdown_rules.txt to Find Out How _Underscores Around Words_ Will Be Interpreted" ); testcase!( q_and_a, "Q&A with Steve Jobs: 'That's what happens in technology'", "Q&A With Steve Jobs: 'That's What Happens in Technology'" ); testcase!( at_and_t, "What is AT&T's problem?", "What Is AT&T's Problem?" ); testcase!( at_and_t2, "Apple deal with AT&T falls through", "Apple Deal With AT&T Falls Through" ); testcase!(thisvthat, "this v that", "This v That"); testcase!(thisvthat2, "this vs that", "This vs That"); testcase!(thisvthat3, "this v. that", "This v. That"); testcase!(thisvthat4, "this vs. that", "This vs. That"); testcase!( sec, "The SEC's Apple probe: what you need to know", "The SEC's Apple Probe: What You Need to Know" ); testcase!( small_word_at_start_in_quotes, "'by the way, small word at the start but within quotes.'", "'By the Way, Small Word at the Start but Within Quotes.'" ); testcase!( small_word_at_end, "Small word at end is nothing to be afraid of", "Small Word at End Is Nothing to Be Afraid Of" ); testcase!( subphrase_starting_with_small_word, "Starting sub-phrase with a small word: a trick, perhaps?", "Starting Sub-Phrase With a Small Word: A Trick, Perhaps?" ); testcase!( subphrase_with_small_word_in_single_quotes, "Sub-phrase with a small word in quotes: 'a trick, perhaps?'", "Sub-Phrase With a Small Word in Quotes: 'A Trick, Perhaps?'" ); testcase!( a_subphrase_with_small_word_in_single_quotes, "a Sub-phrase with a small word in quotes: 'a trick, perhaps?'", "A Sub-Phrase With a Small Word in Quotes: 'A Trick, Perhaps?'" ); testcase!( subphrase_with_small_word_in_double_quotes, "Sub-phrase with a small word in quotes: \"a trick, perhaps?\"", "Sub-Phrase With a Small Word in Quotes: \"A Trick, Perhaps?\"" ); testcase!( all_in_double_quotes, "\"Nothing to Be Afraid of?\"", "\"Nothing to Be Afraid Of?\"" ); testcase!(a_thing, "a thing", "A Thing"); testcase!( dr_strangelove, "Dr. Strangelove (or: how I Learned to Stop Worrying and Love the Bomb)", "Dr. Strangelove (Or: How I Learned to Stop Worrying and Love the Bomb)" ); testcase!(trimming, " this is trimming", "This Is Trimming"); testcase!(trimming2, "this is trimming ", "This Is Trimming"); testcase!(trimming3, " this is trimming ", "This Is Trimming"); testcase!( yelling, "IF IT’S ALL CAPS, FIX IT", "If It’s All Caps, Fix It" ); testcase!( slashes, "What could/should be done about slashes?", "What Could/Should Be Done About Slashes?" ); testcase!( paths, "Never touch paths like /var/run before/after /boot", "Never Touch Paths Like /var/run Before/After /boot" ); // TODO: Implement these // testcase!( // in_flight, // "The in-flight entertainment was excellent", // "The In-Flight Entertainment Was Excellent" // ); // testcase!( // stand_in, // "The Stand-in teacher gave us homework", // "The Stand-In Teacher Gave Us Homework" // ); testcase!( man_in_the_middle, "They executed a man-in-the-middle attack", "They Executed a Man-in-the-Middle Attack" ); testcase!( man_in_the_machine, "Jonathan Kim on Alex Gibney’s ‘Steve Jobs: The man in the machine’", "Jonathan Kim on Alex Gibney’s ‘Steve Jobs: The Man in the Machine’" ); testcase!( lower_small_words, "Way Of The Dragon makes Of In An A lowercase", "Way of the Dragon Makes of in an a Lowercase" ); testcase!(small_greek_letters, "μ", "Μ"); } titlecase-2.2.1/src/main.rs000064400000000000000000000016470072674642500137240ustar 00000000000000use std::env; use std::io::{self, BufRead}; use titlecase::titlecase; fn main() { match env::args().nth(1).as_deref() { Some("-h") | Some("--help") => return help(), Some("-v") | Some("--version") => return version(), Some(option) => return eprintln!("unknown option {}", option), _ => (), } let stdin = io::stdin(); for line in stdin.lock().lines() { match line { Ok(line) => println!("{}", titlecase(&line)), Err(error) => { eprintln!("{}", error); } } } } fn help() { println!( "\ Usage: titlecase [OPTIONS] titlecase reads lines from stdin and applies title casing rules to each line, outputting the result on stdout. Optional arguments: -h, --help print help message -v, --version print the version" ); } fn version() { println!("titlecase {}", env!("CARGO_PKG_VERSION")); }