Normalizador Unicode

O Unicode tem quatro formas de normalização e não são intercambiáveis. NFC e NFD reorganizam o mesmo carácter entre um ponto de código e uma base mais uma marca combinante; NFKC e NFKD vão mais longe e reescrevem caracteres que apenas se parecem uns com os outros — a ligadura fi, o A de largura total A, o círculo ①, o numeral romano Ⅻ. Esta página passa o mesmo texto pelas quatro, imprime uma linha por carácter a dizer o que mudou e porquê, conta grafemas face a pontos de código e bytes UTF-8, e compara duas strings na forma que escolher. A segunda metade trata dos caracteres que nunca aparecem: espaços de largura zero, ligadores, controlos bidirecionais, seletores de variação, caracteres de controlo C0 e C1, pontos de código de uso privado e metades de substitutos partidas, cada um com a sua posição e nome, e a opção de os remover de uma cópia do texto.
Todo o processamento é feito localmente no seu navegador — os seus dados nunca são enviados para nenhum servidor.

Não sabe o que é a normalização? A normalização Unicode explicada em linguagem simples — porque é que é pode ser um carácter ou dois, e porque é que isso quebra um início de sessão.
Forma a reportar e com que comparar:
Comparar as duas caixas depois de:
NFC (ainda nada)
NFD (ainda nada)
NFKC (ainda nada)
NFKD (ainda nada)
Ainda nada comparado. Cole as duas strings e prima Comparar as duas strings.
primeira string, normalizada (ainda nada)
segunda string, normalizada (ainda nada)
Ainda nada analisado. Prima Analisar invisíveis e caracteres perigosos, depois Remover se quiser o texto sem esses caracteres.
texto com as classes selecionadas removidas (ainda nada)
Ctrl+Enter na primeira caixa executa o modo ativo.
Escolha um modo e clique no seu botão de ação. Normalizar escreve as quatro formas do seu texto e uma linha por carácter; Comparar diz se duas strings são iguais depois da normalização; Analisar lista os caracteres que não aparecem, e pode removê-los. O botão Autoteste volta a executar cada caso documentado no ficheiro de dados desta página, cada carácter que as tabelas de invisíveis e de controlo indicam, e reporta aqui o número.
Cada carácter, uma linha de cada:o que é o carácter, o que as quatro formas lhe fazem, e porquê. Uma linha sombreada é um carácter que pelo menos uma forma altera; a coluna NFD mostra em que se decompõe o carácter, quando se decompõe.
Quantos caracteres são?O mesmo texto contado de quatro formas: grupos de grafemas (o que um leitor conta), pontos de código (o que o Unicode atribui), unidades UTF-16 (o que o JavaScript indexa) e bytes UTF-8 (o que viaja). A normalização move os números entre a segunda e a terceira coluna sem alterar a primeira.
O que foi encontrado, pela ordem em que aparece:a posição conta pontos de código, a classe é a que o carácter pertence, e a nota diz o que esse carácter faz a tudo o que lê o texto a seguir.
As classes que esta página trata como suspeitas:a tabela é impressa a partir do próprio analisador, por isso as palavras desta página e o comportamento dos botões não se podem afastar. A gravidade diz respeito ao efeito numa comparação, e não a quão provável é que o carácter seja inofensivo.
Os caracteres de uso privado são assinalados como suspeitos por uma razão diferente das restantes: são apresentados, mas não têm significado acordado, por isso o mesmo ponto de código é uma imagem diferente em cada tipo de letra que o define.
Quão grande é a entrada que esta página aceita, e porquê:a normalização reconstrói a string inteira, e esta página faz várias delas mais uma análise e uma contagem de grafemas, por isso o custo cresce em linha reta com o comprimento da entrada. Os limites abaixo existem porque a tabela por carácter é desenhada no documento, e uma tabela com um milhão de linhas é o que de facto para um navegador.
O que é a normalização Unicode?

O Unicode dá mais de uma maneira de escrever alguns caracteres. A letra é pode ser um único ponto de código, U+00E9, ou um e normal seguido de um acento agudo combinante, U+0065 U+0301. As duas parecem iguais no ecrã, imprimem-se da mesma forma, e como strings são diferentes: têm comprimentos diferentes, bytes UTF-8 diferentes, e 'é' === 'e\u0301' é false.

Isto não é uma curiosidade, é uma classe de erro. Receber uma forma ou a outra depende do esquema do teclado, do sistema operativo, do método de entrada e de o texto alguma vez ter sido copiado através de um processador de texto. Uma verificação de palavra-passe, um índice único da base de dados, uma lista de bloqueio ou uma chave de cache que compara strings byte a byte tratará as duas grafias como dois valores diferentes — por isso um utilizador pode definir uma palavra-passe num dispositivo e não conseguir entrar a partir de outro, e a mesma palavra pode aparecer duas vezes numa lista de nomes « já ocupados ».

A versão de 30 segundos: NFC e NFD são o mesmo texto escrito de duas formas — o NFC compõe (e + acento passa a ser um só carácter), o NFD decompõe (um carácter passa a ser e + acento). Destinam-se a ser sem perdas e são as duas formas a usar para comparação e armazenamento. NFKC e NFKD fazem tudo o que as duas primeiras fazem e depois também substituem caracteres de compatibilidade: a ligadura fi torna-se duas letras, ½ torna-se 1⁄2, o numeral romano ⅲ torna-se XII, um A de largura total torna-se um A normal. É exatamente o que um índice de pesquisa ou um filtro de palavras-chave querem, e exatamente o que nunca deve aplicar a uma palavra-passe.
Porque são quatro formas e não uma

A divisão existe porque duas finalidades diferentes estavam a ser servidas. Compor e decompor texto é uma alteração canónica: o resultado significa o mesmo e, para as duas primeiras formas, nada se perde. Eliminar ligaduras, expoentes, letras em círculo e formas de largura total é uma alteração de compatibilidade: é feita por conveniência de comparação, e destrói de facto a distinção entre duas strings que o Unicode diz serem diferentes. As quatro formas são todas as combinações das duas decisões.

A tabela acima é a lista de casos do ficheiro de dados desta página, verificada contra o próprio String.prototype.normalize da plataforma, e o Autoteste volta a verificar cada linha dela à sua frente. Leia as duas primeiras linhas em conjunto: são o mesmo texto, escrito de duas formas, e depois da normalização são a mesma string.

Exemplos resolvidos

Estas são as strings pelas quais o botão Exemplo passa, normalizadas ao vivo pela página em vez de digitadas como respostas. Cada uma foi escolhida porque uma forma lhe faz algo visível.

Caracteres de compatibilidade: o que o NFKC lhe custa

NFKC e NFKD são com perdas, e a perda é deliberada. Respondem à pergunta « é esta a mesma palavra », não à pergunta « é esta a mesma string ». Seguem-se três consequências, e as três são fáceis de errar em código de produção:

  • Nunca guarde uma palavra-passe, um token ou uma assinatura numa forma de compatibilidade. Duas entradas diferentes colapsariam no mesmo valor guardado, o que transforma uma verificação de comprimento ou de caracteres numa mais fraca. Normalize as palavras-passe com NFC (ou não as normalize) e compare-as byte a byte depois de fazer exatamente o mesmo nos dois lados.
  • Uma dobragem de compatibilidade pode transformar um carácter em vários. A ligadura fi é um ponto de código que se torna duas letras, por isso um campo validado quanto ao comprimento antes da normalização pode ficar mais longo depois. Normalize primeiro, valide depois, e só então guarde.
  • O NFKC não remove tudo o que é invisível. Um seletor de variação sobrevive ao NFKC e ao NFKD, tal como um ligador de largura zero. Por isso esta página tem um modo de análise: a normalização responde a uma pergunta sobre a forma e identidade de um carácter, e o analisador responde a uma pergunta sobre se algo está lá de todo.
Grafemas, pontos de código e bytes

A normalização altera a segunda destas contagens e normalmente deixa a primeira como está. Os três números são todos « o comprimento do texto » e são todos diferentes, por isso um campo de formulário com um limite de 20 caracteres pode pertencer a uma string com 20 pontos de código, 30 unidades UTF-16 e 47 bytes.

O número na coluna de grafemas é o que uma pessoa conta. A diferença entre ele e a contagem de pontos de código é exatamente o que o analisador faz: um nome feito de 12 pontos de código pode mostrar-se como 12 letras, como 3 letras com 9 marcas invisíveis entre elas, ou como um único emoji.

Os caracteres que não consegue ver

Um espaço de largura zero não ocupa largura, não altera nenhum glifo, e é um carácter real com um ponto de código real. Dois identificadores que parecem idênticos no ecrã podem diferir por causa de um deles, e é assim que um nome numa lista pode ser um impostor que se ordena, pesquisa e autentica de forma diferente do nome ao lado. O mesmo acontece com os controlos bidirecionais, que podem fazer uma string ser apresentada numa ordem diferente da ordem em que foi escrita — um truque que já foi usado para disfarçar nomes de ficheiros.

O analisador do terceiro modo lista cada um deles com a sua posição: caracteres de largura zero e ligadores, controlos bidirecionais, seletores de variação, espaços invulgares, caracteres de controlo C0 e C1, pontos de código de uso privado, e um substituto que não é metade de um par válido, que nem sequer é um carácter. A tabela de classes mais acima nesta página diz o que cada classe faz, e o botão de remoção apaga uma cópia desses caracteres sem tocar na sua entrada.

Os erros que esta página foi feita para apanhar
  • Comparar duas strings sem as normalizar. 'é' === 'e\u0301' é falso. Normalize os dois lados com a mesma forma antes de comparar, e use a mesma forma no índice com que comparar.
  • Normalizar apenas de um lado. Uma pesquisa que normaliza a consulta mas não o valor guardado não encontra nada, e parece um problema de dados e não de normalização.
  • Aplicar uma forma de compatibilidade a um formulário e chamar-lhe seguro para NFC. Aplicar NFKC a uma palavra-passe reduz o espaço de palavras-passe possíveis e funde entradas que deviam ser distintas.
  • Presumir que uma forma de normalização corrige uma falsificação. Não remove um ligador de largura zero, um seletor de variação ou um homógrafo — um а cirílico e um a latino são pontos de código diferentes que o NFC deixa como estão.
  • Confiar numa contagem de caracteres. A contagem que o utilizador vê, a que uma coluna da base de dados impõe e a que um limite de bytes impõe são três números diferentes, e as linhas acima mostram a distância a que podem estar.
  • Presumir que vai receber NFC. O texto que chega de um método de entrada, de um ficheiro ou de outro sistema está na forma que esse sistema produziu. Esta página começa em NFC porque é a forma que se espera que a web use, e não porque é isso que a sua entrada é.
Respostas rápidas

“Porque é que estas duas strings não são iguais se parecem iguais?”

Porque um deles está escrito com um carácter composto e o outro com uma letra base mais uma marca combinante, ou porque um deles contém um carácter que não desenha absolutamente nada. Mude para o modo Comparar e coloque uma string em cada caixa: a página reporta a primeira posição onde as strings normalizadas diferem e o que está nessa posição em cada uma delas.

“Que forma devo usar numa aplicação?”

NFC para tudo o que uma pessoa vai ler, mostrar, guardar e pesquisar, que é a forma que o W3C recomenda para a web. NFD é a resposta certa quando o código por trás quer letras base e marcas separadas, o que é comum no processamento de texto e em alguns sistemas de ficheiros do macOS. Use NFKC ou NFKD apenas quando estiver a comparar em vez de a guardar: índices de pesquisa, filtros de palavras-chave, deteção de duplicados.

“Uma string já normalizada continua normalizada depois de eu lhe concatenar algo?”

Nem sempre. A normalização não se mantém com a concatenação: juntar duas strings NFC pode produzir uma sequência que o NFC teria escrito de forma diferente, porque uma marca combinante no início da segunda string pode ligar-se ao carácter no fim da primeira. Normalize depois da montagem, e não apenas antes.

“A normalização remove os caracteres invisíveis?”

Não, com uma exceção parcial: o NFKC e o NFKD substituem de facto o espaço sem quebra por um espaço normal, que é um resultado visível de uma dobragem de compatibilidade. Tudo o resto — espaços de largura zero, ligadores, controlos bidirecionais, seletores de variação — passa pelas quatro formas sem alterações. É para isso que existe o terceiro modo.

“Porque é que a contagem de bytes muda quando a contagem de caracteres não muda?”

Porque as duas formas são construídas a partir de pontos de código diferentes que custam números diferentes de bytes em UTF-8. Um é pré-composto são dois bytes; o par decomposto são três. A tabela de contagens acima imprime os quatro números lado a lado para a entrada e para cada forma, por isso a diferença fica visível em vez de descrita.

“Como sei que os resultados desta página estão certos?”

Prima Autoteste. Normaliza cada caso do ficheiro de dados desta página, volta a verificar as quatro formas umas contra as outras quanto às relações documentadas (o NFC é idempotente, o NFD de uma string NFC é o mesmo que o NFD do original, e assim por diante), passa cada carácter nomeado nas tabelas de invisíveis e de controlo pelo analisador, e conta os casos de grafemas face a Intl.Segmenter.

Normas e créditos

As quatro formas e as suas definições vêm do Unicode Standard, Anexo 15 (Unicode Normalization Forms). Os pontos de código usados como exemplos de decomposição são os que a norma atribui; esta página não implementa ela própria as tabelas de decomposição — chama o próprio String.prototype.normalize do navegador, que é a mesma implementação que a plataforma usa para entrada de texto, ordenação e comparação, por isso o que aqui é reportado é o que o resto do sistema faz com a mesma string. Os agrupamentos de grafemas vêm de Intl.Segmenter onde o navegador o tem, e de uma aproximação documentada onde não o tem.

O texto desta página foi escrito para este site. Cada exemplo resolvido nesta página foi produzido pelo próprio código da página e depois verificado contra a lista de casos registada no ficheiro de dados, que por sua vez foi verificada contra os dados NormalizationTest do Unicode para os mesmos caracteres. As tabelas de caracteres invisíveis e perigosos são um subconjunto curado, escrito para avisos legíveis por pessoas, e não o perfil de segurança Unicode completo (UTS #39), e a página diz isso em vez de sugerir uma completude que não tem.