diff --git a/README.md b/README.md index 0539537..d8e5812 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ A CLI utility, Rust crate, Lua Rock, and Python module to cast strings to title- This project was born out of frustration with ALL CAPS TITLES in Markdown that no tooling seemed to properly support casting to title-cased strings, particularly coming from Turkish. Many tools can handle casing single words, and some others can handle English strings, but nothing seemed to be out there for full Turkish strings. -The CLI defaults to titlecase and English, but lower and upper case options are also available. +The CLI defaults to titlecase and English, but lower, upper, and scentence case options are also available. The crate library, Lua Rock and Python Module APIs have functions specific to each operation. Where possible the APIs currently default to English rules and (for English) the Gruber style rules, but others are available. The Turkish rules follow Turkish Language Institute's [guidelines][tdk]. diff --git a/spec/decasify_spec.lua b/spec/decasify_spec.lua index 53b17b5..0422a60 100644 --- a/spec/decasify_spec.lua +++ b/spec/decasify_spec.lua @@ -7,11 +7,13 @@ describe("decasify", function () local titlecase = decasify.titlecase local lowercase = decasify.lowercase local uppercase = decasify.uppercase + local sentencecase = decasify.sentencecase it("should provide the casing functions", function () assert.is_function(titlecase) assert.is_function(lowercase) assert.is_function(uppercase) + assert.is_function(sentencecase) end) describe("titlecase", function () @@ -87,4 +89,25 @@ describe("decasify", function () assert.equals("İLKİ ILIK ÖĞLEN", result) end) end) + + describe("sentencecase", function () + it("should not balk at nil values for optional args", function () + assert.no.error(function () + sentencecase("foo", "en") + end) + assert.no.error(function () + sentencecase("foo") + end) + end) + + it("should default to handling string as English", function () + local result = sentencecase("insert BIKE here") + assert.equals("Insert bike here", result) + end) + + it("should be at peace with Turkish characters", function () + local result = sentencecase("ilk DAVRANSIN", "tr") + assert.equals("İlk davransın", result) + end) + end) end) diff --git a/src/bin/decasify.rs b/src/bin/decasify.rs index 7eab33f..26257d0 100644 --- a/src/bin/decasify.rs +++ b/src/bin/decasify.rs @@ -1,5 +1,5 @@ use decasify::cli::Cli; -use decasify::{to_lowercase, to_titlecase, to_uppercase}; +use decasify::{to_lowercase, to_sentencecase, to_titlecase, to_uppercase}; use decasify::{InputLocale, Result, StyleGuide, TargetCase}; use clap::CommandFactory; @@ -53,7 +53,10 @@ fn process>( let output = to_uppercase(&string, locale.clone()); println!("{output}") } - _ => eprintln!("Target case {case:?} not implemented!"), + TargetCase::Sentence => { + let output = to_sentencecase(&string, locale.clone()); + println!("{output}") + } } } } diff --git a/src/lib.rs b/src/lib.rs index c86980d..0b297f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,15 @@ pub fn to_uppercase(string: &str, locale: InputLocale) -> String { } } +/// Convert a string to sentence case following typesetting conventions for a target locale +pub fn to_sentencecase(string: &str, locale: InputLocale) -> String { + let words: Vec<&str> = string.split_whitespace().collect(); + match locale { + InputLocale::EN => to_sentencecase_en(words), + InputLocale::TR => to_sentencecase_tr(words), + } +} + fn to_titlecase_en(words: Vec<&str>, style: Option) -> String { match style { Some(StyleGuide::AssociatedPress) => to_titlecase_ap(words), @@ -156,6 +165,28 @@ fn to_uppercase_tr(words: Vec<&str>) -> String { output.join(" ") } +fn to_sentencecase_en(words: Vec<&str>) -> String { + let mut words = words.iter().peekable(); + let mut output: Vec = Vec::new(); + let first = words.next().unwrap(); + output.push(gruber_titlecase(first)); + for word in words { + output.push(word.to_lowercase()); + } + output.join(" ") +} + +fn to_sentencecase_tr(words: Vec<&str>) -> String { + let mut words = words.iter().peekable(); + let mut output: Vec = Vec::new(); + let first = words.next().unwrap(); + output.push(first.to_titlecase_tr_or_az()); + for word in words { + output.push(word.to_lowercase_tr_az()); + } + output.join(" ") +} + #[cfg(test)] mod tests { use super::*; @@ -299,4 +330,28 @@ mod tests { "foo BAR BaZ ILIK İLE", "FOO BAR BAZ ILIK İLE" ); + + macro_rules! sentencecase { + ($name:ident, $locale:expr, $input:expr, $expected:expr) => { + #[test] + fn $name() { + let actual = to_sentencecase($input, $locale); + assert_eq!(actual, $expected); + } + }; + } + + sentencecase!( + sentence_en, + InputLocale::EN, + "insert BIKE here", + "Insert bike here" + ); + + sentencecase!( + sentence_tr, + InputLocale::TR, + "ilk DAVRANSIN", + "İlk davransın" + ); } diff --git a/src/lua.rs b/src/lua.rs index 2830a4d..da54b08 100644 --- a/src/lua.rs +++ b/src/lua.rs @@ -12,6 +12,8 @@ fn decasify(lua: &Lua) -> LuaResult { exports.set("lowercase", lowercase).unwrap(); let uppercase = lua.create_function(uppercase)?; exports.set("uppercase", uppercase).unwrap(); + let sentencecase = lua.create_function(sentencecase)?; + exports.set("sentencecase", sentencecase).unwrap(); let version = option_env!("VERGEN_GIT_DESCRIBE").unwrap_or_else(|| env!("CARGO_PKG_VERSION")); let version = lua.create_string(version)?; exports.set("version", version).unwrap(); @@ -64,3 +66,16 @@ fn uppercase<'a>( let output = to_uppercase(&input, locale); lua.create_string(output) } + +fn sentencecase<'a>( + lua: &'a Lua, + (input, locale): (LuaString<'a>, LuaValue<'a>), +) -> LuaResult> { + let input = input.to_string_lossy(); + let locale: InputLocale = match locale { + LuaValue::String(s) => s.to_string_lossy().parse().unwrap_or(InputLocale::EN), + _ => InputLocale::EN, + }; + let output = to_sentencecase(&input, locale); + lua.create_string(output) +} diff --git a/src/python.rs b/src/python.rs index 37f1bf8..271210a 100644 --- a/src/python.rs +++ b/src/python.rs @@ -10,6 +10,7 @@ fn decasify(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_function(wrap_pyfunction!(py_titlecase, module)?)?; module.add_function(wrap_pyfunction!(py_lowercase, module)?)?; module.add_function(wrap_pyfunction!(py_uppercase, module)?)?; + module.add_function(wrap_pyfunction!(py_sentencecase, module)?)?; Ok(()) } @@ -33,3 +34,10 @@ fn py_lowercase(input: String, locale: InputLocale) -> PyResult { fn py_uppercase(input: String, locale: InputLocale) -> PyResult { Ok(to_uppercase(&input, locale)) } + +#[pyfunction] +#[pyo3(name = "sentencecase")] +#[pyo3(signature = (input, locale))] +fn py_sentencecase(input: String, locale: InputLocale) -> PyResult { + Ok(to_sentencecase(&input, locale)) +} diff --git a/tests/test_all.py b/tests/test_all.py index f4b5395..057de3c 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -1,10 +1,11 @@ -from decasify import titlecase, lowercase, uppercase, InputLocale, StyleGuide +from decasify import titlecase, lowercase, uppercase, sentencecase, InputLocale, StyleGuide def test_isfuction(): assert callable(titlecase) assert callable(lowercase) assert callable(uppercase) + assert callable(sentencecase) class TestTitlecase: @@ -52,3 +53,15 @@ def test_turkish_characters(self): text = "ilki ılık öğlen" outp = "İLKİ ILIK ÖĞLEN" assert uppercase(text, InputLocale.TR) == outp + + +class TestSentencecase: + def test_english_defaults(self): + text = "insert BIKE here" + outp = "Insert bike here" + assert sentencecase(text, InputLocale.EN) == outp + + def test_turkish_characters(self): + text = "ilk DAVRANSIN" + outp = "İlk davransın" + assert sentencecase(text, InputLocale.TR) == outp