Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ licensedir = $(datarootdir)/licenses/$(TRANSFORMED_PACKAGE_NAME)
bin_PROGRAMS = decasify
decasify_SOURCES = src/bin/decasify.rs src/content.rs src/cli.rs src/lib.rs src/types.rs src/traits.rs
decasify_SOURCES += src/lua.rs src/python.rs src/wasm.rs
decasify_SOURCES += src/en.rs src/tr.rs
decasify_SOURCES += src/en.rs src/es.rs src/tr.rs
EXTRA_decasify_SOURCES = tests/cli.rs tests/lib.rs
EXTRA_DIST = pyproject.toml spec/decasify_spec.lua tests/test_all.py plugin/decasify.lua sile/packages/decasify.lua
dist_doc_DATA = README.md CHANGELOG.md
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ The CLI defaults to titlecase and English, but lower, upper, and sentence case o
The Rust, Lua, Python, and JavaScript library APIs have functions specific to each operation.
Where possible the APIs currently default to English rules and (for English) the Gruber style guide, but others are available.

The Spanish style roughly follows [optional stylistic exceptions](https://www.rae.es/dpd/may%C3%BAsculas) noted by Real Academia Española.
Keep in mind most Spanish style guides eschew title casing and use sentence-case for many things that would traditionally be title-cased in English.
This library implements a best-guess at title-casing when asked to, it does not help you understand when (not) to use it in the first place.

The Turkish style follows the Turkish Language Institute's [guidelines][tdk].

For English, three style guides are known: Associated Press (AP), Chicago Manual of Style (CMOS), and John Gruber's Daring Fireball (Gruber).
Expand Down
4 changes: 2 additions & 2 deletions plugin/decasify.lua
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ end, {
if arg_index == 1 then
return filter({ "lower", "sentence", "title", "upper" })
elseif arg_index == 2 then
return filter({ "en", "tr" })
return filter({ "en", "es", "tr" })
elseif arg_index == 3 then
return filter({ "ap", "cmos", "default", "grubber", "tdk" })
return filter({ "ap", "cmos", "default", "fundeu", "grubber", "rae", "tdk" })
else
return {}
end
Expand Down
4 changes: 2 additions & 2 deletions plugin/decasify.vim
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ function! s:DecasifyComplete(arg_lead, cmd_line, _) abort
if l:arg_index == 1
let l:candidates = ['lower', 'sentence', 'title', 'upper']
elseif l:arg_index == 2
let l:candidates = ['en', 'tr']
let l:candidates = ['en', 'es', 'tr']
elseif l:arg_index == 3
let l:candidates = ['ap', 'cmos', 'default', 'grubber', 'tdk']
let l:candidates = ['ap', 'cmos', 'default', 'fundeu', 'grubber', 'rae', 'tdk']
else
let l:candidates = []
endif
Expand Down
97 changes: 97 additions & 0 deletions src/es.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// SPDX-FileCopyrightText: © 2023 Caleb Maclennan <caleb@alerque.com>
// SPDX-License-Identifier: LGPL-3.0-only

use crate::content::{Chunk, Segment};
use crate::get_override;
use crate::types::{StyleGuide, StyleOptions, Word};

use unicode_titlecase::StrTitleCase;

pub fn titlecase(chunk: Chunk, style: StyleGuide, opts: StyleOptions) -> String {
let articles_prepositions_conjunctions = [
"a", "al", "ante", "bajo", "con", "contra", "de", "del", "desde", "durante", "e", "el",
"en", "entre", "hacia", "hasta", "la", "las", "los", "mas", "mediante", "ni", "o", "para",
"pero", "por", "que", "según", "si", "sin", "so", "sino", "sobre", "tras", "u", "un",
"una", "unas", "unos", "y",
];
let determiners = [
"mi", "mis", "nuestro", "nuestra", "nuestros", "nuestras", "tu", "tus", "vuestro",
"vuestra", "vuestros", "vuestras", "su", "sus",
];
match style {
StyleGuide::LanguageDefault => {
titlecase_spanish(chunk, opts, &articles_prepositions_conjunctions)
}
StyleGuide::RealAcademiaEspanola => {
titlecase_spanish(chunk, opts, &articles_prepositions_conjunctions)
}
StyleGuide::FundeuRealAcademiaEspanola => {
let mut combined = articles_prepositions_conjunctions.to_vec();
combined.extend_from_slice(&determiners);
titlecase_spanish(chunk, opts, &combined)
}
_ => todo!("Spanish implementation doesn't support this style guide."),
}
}

fn titlecase_spanish(chunk: Chunk, opts: StyleOptions, reserved_words: &[&str]) -> String {
let mut chunk = chunk.clone();
let mut done_first = false;
chunk.segments.iter_mut().for_each(|segment| {
if let Segment::Word(word) = segment {
word.word =
if let Some(word) = get_override(word, &opts.overrides, |w| w.to_lowercase()) {
word.to_string()
} else if !done_first {
done_first = true;
word.to_titlecase_lower_rest()
} else {
match is_reserved(word, reserved_words) {
true => word.word.to_lowercase(),
false => word.word.to_titlecase_lower_rest(),
}
}
}
});
chunk.into()
}

fn is_reserved(word: &Word, reserved_words: &[&str]) -> bool {
reserved_words.contains(&word.word.to_lowercase().as_str())
}

pub fn lowercase(chunk: Chunk) -> String {
let mut chunk = chunk.clone();
chunk.segments.iter_mut().for_each(|segment| {
if let Segment::Word(word) = segment {
word.word = word.to_lowercase()
}
});
chunk.into()
}

pub fn uppercase(chunk: Chunk) -> String {
let mut chunk = chunk.clone();
chunk.segments.iter_mut().for_each(|segment| {
if let Segment::Word(word) = segment {
word.word = word.to_uppercase()
}
});
chunk.into()
}

pub fn sentencecase(chunk: Chunk) -> String {
let mut chunk = chunk.clone();
let mut done_first = false;
chunk.segments.iter_mut().for_each(|segment| {
if let Segment::Word(word) = segment {
word.word = if !done_first {
done_first = true;
word.to_titlecase_lower_rest()
} else {
word.to_lowercase()
}
}
});
chunk.into()
}
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub mod python;
pub mod wasm;

mod en;
mod es;
mod tr;

/// Convert a string to a specific case following typesetting conventions for a target locale
Expand Down Expand Up @@ -85,6 +86,7 @@ where
let opts: StyleOptions = opts.try_into()?;
Ok(match locale {
Locale::EN => en::titlecase(chunk, style, opts),
Locale::ES => es::titlecase(chunk, style, opts),
Locale::TR => tr::titlecase(chunk, style, opts),
})
}
Expand All @@ -99,6 +101,7 @@ where
let locale: Locale = locale.try_into()?;
Ok(match locale {
Locale::EN => en::lowercase(chunk),
Locale::ES => es::lowercase(chunk),
Locale::TR => tr::lowercase(chunk),
})
}
Expand All @@ -113,6 +116,7 @@ where
let locale: Locale = locale.try_into()?;
Ok(match locale {
Locale::EN => en::uppercase(chunk),
Locale::ES => es::uppercase(chunk),
Locale::TR => tr::uppercase(chunk),
})
}
Expand All @@ -127,6 +131,7 @@ where
let locale: Locale = locale.try_into()?;
Ok(match locale {
Locale::EN => en::sentencecase(chunk),
Locale::ES => es::sentencecase(chunk),
Locale::TR => tr::sentencecase(chunk),
})
}
Expand Down
14 changes: 13 additions & 1 deletion src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ pub struct Word {
pub word: String,
}

// WARNING: These enums can't change order when adding new variants because some modules cast them
// to integers, and that would make for ABI breakage. The variants can be re-sorted (alphabetically
// or logically or whatever) when a major version with no ABI compatibility guarantees is okay.

/// Locale selector to change language support rules of case functions.
#[derive(Default, Display, VariantNames, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "pythonmodule", pyclass(eq))]
Expand All @@ -63,6 +67,7 @@ pub enum Locale {
#[default]
EN,
TR,
ES,
}

/// Target case selector.
Expand Down Expand Up @@ -98,6 +103,10 @@ pub enum StyleGuide {
LanguageDefault,
#[strum(serialize = "tdk")]
TurkishLanguageInstitute,
#[strum(serialize = "rae")]
RealAcademiaEspanola,
#[strum(serialize = "fundeu")]
FundeuRealAcademiaEspanola,
}

#[derive(Clone, Debug, Default, PartialEq)]
Expand Down Expand Up @@ -180,7 +189,8 @@ impl FromStr for Locale {
fn from_str(s: &str) -> Result<Self> {
match s.to_ascii_lowercase().as_str() {
"en" | "english" | "en_en" => Ok(Locale::EN),
"tr" | "turkish" | "tr_tr" | "türkçe" => Ok(Locale::TR),
"es" | "spanish" | "es_es" | "espanol" | "español" => Ok(Locale::ES),
"tr" | "turkish" | "tr_tr" | "turkce" | "türkçe" => Ok(Locale::TR),
input => LocaleSnafu { input }.fail()?,
}
}
Expand Down Expand Up @@ -266,6 +276,8 @@ impl FromStr for StyleGuide {
"daringfireball" | "gruber" | "fireball" => Ok(StyleGuide::DaringFireball),
"associatedpress" | "ap" => Ok(StyleGuide::AssociatedPress),
"chicagoManualofstyle" | "chicago" | "cmos" => Ok(StyleGuide::ChicagoManualOfStyle),
"fundeu" | "fundeurealacademiaespanola" => Ok(StyleGuide::FundeuRealAcademiaEspanola),
"rae" | "realacademiaespanola" => Ok(StyleGuide::RealAcademiaEspanola),
"tdk" | "turkishlanguageinstitute" => Ok(StyleGuide::TurkishLanguageInstitute),
"default" | "languagedefault" | "language" | "none" | "" => {
Ok(StyleGuide::LanguageDefault)
Expand Down
61 changes: 61 additions & 0 deletions tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ case!(
" Foo Bar "
);

case!(
trivia_es,
Case::Title,
Locale::ES,
StyleGuide::LanguageDefault,
StyleOptions::default(),
" foo bar ",
" Foo Bar "
);

case!(
trivia_tr,
Case::Title,
Expand Down Expand Up @@ -199,6 +209,51 @@ titlecase!(
" Free Trolling\n Space "
);

titlecase!(
rae_articles,
Locale::ES,
StyleGuide::LanguageDefault,
StyleOptions::default(),
"el libro del autor",
"El Libro del Autor"
);

titlecase!(
rae_holiday,
Locale::ES,
StyleGuide::RealAcademiaEspanola,
StyleOptions::default(),
"DÍA DE los muertos",
"Día de los Muertos"
);

titlecase!(
rae_magazine,
Locale::ES,
StyleGuide::RealAcademiaEspanola,
StyleOptions::default(),
"cien años DE soledad",
"Cien Años de Soledad"
);

titlecase!(
rae_prepositions,
Locale::ES,
StyleGuide::LanguageDefault,
StyleOptions::default(),
"en la casa de mi madre",
"En la Casa de Mi Madre"
);

titlecase!(
fundu_prepositions,
Locale::ES,
StyleGuide::FundeuRealAcademiaEspanola,
StyleOptions::default(),
"en la casa de mi madre",
"En la Casa de mi Madre"
);

titlecase!(
turkish_question,
Locale::TR,
Expand Down Expand Up @@ -256,6 +311,8 @@ macro_rules! lowercase {

lowercase!(lower_en, Locale::EN, "foo BAR BaZ BIKE", "foo bar baz bike");

lowercase!(lower_es, Locale::ES, "Hola MUNDO", "hola mundo");

lowercase!(
lower_tr,
Locale::TR,
Expand All @@ -275,6 +332,8 @@ macro_rules! uppercase {

uppercase!(upper_en, Locale::EN, "foo BAR BaZ bike", "FOO BAR BAZ BIKE");

uppercase!(upper_es, Locale::ES, "hola MUNDo", "HOLA MUNDO");

uppercase!(
upper_tr,
Locale::TR,
Expand All @@ -299,4 +358,6 @@ sentencecase!(
"Insert bike here"
);

sentencecase!(sentence_es, Locale::ES, "hola MUNDO", "Hola mundo");

sentencecase!(sentence_tr, Locale::TR, "ilk DAVRANSIN", "İlk davransın");
Loading