Łączenie Unicode

1

Jakiś czas temu napisałem prostą aplikację HTML/JS, była to pierwsza mająca praktyczne zastosowanie, od czasu do czasu rozbudowuję i aktualizuję:

Aplikacja
Repozytorium

Nie opisuję apki, bo nie mam po co przepisywać readme.md z repozytorium, więc od razu przechodzę do sedna.

Czasami, jak w internecie zobaczę jakiś obrazek, to lubię wkleić do tej apki i zobaczyć jego opis. W ten sposób, przez przypadek odkryłem, ze niektóre znaki powstają z kilku znaków składowych.

Bardzo popularnym połączeniem jest dostawianie znaku FE0F, który zamienia czarno-biały znak na kolorowy, a dostawienie znaku FE0E zamienia kolorowy znak na czarno-biały (nie wszystkie znaki, ale w wielu przypadkach to działa). Właśnie po tym odkryciu doimplementowałem belkę z 9 "slotami", która umożliwia utworzenie dowolnego zestawienia, jednakże zamiana zestawu znaków na jeden zależy od tego, jakie znaki się dobierze.

Poniżej kilka przykładów, czyli znak złożony i numery znaków prostych w postaci szesnastkowej.

🌈︎ 01F308|FE0E
‼️ 203C|FE0F
🏳️‍🌈 01F3F3|FE0F|200D|01F308
👁️‍🗨️ 01F441|FE0F|200D|01F5E8|FE0F
🤷🏻‍♀️ 01F937|01F3FB|200D|2640|FE0F
🙆‍♀️ 01F646|200D|2640|FE0F
🙏🏾 01F64F|01F3FE

Pytania są następujące:

  1. Gdzie i w jaki sposób są zdefiniowanie wszystkie możliwe znaki złożone, czyli co z czym można łączyć, żeby otrzymać inny znak? Czy to jest słownik, czy jakieś zasady oprócz tej z dostawianiem FE0F lub FE0E? Gdzie jest określone, w których przypadkach FE0F bądź FE0E zmienia wygląd znaku?
  2. W jaki sposób w JavaScript, jak się poda ciąg znaków (jako tekst) można wyczuć, ile jest znaków wizualnie? Np. jak się poda znak tęczowej flagi z przykładów powyżej, to widzi się jeden znak, ale tak naprawdę są to 4 osobne znaki.

Chodzi o to, żeby na przykład dla znaku prostego o podanym numerze wypisać, z jakimi znakami można połączyć i tym samym, jakie znaki złożone można otrzymać poczynając od tego znaku prostego. Np. biorę znak 01F3F3 i algorytm wygeneruje między innymi takie zestawienia:
🏳 01F3F3
🏳️ 01F3F3|FE0F
🏳️‍🌈 01F3F3|FE0F|200D|01F308

Natomiast dla znaku prostego 01F1E6 można wygenerować między innymi:
🇦 01F1E6
🇦🇨 01F1E6|01F1E8
🇦🇫 01F1E6|01F1EB
🇦🇽 01F1E6|01F1FD

Gdzie u źródła, czyli link, jest określone co z czym można łączyć?

0

Znalazłem coś w międzyczasie, to już chyba prędzej z tego. Emoji, przy czym trzeba by patrzeć na numery wersji. W swojej apce bym po prostu zaimplementował listę z tych plików. Wygląda na to, że organizator Unikodu zrobił listę wszystkich możliwości.

0

Co do emoji, to sprawa jest chyba wyjaśniona.

Właśnie zauważyłem, że wśród znaków języka "Sinhala" i "Tamil" są znaki, które doklejają się do poprzedniego znaku i po takim sklejeniu znak jest traktowany pojedynczo (nie można wstawić kursora pomiędzy te dwa sklejone znaki).

Krótkie napisy w tych językach są chociażby tu: link
Na przykład, znak sklejony කා składa się ze znaków 0D9A i 0DCF. Ten drugi to znak doklejający się do pierwszego Jak jest samotnym znakiem w linii tekstu, to pokazuje się kółko, jak poniżej.
ා︎

Oczywiście można wizualnie i empirycznie stwierdzić, które znaki "Sinhala", "Tamil" i pewnie jeszcze inne doklejają się do innych znaków. Czy mozna to stwierdzić jednoznacznie wykorzystując sam JavaScript lub czy to jest opisane na Unicode.org?

0

Trochę czasu zeszło, ale przechodząc do rzeczy: Obecnie skupiam się na analizie już napisanego tekstu. Na wejściu jest ciąg znaków elementarnych na podstawie dowolnego tekstu, a na wyjściu chcę otrzymać ciąg znaków elementarnych i złożonych, to znaczy, że chciałbym wyłapać znaki złożone i podciąg znaków elementarnych wymienić na ciąg znaków złożonych. Strukturalnie, obrabiany ciąg to jest lista, w której jedna pozycja to lista liczb całkowitych.

Przykład - dane, numery znaków w hex:

  • 61
  • 01F3F3
  • FE0F
  • 200D
  • 01F308
  • 62
  • 6F
  • 0337
  • 77
  • 0337
  • 6E
  • 0337
  • 71
  • 01F1EC
  • 01F1E7
  • 77

Przykład - wynik, jedna pozycja to jeden znak, kilka numerów to znak złożony:

  • 61
  • 01F3F3,FE0F,200D,01F308
  • 62
  • 6F,0337
  • 77,0337
  • 6E,0337
  • 71
  • 01F1EC,01F1E7
  • 77

Przekształcenie składa się z 3 etapów.

Etap 1: Na podstawie Plik1 i Plik2 mam listę nazwanych znaków złożonych, między innymi większość emoji i flagi państwowe. W ciągu wejściowym wyszukuję podciągi będące ciągami znaków z wymienionych plików i wymieniam na pojedynczy znak złożony.

Etap 2: Jest tablica ComplexSuffixCodes zawierająca wszystkie znaki będące przyrostkami, czyli znakami będącymi dodatkami do znaku poprzedzającego, tzw. "combining character". Ciąg wynikowy z pierwszego etapu jest jednocześnie wejściowy do drugiego etapu. Iteruję go od drugiej pozycji do ostatniej. Jeżeli dana pozycja jest listą jednoelementową i ten element znajduje się w tablicy ComplexSuffixCodes, to ten numer dopisuję jako kolejny numer do poprzedniej pozycji i usuwam bieżącą pozycję.

Etap 3: Jest tablica ComplexJoinerCodes zawierająca znaki łączące poprzedni i następny. Ciąg znaków po drugim etapie jest wejściem do etapu trzeciego. Ten ciąg iteruję od drugiej do przedostatniej pozycji. Jeżeli dana pozycja jest listą jednoelementową i ten element znajduje się w tablicy ComplexJoinerCodes, to łączę ze sobą poprzednią pozycję, bieżącą i następną, a więc następuje zamiana trzech pozycji na jedną.

Jeżeli ciąg wejściowy nie zawiera znaków o numerach od 01F1EC do 01F1FF (znaki tworzące flagi państwowe), to pominięcie etapu 1 (czyli wykonanie tylko etapu 2 i 3) da ten sam wynik, co wykonanie wszystkich trzech etapów po kolei. W ten sposób obsługuję ponadto między innymi nienazwane emoji, których przykłady są NON-RGI, znaki diakrytyczne i selektory wariantów.

A jeżeli ciąg wejściowy zawiera same nazwane emoji bez flag państwowych, to wykonanie samego etapu 1 lub pominięcie etapu 1 i wykonanie etapów 2 i 3 da ten sam wynik.

Na podstawie znaków diakrytycznych z Wikipedii, znaków tagów i własnych prób, udało mi się hardcodować wypełnienie tablic w sposób pokrywający większość znaków.

var ComplexSuffixCodes = [];
var ComplexJoinerCodes = [];

function FillInComplexCodes()
{
    // Combining Diacritical Marks
    for (var I = 0x0300; I <= 0x036F; I++) { ComplexSuffixCodes.push(I); }

    // Combining Cyrillic (selected chars)
    for (var I = 0x0483; I <= 0x0489; I++) { ComplexSuffixCodes.push(I); }

    // Combining Diacritical Marks Extended
    for (var I = 0x1AB0; I <= 0x1AFF; I++) { ComplexSuffixCodes.push(I); }

    // Combining Diacritical Marks Supplement
    for (var I = 0x1DC0; I <= 0x1DFF; I++) { ComplexSuffixCodes.push(I); }

    // Combining Diacritical Marks for Symbols
    for (var I = 0x20D0; I <= 0x20FF; I++) { ComplexSuffixCodes.push(I); }

    // Cyrillic Extended-A
    for (var I = 0x2DE0; I <= 0x2DFF; I++) { ComplexSuffixCodes.push(I); }

    // Combining Devanagari (selected chars)
    for (var I = 0xA8E0; I <= 0xA8F1; I++) { ComplexSuffixCodes.push(I); }

    // Variation Selectors
    for (var I = 0xFE00; I <= 0xFE0F; I++) { ComplexSuffixCodes.push(I); }

    // Combining Half Marks
    for (var I = 0xFE20; I <= 0xFE2F; I++) { ComplexSuffixCodes.push(I); }

    // Emoji Modifier Fitzpatrick (selected chars)
    for (var I = 0x01F3FB; I <= 0x01F3FF; I++) { ComplexSuffixCodes.push(I); }

    // Tags
    for (var I = 0x0E0000; I <= 0x0E007F; I++) { ComplexSuffixCodes.push(I); }

    // Variation Selectors Supplement
    for (var I = 0x0E0100; I <= 0x0E01EF; I++) { ComplexSuffixCodes.push(I); }


    // Zero-width joiner
    ComplexJoinerCodes.push(0x200D);
}

FillInComplexCodes();

Tam, gdzie w komentarzu odnośnie ComplexSuffixCodes jest napisane "selected chars", jest dodany pewien zakres znaków, w pozostałych przypadkach do tablicy ComplexSuffixCodes jest dodany cały blok. Tablica ComplexJoinerCodes zawiera jeden znak i póki co nie stwierdziłem potrzeby, żeby zawierała inne znaki.

O ile algorytm działa zgodnie z oczekiwaniem i mam żadnych pytań odnośnie jego, o tyle mam wątpliwość, na podstawie czego wypełniać tablice wykorzystywane w etapie 2 i 3.

Znalazłem stronę Combining characters, która zawiera chyba wszystkie znaki potrzebne do etapu 2. To nie jest strona od Unicode, więc pytanie, na podstawie którego lub których plików z oficjalnych plików Unicode można tą informację uzyskać? W jaki sposób autorzy mogli wygenerować tą stronę?

W chwili obecnej przetestowałem między innymi na tytułach filmów tytuł 1 i tytuł 2, algorytm zadziałał prawidłowo i akurat wszystkie potrzebne znaki już mam pokryte.

Jest dużo pojedynczych znaków, które powinny znaleźć się w tablicy ComplexSuffixCodes, ale są rozrzucone po całym bloku zawierającym znaki z danego języka. Gdzie jest rzetelna informacja, które znaki są znakami typu "combining"?

Czy w przypadku łączników mających skleić i to, co jest po jednej stronie i po drugiej stronie, to czy faktycznie istnieje tylko jeden znak, czyli 0x200D (Zero-width joiner)? Jeśli chodzi o znaki takie, jak "Zero-width non-joiner", "Zero-width space" to oczywiście nie mogą to być łączniki, bo one właśnie wymuszają nieskładanie znaków w jeden.

Chodzi o to, żebym zaimplementował i wykonał algorytm, który wygeneruje kod funkcji FillInComplexCodes() na podstawie oficjalnej informacji. Jak konsorcjum Unicode wypuści aktualizację standardu, to wezmę nowsze pliki źródłowe i powtórzę proces.

1

Wszystkie dane powinny znajdować się w UnicodeData.txt, którego format jest tutaj. Natomiast jest też wyciąg z tego pliku: DerivedCombiningClass.txt.
Warty uwagi jest też opis właściwości znaków Unicode pogrupowanych według zastosowań: https://www.unicode.org/reports/tr44/tr44-28.html#Property_Index_Table , gdzie dla tego algorytmu istotne są grupy “Normalization” i “Shaping and Rendering”.
Opis emoji jest tutaj.

0

Właśnie miałem problem ze znalezieniem opisu do UnicodeData.txt, ponieważ ten plik i tak analizowałem w innym celu, to wystarczyło wczytać kolumnę 2 i 3.

Jak wykorzystałem samą kolumnę 3 i wziąłem wszystkie znaki bające tam liczbę inną niż 0, to dostałem znaczne, ale nie pełne pokrycie, a przy ręcznym przeglądaniu widziałem, ze niektóre znaki mają 0, a są doczepianą końcówką. Takim znakiem jest na przykład 0488 i 0489.

Jak się analizuje kolumnę 2, to wystarczy wziąć pod uwagę wszystkie znaki mające tam "Mn", "Mc" i "Me". Okazało się, że wszystkie znaki mające w kolumnie 3 wartość inną niż 0 mają jedną z tych trzech wartości, więc tak naprawdę wystarczyła analiza wartości kolumny 2. Jednakże, oprócz znaków z "Mn"/"Mc"/"Me", należy też potraktować jako przyrostki wszystkie znaki z zakresu od 01F3FB do 01F3FF (kolory skóry człowieka) i znaki od 0E0000 do 0E007F (tagi językowe).

W ten sposób uzyskałem chyba pełne pokrycie, a przynajmniej na testowanych ciągach zawierających emoji i teksty w różnych językach, a także kilka różnych "Zalgo text", nie stwierdziłem problemu ani, że jeszcze czegoś brakuje.

Na podstawie tekstu o emoji stwierdziłem, że szczególny znak, który musi być traktowany jako łącznik, to tylko 200D. Były tam też wymienione znaki zmieniające kolor włosów, ale taki znak jest zawsze poprzedzony znakiem ZWJ, więc tych znaków z włosami nie potrzeba traktować w szczególny sposób.

1

Wgrałem aktualizację aplikacji, linki w pierwszym poście. Oprócz interpretacji znaków złożonych jeszcze kilka innych ulepszeń i poprawek. Link ten sam, co w pierwszym poście. To do niej potrzebowałem algorytm wyszukiwania znaków złożonych, o którym dyskutuję.

W polu Find należy wpisać lub wkleić ciąg znaków, a następnie kliknąć przycisk Char. Znaki złożone będą miały w tabeli znak + w kolumnie #, którego kliknięcie pokaże znaki elementarne, które składają się na ten znak złożony. Aby zrobić nowe wyszukiwanie, należy kliknąć przycisk Clear.

1 użytkowników online, w tym zalogowanych: 0, gości: 1