//! **Presentación emoji** — qué se pinta a color y qué se pinta como símbolo. //! //! Existe por un número medido (§8): antes de esto, `😀` resolvía a DejaVu Sans //! y `❤️` a Inter, y **ninguna app de la suite pintaba un emoji a color** — //! cero píxeles cromáticos. No era el rasterizador: vello 0.9 dibuja COLR y //! CBDT solo, y fontconfig resuelve `Noto Color Emoji` con el alias `emoji`. //! Era el ORDEN de la pila de fuentes (ver [`crate::rangos_emoji`]). //! //! ## Por qué no alcanza con lo que ya trae swash //! //! El agrupador de clusters de swash —el mismo que parley usa para decidir //! `is_emoji`— clasifica por **Extended_Pictographic**, que es más ancho que //! «se pinta a color». Medido acá: `⚠ ▶ ✔ © ‼ ☀ ⤴` dan `extpict = true`. Si //! se los mandara a la fuente de emoji, los símbolos del chrome de la UI //! saldrían como caritas de colores — una regresión visible, y en la dirección //! contraria a la que este arreglo viene a corregir. //! //! Lo que separa una cosa de la otra es la propiedad Unicode //! **`Emoji_Presentation`**, y no se puede deducir de las otras dos: es una //! tabla. Ésta. //! //! ## De dónde salen estos rangos //! //! Derivados de `emoji-test.txt` de **Unicode 16.0** (2024-08-14) con la regla //! que define UTS #51: en una línea `fully-qualified`, un emoji de **un solo //! codepoint** tiene `Emoji_Presentation = Yes`; si su forma completa lleva //! `U+FE0F` detrás, tiene `No`. Salieron 1.208 codepoints en 81 rangos, con //! **cero** solapamiento entre los dos conjuntos — que es el control de que la //! regla se aplicó bien. //! //! Se les suman a mano dos bloques de **componentes**, que nunca aparecen //! sueltos en ese archivo (siempre van dentro de una secuencia) pero se pintan //! a color: los indicadores regionales de las banderas (`U+1F1E6..=U+1F1FF`) y //! los modificadores de tono de piel (`U+1F3FB..=U+1F3FF`). //! //! Para regenerar con una versión nueva de Unicode, bajar `emoji-test.txt` y: //! //! ```text //! yes = {cp for una línea fully-qualified de UN codepoint} //! no = {cp for una línea fully-qualified de (cp, FE0F)} //! assert not (yes & no) # el control //! yes |= 1F1E6..=1F1FF | 1F3FB..=1F3FF //! ``` //! //! El único costo de que esta tabla envejezca es que un emoji **nuevo** salga //! monocromo, que es exactamente lo que pasaba con todos antes de existir. /// Rangos (inclusivos) de codepoints con `Emoji_Presentation = Yes`. /// Ordenados y disjuntos: se buscan por bisección. const PRESENTACION_EMOJI: &[(u32, u32)] = &[ (0x231A, 0x231B), (0x23E9, 0x23EC), (0x23F0, 0x23F0), (0x23F3, 0x23F3), (0x25FD, 0x25FE), (0x2614, 0x2615), (0x2648, 0x2653), (0x267F, 0x267F), (0x2693, 0x2693), (0x26A1, 0x26A1), (0x26AA, 0x26AB), (0x26BD, 0x26BE), (0x26C4, 0x26C5), (0x26CE, 0x26CE), (0x26D4, 0x26D4), (0x26EA, 0x26EA), (0x26F2, 0x26F3), (0x26F5, 0x26F5), (0x26FA, 0x26FA), (0x26FD, 0x26FD), (0x2705, 0x2705), (0x270A, 0x270B), (0x2728, 0x2728), (0x274C, 0x274C), (0x274E, 0x274E), (0x2753, 0x2755), (0x2757, 0x2757), (0x2795, 0x2797), (0x27B0, 0x27B0), (0x27BF, 0x27BF), (0x2B1B, 0x2B1C), (0x2B50, 0x2B50), (0x2B55, 0x2B55), (0x1F004, 0x1F004), (0x1F0CF, 0x1F0CF), (0x1F18E, 0x1F18E), (0x1F191, 0x1F19A), (0x1F1E6, 0x1F1FF), (0x1F201, 0x1F201), (0x1F21A, 0x1F21A), (0x1F22F, 0x1F22F), (0x1F232, 0x1F236), (0x1F238, 0x1F23A), (0x1F250, 0x1F251), (0x1F300, 0x1F320), (0x1F32D, 0x1F335), (0x1F337, 0x1F37C), (0x1F37E, 0x1F393), (0x1F3A0, 0x1F3CA), (0x1F3CF, 0x1F3D3), (0x1F3E0, 0x1F3F0), (0x1F3F4, 0x1F3F4), (0x1F3F8, 0x1F43E), (0x1F440, 0x1F440), (0x1F442, 0x1F4FC), (0x1F4FF, 0x1F53D), (0x1F54B, 0x1F54E), (0x1F550, 0x1F567), (0x1F57A, 0x1F57A), (0x1F595, 0x1F596), (0x1F5A4, 0x1F5A4), (0x1F5FB, 0x1F64F), (0x1F680, 0x1F6C5), (0x1F6CC, 0x1F6CC), (0x1F6D0, 0x1F6D2), (0x1F6D5, 0x1F6D7), (0x1F6DC, 0x1F6DF), (0x1F6EB, 0x1F6EC), (0x1F6F4, 0x1F6FC), (0x1F7E0, 0x1F7EB), (0x1F7F0, 0x1F7F0), (0x1F90C, 0x1F93A), (0x1F93C, 0x1F945), (0x1F947, 0x1F9AF), (0x1F9B4, 0x1F9FF), (0x1FA70, 0x1FA7C), (0x1FA80, 0x1FA89), (0x1FA8F, 0x1FAC6), (0x1FACE, 0x1FADC), (0x1FADF, 0x1FAE9), (0x1FAF0, 0x1FAF8),]; /// ¿Este carácter se pinta a color **por defecto**, sin que nadie se lo pida /// con un selector de variación? pub(crate) fn presentacion_emoji(ch: char) -> bool { let cp = ch as u32; PRESENTACION_EMOJI .binary_search_by(|&(ini, fin)| { if cp < ini { core::cmp::Ordering::Greater } else if cp > fin { core::cmp::Ordering::Less } else { core::cmp::Ordering::Equal } }) .is_ok() } #[cfg(test)] mod tests { use super::presentacion_emoji; /// La tabla tiene que estar ordenada y sin solapes, o la bisección miente /// en silencio: devolvería `false` para un codepoint que sí está. #[test] fn la_tabla_esta_ordenada_y_es_disjunta() { let mut anterior = None; for &(ini, fin) in super::PRESENTACION_EMOJI { assert!(ini <= fin, "rango dado vuelta: {ini:04X}..{fin:04X}"); if let Some(prev_fin) = anterior { assert!( ini > prev_fin + 1, "rango {ini:04X} pegado o solapado con el anterior que termina en {prev_fin:04X} \ (si son contiguos hay que fundirlos, no listarlos aparte)" ); } anterior = Some(fin); } } /// Los símbolos del chrome de la UI **no** son presentación emoji, y por /// eso este arreglo no los toca. Son justo los que `extended_pictographic` /// sí marca — o sea, el control negativo de por qué esta tabla existe. #[test] fn los_simbolos_del_chrome_no_van_a_la_fuente_de_emoji() { for ch in ['⚠', '❤', '✔', '▶', '©', '‼', '☀', '⤴', '✎', '→', '●', '✓', '★'] { assert!( !presentacion_emoji(ch), "{ch:?} (U+{:04X}) se iría a la fuente de emoji y saldría de colores", ch as u32 ); } } /// Y los emoji de verdad sí, incluidos los del BMP —que es donde una /// heurística por «está arriba de U+1F000» se habría equivocado. #[test] fn los_emoji_de_verdad_son_presentacion_emoji() { for ch in ['😀', '👍', '⭐', '⚽', '✅', '❌', '⌚', '🇦', '🏽', '🎉', '🔥'] { assert!( presentacion_emoji(ch), "{ch:?} (U+{:04X}) saldría monocromo", ch as u32 ); } } }