Skip to content

Commit

Permalink
Merge e1b0229 into a45d25e
Browse files Browse the repository at this point in the history
  • Loading branch information
alerque authored Jul 13, 2024
2 parents a45d25e + e1b0229 commit 721ab56
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 4 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down
23 changes: 23 additions & 0 deletions spec/decasify_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
Expand Down Expand Up @@ -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)
7 changes: 5 additions & 2 deletions src/bin/decasify.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -53,7 +53,10 @@ fn process<I: IntoIterator<Item = String>>(
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}")
}
}
}
}
55 changes: 55 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<StyleGuide>) -> String {
match style {
Some(StyleGuide::AssociatedPress) => to_titlecase_ap(words),
Expand Down Expand Up @@ -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<String> = 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<String> = 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::*;
Expand Down Expand Up @@ -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"
);
}
15 changes: 15 additions & 0 deletions src/lua.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ fn decasify(lua: &Lua) -> LuaResult<LuaTable> {
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();
Expand Down Expand Up @@ -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<LuaString<'a>> {
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)
}
8 changes: 8 additions & 0 deletions src/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand All @@ -33,3 +34,10 @@ fn py_lowercase(input: String, locale: InputLocale) -> PyResult<String> {
fn py_uppercase(input: String, locale: InputLocale) -> PyResult<String> {
Ok(to_uppercase(&input, locale))
}

#[pyfunction]
#[pyo3(name = "sentencecase")]
#[pyo3(signature = (input, locale))]
fn py_sentencecase(input: String, locale: InputLocale) -> PyResult<String> {
Ok(to_sentencecase(&input, locale))
}
15 changes: 14 additions & 1 deletion tests/test_all.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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

0 comments on commit 721ab56

Please sign in to comment.