Text

Unicode

Unicode text made comparable and measured: NFC and NFKC forms, accents removed, characters counted as people see them, safe truncation and terminal width.

Version 1.0.0 · 6 functions · 3 crates

#unicode#text#forms#search

Functions

What use unicode.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use unicode_normalize, unicode_strip_accents from unicode (Quillo 0.6).

unicode_normalize(text: str, form: str) → stror an error

text in a normal form: "nfc" (composed: what to store and compare; "é" as one character), "nfd" (decomposed: "e" and an accent), "nfkc" or "nfkd" (also folding look-alikes: "fi" is "fi", "①" is "1", full-width "A" is "A", for usernames and search).

unicode_strip_accents(text: str) → stror an error

text without accents and other marks: "Crème Brûlée" is "Creme Brulee", "Ångström" is "Angstrom". Letters that are not a base letter with marks ("ø", "ß", "æ") stay as they are.

unicode_length(text: str) → intor an error

How many characters text has as a person counts them (grapheme clusters): "👍🏽" and "🇫🇷" are 1, "é" is 1 however it is stored. Quillo's len counts code points, which differ for these.

unicode_truncate(text: str, n: int) → stror an error

The first n characters of text as a person counts them, never cutting an emoji or an accent in two: for previews and limits like "30 characters".

unicode_width(text: str) → intor an error

How many columns text takes in a terminal or a monospace font: Chinese, Japanese and most emoji take 2, accents 0. For tables in plain-text emails and CLIs.

unicode_same(a: str, b: str) → boolor an error

Whether two texts are the same once both are in NFKC form and lower case: "Café" and "café" are.

Get it

$ quillo install unicode               # unicode.qwer into this folder$ quillo install unicode --example     # and its example, unicode.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/unicode.qwer$ curl -O https://quillolang.org/plugins/files/unicode.qwe

Then, in your program, all of it or only what you call:

main.qwe
use unicode.qweruse unicode_normalize, unicode_strip_accents from unicode      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
unicode-normalization = "0.1"unicode-segmentation = "1.12"unicode-width = "0.2"

Example

unicode.qwe
// Usernames compared as people read them, and a 10-character preview that never breaks an emoji.////   quillo run unicode.qwe      then open http://localhost:8080/textuse "unicode.qwer"let thumbs = "Great job 👍🏽🇫🇷!"let decomposed = "Cafe\u{301}"route GET /text = {    code_points: len(thumbs),    seen: unicode_length(thumbs)?,    preview: unicode_truncate(thumbs, 11)?,    plain: unicode_strip_accents("Crème Brûlée, Ångström, São Paulo")?,    same_user: unicode_same("Café", "café")?,    stored: len(unicode_normalize(decomposed, "nfc")?),    width: unicode_width("日本語 ok")?,    folded: unicode_normalize("file ①", "nfkc")?,}
$ quillo run unicode.qwe$ curl 'localhost:8080/text'

Source

unicode.qwer
//! Unicode text made comparable and measured: NFC/NFD/NFKC/NFKD forms, accents removed, characters counted as people see them (👍🏽 is one), and width in a terminal.//! version: 1.0.0//!//! [dependencies]//! unicode-normalization = "0.1"//! unicode-segmentation = "1.12"//! unicode-width = "0.2"use unicode_normalization::char::is_combining_mark;use unicode_normalization::UnicodeNormalization;use unicode_segmentation::UnicodeSegmentation;use unicode_width::UnicodeWidthStr;const MAX: usize = 10_000_000;fn check(text: &str) -> Result<(), String> {    if text.len() > MAX { Err("text up to 10 MB".into()) } else { Ok(()) }}/// `text` in a normal form: "nfc" (composed: what to store and compare; "é" as one character), "nfd" (decomposed: "e" and an accent), "nfkc" or "nfkd" (also folding look-alikes: "fi" is "fi", "①" is "1", full-width "A" is "A", for usernames and search).pub fn unicode_normalize(text: &str, form: &str) -> Result<String, String> {    check(text)?;    Ok(match form.to_lowercase().as_str() {        "nfc" | "" => text.nfc().collect(),        "nfd" => text.nfd().collect(),        "nfkc" => text.nfkc().collect(),        "nfkd" => text.nfkd().collect(),        o => return Err(format!("form is \"nfc\", \"nfd\", \"nfkc\" or \"nfkd\", not {:?}", o)),    })}/// `text` without accents and other marks: "Crème Brûlée" is "Creme Brulee", "Ångström" is "Angstrom". Letters that are not a base letter with marks ("ø", "ß", "æ") stay as they are.pub fn unicode_strip_accents(text: &str) -> Result<String, String> {    check(text)?;    Ok(text.nfd().filter(|c| !is_combining_mark(*c)).nfc().collect())}/// How many characters `text` has as a person counts them (grapheme clusters): "👍🏽" and "🇫🇷" are 1, "é" is 1 however it is stored. Quillo's `len` counts code points, which differ for these.pub fn unicode_length(text: &str) -> Result<i64, String> {    check(text)?;    Ok(text.graphemes(true).count() as i64)}/// The first `n` characters of `text` as a person counts them, never cutting an emoji or an accent in two: for previews and limits like "30 characters".pub fn unicode_truncate(text: &str, n: i64) -> Result<String, String> {    check(text)?;    Ok(text.graphemes(true).take(n.max(0) as usize).collect())}/// How many columns `text` takes in a terminal or a monospace font: Chinese, Japanese and most emoji take 2, accents 0. For tables in plain-text emails and CLIs.pub fn unicode_width(text: &str) -> Result<i64, String> {    check(text)?;    Ok(UnicodeWidthStr::width(text) as i64)}/// Whether two texts are the same once both are in NFKC form and lower case: "Café" and "café" are.pub fn unicode_same(a: &str, b: &str) -> Result<bool, String> {    check(a)?;    check(b)?;    Ok(a.nfkc().collect::<String>().to_lowercase() == b.nfkc().collect::<String>().to_lowercase())}
Thread
58Useful? Like it, or add to its thread below.

Thread 0 posts

On Unicode: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.