Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into wasm
Browse files Browse the repository at this point in the history
  • Loading branch information
alerque committed Jul 15, 2024
2 parents 8643c96 + 50f9ae2 commit eb085f0
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 11 deletions.
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github: [alerque]
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ 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 urules, but others are available.
The Turkish rules follow Turkish Language Instutute's [guidelines][tdk].
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].

For English, three style guides are known: Associated Press (AP), Chicago Manual of Style (CMOS), and John Grubber's Daring Fireball (Gruber).
The Gruber style is by far the most complete, being implemented by the [titlecase crate][titlecase_crate].
Expand Down Expand Up @@ -127,7 +127,7 @@ dependencies = {
}
```

Then import and use the provided function:
Then import and use the provided functions:

```lua
local decasify = require("decasify")
Expand All @@ -150,7 +150,7 @@ dependencies = [
]
```

Then import and use the provided function and classes:
Then import and use the provided functions and type classes:

```python
from decasify import *
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}")
}
}
}
}
61 changes: 58 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub mod python;
#[cfg(feature = "wasm")]
pub mod wasm;

/// Convert a string to title case following typestting conventions for a target locale
/// Convert a string to title case following typesetting conventions for a target locale
pub fn to_titlecase(string: &str, locale: InputLocale, style: Option<StyleGuide>) -> String {
let words: Vec<&str> = string.split_whitespace().collect();
match locale {
Expand All @@ -30,7 +30,7 @@ pub fn to_titlecase(string: &str, locale: InputLocale, style: Option<StyleGuide>
}
}

/// Convert a string to lower case following typestting conventions for a target locale
/// Convert a string to lower case following typesetting conventions for a target locale
pub fn to_lowercase(string: &str, locale: InputLocale) -> String {
let words: Vec<&str> = string.split_whitespace().collect();
match locale {
Expand All @@ -39,7 +39,7 @@ pub fn to_lowercase(string: &str, locale: InputLocale) -> String {
}
}

/// Convert a string to upper case following typestting conventions for a target locale
/// Convert a string to upper case following typesetting conventions for a target locale
pub fn to_uppercase(string: &str, locale: InputLocale) -> String {
let words: Vec<&str> = string.split_whitespace().collect();
match locale {
Expand All @@ -48,6 +48,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 @@ -159,6 +168,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 @@ -302,4 +333,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))
}
3 changes: 3 additions & 0 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ impl error::Error for DecasifyError {}
#[derive(Default, Display, VariantNames, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "pythonmodule", pyclass(eq, eq_int))]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[strum(serialize_all = "lowercase")]
pub enum InputLocale {
#[default]
EN,
Expand All @@ -32,6 +33,7 @@ pub enum InputLocale {

/// Target case selector.
#[derive(Default, Display, VariantNames, Debug, Clone, PartialEq)]
#[strum(serialize_all = "lowercase")]
pub enum TargetCase {
Lower,
Sentence,
Expand All @@ -44,6 +46,7 @@ pub enum TargetCase {
#[derive(Default, Display, VariantNames, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "pythonmodule", pyclass(eq, eq_int))]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[strum(serialize_all = "lowercase")]
pub enum StyleGuide {
#[strum(serialize = "ap")]
AssociatedPress,
Expand Down
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 eb085f0

Please sign in to comment.