Unicode-normaliserer

Unicode har fire normaliseringsformer, og de er ikke indbyrdes udskiftelige. NFC og NFD omarrangerer det samme tegn mellem ét kodepunkt og et grundtegn plus et kombinerende mærke; NFKC og NFKD går længere og omskriver tegn, der blot ligner hinanden — ligaturen fi, det fuldbredde A, det omringede ①, det romerske tal Ⅻ. Siden sender den samme tekst gennem alle fire, udskriver en række pr. tegn med angivelse af, hvad der er ændret, og hvorfor, tæller grafemer mod kodepunkter og UTF-8-bytes, og sammenligner to strenge under den form, man vælger. Den anden halvdel handler om de tegn, der slet ikke dukker op: nul-bredde-mellemrum, joinere, bidirektionelle styreelementer, variationsvælgere, C0- og C1-styretegn, kodepunkter til privat brug og ødelagte surrogathalvdele, hver med sin position og sit navn, og muligheden for at fjerne dem fra en kopi af teksten.
Al behandling sker lokalt i browseren — data uploades aldrig til nogen server.

Usikker på, hvad normalisering er? Unicode-normalisering forklaret på klart dansk — hvorfor é kan være ét tegn eller to, og hvorfor det kan ødelægge et login.
Form at rapportere og sammenligne med:
Sammenlign de to bokse efter:
NFC (intet endnu)
NFD (intet endnu)
NFKC (intet endnu)
NFKD (intet endnu)
Intet sammenlignet endnu. Indsæt de to strenge, og tryk på Sammenlign de to strenge.
første streng, normaliseret (intet endnu)
anden streng, normaliseret (intet endnu)
Intet scannet endnu. Tryk på Scan for usynlige og farlige tegn, og derefter på Fjern, hvis teksten skal vises uden disse tegn.
tekst med de valgte klasser fjernet (intet endnu)
Ctrl+Enter i den første boks kører den aktive tilstand.
Vælg en tilstand, og klik på dens handlingsknap. Normalisér skriver alle fire former af teksten og en række pr. tegn; Sammenlign angiver, om to strenge er ens efter normalisering; Scan lister de tegn, der ikke vises, og kan fjerne dem. Selvtest-knappen kører alle dokumenterede tilfælde i sidens datafil igen, alle tegn tabellerne for usynlige og styretegn nævner, og rapporterer antallet her.
Hvert tegn, én række pr. tegn:hvad tegnet er, hvad de fire former gør ved det, og hvorfor. En skygget række er et tegn, som mindst én form ændrer; NFD-kolonnen viser, hvad tegnet dekomponeres til, når det overhovedet dekomponeres.
Hvor mange tegn er det?Den samme tekst talt fire måder: grafemklynger (det, en læser tæller), kodepunkter (det, Unicode tildeler), UTF-16-enheder (det, JavaScript indekserer) og UTF-8-bytes (det, der rejser). Normalisering flytter tallene mellem anden og tredje kolonne uden at ændre den første.
Hvad der blev fundet, i den rækkefølge det optræder:positionen tæller kodepunkter, klassen er den, tegnet tilhører, og noten siger, hvad tegnet gør ved det, der læser teksten bagefter.
De klasser, siden behandler som mistænkelige:tabellen udskrives fra scanneren selv, så ordene på denne side og knappernes opførsel ikke kan glide fra hinanden. Alvorlighed handler om effekten på en sammenligning, ikke om hvor sandsynligt det er, at tegnet er uskyldigt.
Tegn til privat brug er markeret som mistænkelige af en anden grund end resten: de bliver vist, men de har ingen aftalt betydning, så samme kodepunkt er et andet billede i hver skrifttype, der definerer det.
Hvor stort et input siden accepterer, og hvorfor:normalisering er en genopbygning af hele strengen, og siden udfører flere af dem plus en scanning og en grafemtælling, så omkostningen vokser lineært med inputtets længde. Grænserne nedenfor findes, fordi tabellen pr. tegn tegnes i dokumentet, og en tabel med en million rækker er netop det, der standser en browser.
Hvad er Unicode-normalisering?

Unicode giver mere end én måde at skrive nogle tegn på. Bogstavet é kan være ét kodepunkt, U+00E9, eller et almindeligt e efterfulgt af et kombinerende akut accent, U+0065 U+0301. De to ser ens ud på skærmen, de udskrives ens, og som strenge er de forskellige: de har forskellige længder, forskellige UTF-8-bytes, og 'é' === 'e\u0301' er false.

Det er ikke en kuriositet, det er en fejlklasse. Om man får den ene eller den anden form, afhænger af tastaturlayoutet, operativsystemet, inputmetoden og om teksten nogensinde er kopieret gennem et tekstbehandlingsprogram. Et adgangskodetjek, et unikt databaseindeks, en blokeringsliste eller en cachenøgle, der sammenligner strenge byte for byte, vil behandle de to stavemåder som to forskellige værdier — så en bruger kan sætte en adgangskode på én enhed og ikke kunne logge ind fra en anden, og det samme ord kan optræde to gange på en liste over „allerede taget“-navne.

Version på 30 sekunder: NFC og NFD er den samme tekst skrevet på to måder — NFC sammensætter (e + accent bliver til ét tegn), NFD dekomponerer (ét tegn bliver til e + accent). De er beregnet til at være tabsfrie og er de to former, der skal bruges til sammenligning og lagring. NFKC og NFKD gør alt, hvad de to første gør, og erstatter derefter også kompatibilitetstegn: ligaturen fi bliver til to bogstaver, ½ bliver til 1⁄2, det romerske tal ⅲ bliver til XII, et fuldbredde A bliver til et almindeligt A. Det er præcis, hvad et søgeindeks eller et nøgleordsfilter har brug for, og præcis det, man aldrig må anvende på en adgangskode.
Hvorfor fire former og ikke én

Opdelingen findes, fordi to forskellige mål blev varetaget. At sammensætte og dekomponere tekst er en kanonisk ændring: resultatet betyder det samme, og for de to første former går intet tabt. At fjerne ligaturer, hævede tal, omringede bogstaver og fuldbreddeformer er en kompatibilitets-ændring: den udføres for at lette matchning, og den ødelægger faktisk skellet mellem to strenge, som Unicode siger er forskellige. De fire former er alle kombinationer af de to beslutninger.

Tabellen ovenfor er tilfældelisten fra sidens datafil, kontrolleret mod platformens egen String.prototype.normalize, og selvtesten kontrollerer hver række af den direkte på siden. Læs de to første rækker sammen: de er den samme tekst skrevet på to måder, og efter normalisering er de den samme streng.

Gennemregnede eksempler

Disse er de strenge, Eksempel-knappen gennemgår, normaliseret live af siden i stedet for at blive skrevet ind som svar. Hver af dem blev valgt, fordi en form gør noget synligt ved den.

Kompatibilitetstegn: hvad NFKC koster

NFKC og NFKD er tabsgivende, og tabet er bevidst. De besvarer spørgsmålet „er det samme ord“, ikke spørgsmålet „er det den samme streng“. Tre konsekvenser følger, og alle tre er lette at få galt i produktionskode:

  • Gem aldrig en adgangskode, et token eller en signatur i en kompatibilitetsform. To forskellige input ville falde sammen til den samme gemte værdi, og det gør en længde- eller tegnkontrol svagere. Normalisér adgangskoder med NFC (eller slet ikke), og sammenlign dem byte for byte efter at have gjort præcis det samme på begge sider.
  • En kompatibilitetsfold kan gøre ét tegn til flere. Ligaturen fi er ét kodepunkt, der bliver til to bogstaver, så et felt, der blev valideret for længde før normalisering, kan være længere bagefter. Normalisér først, valider derefter, og gem til sidst.
  • NFKC fjerner ikke alt usynligt. En variationsvælger overlever NFKC og NFKD, og det gør en nul-bredde-joiner også. Det er grunden til, at siden har en scanningstilstand: normalisering besvarer et spørgsmål om et tegns form og identitet, mens scanneren besvarer et spørgsmål om, hvorvidt noget overhovedet er der.
Grafemer, kodepunkter og bytes

Normalisering ændrer det andet af disse tal og lader det første normalt være. De tre tal er alle „tekstens længde“ og alle forskellige, og derfor kan et formularfelt med en grænse på 20 tegn tilhøre en streng, der er 20 kodepunkter, 30 UTF-16-enheder og 47 bytes lang.

Tallet i grafemkolonnen er det, et menneske tæller. Forskellen mellem det og antallet af kodepunkter er netop det, scanneren handler om: et navn bestående af 12 kodepunkter kan vises som 12 bogstaver, som 3 bogstaver med 9 usynlige mærker imellem, eller som én emoji.

Tegnene, man ikke kan se

Et nul-bredde-mellemrum optager ingen bredde, ændrer intet glyf og er et rigtigt tegn med et rigtigt kodepunkt. To identifikatorer, der ser ens ud på skærmen, kan adskille sig med ét af dem, og det er sådan, et navn på en liste kan være en fup, der sorteres, søges og autentificeres anderledes end navnet ved siden af. Det samme gælder de bidirektionelle styreelementer, der kan få en streng til at blive vist i en anden rækkefølge end den, den blev skrevet i — et trick, der har været brugt til at skjule filnavne.

Scanneren i den tredje tilstand lister hver enkelt af dem med sin position: nul-bredde- og joiner-tegn, bidirektionelle styreelementer, variationsvælgere, usædvanlige mellemrum, C0- og C1-styretegn, kodepunkter til privat brug og en surrogat, der ikke er halvdelen af et gyldigt par, hvilket slet ikke er et tegn. Klasseoversigten højere oppe på siden forklarer, hvad hver klasse gør, og knappen Fjern fjerner en kopi af disse tegn uden at røre inputtet.

De fejl, siden er bygget til at fange
  • At sammenligne to strenge uden at normalisere dem. 'é' === 'e\u0301' er falsk. Normalisér begge sider med den samme form, før der sammenlignes, og brug den samme form i det indeks, der sammenlignes med.
  • At normalisere kun på den ene side. Et opslag, der normaliserer forespørgslen men ikke den gemte værdi, finder intet og ser ud som et dataproblem snarere end et normaliseringsproblem.
  • At bruge en kompatibilitetsform og kalde det NFC-sikkert. NFKC anvendt på en adgangskode formindsker mængden af mulige adgangskoder og sammenlægger input, der var tænkt som forskellige.
  • At antage, at en normaliseringsform retter en forfalskning. Den fjerner ikke en nul-bredde-joiner, en variationsvælger eller et homoglyf — et kyrillisk а og et latinsk a er forskellige kodepunkter, som NFC lader være urørt.
  • At stole på en tegntælling. Den tælling, brugeren ser, den tælling, en databasekolonne håndhæver, og den tælling, en bytegrænse håndhæver, er tre forskellige tal, og rækkerne ovenfor viser, hvor langt fra hinanden de kan ligge.
  • At antage, at man får NFC. Tekst, der kommer fra en inputmetode, en fil eller et andet system, er i den form, det system producerede. Siden starter på NFC, fordi det er den form, nettet forventes at bruge, ikke fordi det er den form, inputtet har.
Hurtige svar

“Hvorfor er disse to strenge ikke ens, når de ser ens ud?”

Fordi det ene er skrevet med et sammensat tegn og det andet med et grundbogstav plus et kombinerende mærke, eller fordi et af dem indeholder et tegn, der slet ikke tegner noget. Skift til sammenligningstilstanden og læg én streng i hver boks: siden rapporterer den første position, hvor de normaliserede strenge adskiller sig, og hvad der står på den position i hver af dem.

“Hvilken form skal jeg bruge i en applikation?”

NFC til alt, som et menneske vil læse, vise, gemme og søge i, hvilket er den form, W3C anbefaler til nettet. NFD er det rigtige svar, når koden bagved vil have grundbogstaver og adskilte mærker, hvilket er almindeligt i tekstbehandling og i nogle filsystemer på macOS. Brug NFKC eller NFKD kun der, hvor der matches frem for lagres: søgeindeks, nøgleordsfiltre, dubletgenkendelse.

“Er en streng, der allerede er normaliseret, stadig normaliseret, når jeg sammenkæder den med noget?”

Ikke nødvendigvis. Normalisering bevares ikke ved sammenkædning: at sammenføje to NFC-strenge kan give en sekvens, som NFC ville have skrevet anderledes, fordi et kombinerende mærke i begyndelsen af den anden streng kan hæfte sig på tegnet i slutningen af den første. Normalisér efter samling, ikke kun før.

“Fjerner normalisering de usynlige tegn?”

Nej, med én delvis undtagelse: NFKC og NFKD erstatter faktisk mellemrumstegnet uden brydning med et almindeligt mellemrum, hvilket er et synligt resultat af en kompatibilitetsfold. Alt andet — nul-bredde-mellemrum, joinere, bidirektionelle styreelementer, variationsvælgere — passerer uberørt gennem alle fire former. Det er det, den tredje tilstand findes til.

“Hvorfor ændrer antallet af bytes sig, når antallet af tegn ikke gør?”

Fordi de to former er bygget af forskellige kodepunkter, der koster forskelligt antal bytes i UTF-8. Et forsammensat é er to bytes; det dekomponerede par er tre. Tabellen med tællinger ovenfor udskriver alle fire tal side om side for inputtet og for hver form, så forskellen er synlig frem for beskrevet.

“Hvordan ved jeg, at resultaterne på denne side er rigtige?”

Tryk Selvtest. Den normaliserer alle tilfælde i sidens datafil, kontrollerer de fire former mod hinanden for de dokumenterede sammenhænge (NFC er idempotent, NFD af en NFC-streng er den samme som NFD af originalen, og så videre), kører alle tegn, der er nævnt i tabellerne for usynlige og styretegn, gennem scanneren, og tæller grafemtilfældene mod Intl.Segmenter.

Standarder og krediteringer

De fire former og deres definitioner stammer fra Unicode Standard, Annex 15 (Unicode Normalization Forms). Kodepunkterne, der bruges som dekompositionseksempler, er dem, standarden tildeler; siden implementerer ikke selv dekompositionstabellerne — den kalder browserens egen String.prototype.normalize, som er den samme implementering, platformen bruger til tekstinput, sortering og sammenligning, så det, der rapporteres her, er det, resten af systemet gør med den samme streng. Grafemklynger kommer fra Intl.Segmenter, hvor browseren har det, og fra en dokumenteret tilnærmelse, hvor den ikke har det.

Sidens tekst er skrevet til dette websted. Alle gennemregnede eksempler på siden er produceret af sidens egen kode og derefter kontrolleret mod datafilens registrerede tilfældeliste, som igen er kontrolleret mod Unicodes NormalizationTest-data for de samme tegn. Tabellerne over usynlige og farlige tegn er et kurateret udvalg, skrevet til læsbare advarsler, og ikke hele Unicodes sikkerhedsprofil (UTS #39); siden siger det og lader være med at antyde en fuldstændighed, den ikke har.