123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665 |
- {
- This file is part of the Free Pascal run time library.
- Copyright (c) 2015 by Jonas Maebe,
- member of the Free Pascal development team.
- CoreFoundation-based wide string support
- See the file COPYING.FPC, included in this distribution,
- for details about the copyright.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
- **********************************************************************}
- {$mode objfpc}
- {$implicitexceptions off}
- unit iosxwstr;
- interface
- {$linkframework CoreFoundation}
- procedure SetCFWidestringManager;
- implementation
- uses
- unixcp,
- { for access to libiconv-based routines }
- cwstring,
- MacTypes,
- CFBase,CFString,CFStringEncodingExt,CFLocale;
- procedure fpc_rangeerror; [external name 'FPC_RANGEERROR'];
- var
- CWStringWideStringManager: TUnicodeStringManager;
- procedure InitThread;
- begin
- { we don't need anything special, but since we may use cwstring itself,
- call through to it }
- CWStringWideStringManager.ThreadInitProc;
- end;
- procedure FiniThread;
- begin
- { we don't need anything special, but since we may use cwstring itself,
- call through to it }
- CWStringWideStringManager.ThreadFiniProc;
- end;
- function get_cfencoding_for_cp(cp: TSystemCodePage): CFStringEncoding;
- var
- defscp: TSystemCodePage;
- begin
- { translate placeholder code pages }
- if (cp=CP_ACP) or
- (cp=CP_OEMCP) then
- cp:=DefaultSystemCodePage;
- result:=CFStringConvertWindowsCodepageToEncoding(cp);
- end;
- procedure GetAnsiDataFromCFString(str: CFstringRef; cfcp: CFStringEncoding; estimated_length: SizeInt; var dest: RawByteString);
- var
- range: CFRange;
- encodedlen,convertedchars: CFIndex;
- strlen: SizeInt;
- begin
- { first rough estimate for the length }
- setlength(dest,estimated_length);
- { try to convert }
- range.location:=0;
- strlen:=CFStringGetLength(str);
- range.length:=strlen;
- convertedchars:=CFStringGetBytes(str,range,cfcp,ByteParameter('?'),false,UInt8Ptr(dest),estimated_length,encodedlen);
- { failed -> bail out }
- if convertedchars<0 then
- begin
- CFRelease(str);
- runerror(231);
- end
- { if partially succesful, recreate with the required len }
- else if convertedchars<strlen then
- begin
- setlength(dest,encodedlen);
- { try again }
- convertedchars:=CFStringGetBytes(str,range,cfcp,ByteParameter('?'),false,UInt8Ptr(dest),encodedlen,encodedlen);
- { failed again ? }
- if convertedchars<>strlen then
- begin
- CFRelease(str);
- runerror(231);
- end;
- end;
- { truncate }
- setlength(dest,encodedlen);
- end;
- function CFStringCreateFromAnsiData(data: pchar; len: SizeInt; cp: TSystemCodePage): CFStringRef;
- var
- strlen,encodedlen: CFIndex;
- range: CFRange;
- cfcp: CFStringEncoding;
- begin
- result:=nil;
- { get source cf codepage }
- cfcp:=get_cfencoding_for_cp(cp);
- { unsupported encoding -> try libiconv instead }
- if cfcp=kCFStringEncodingInvalidId then
- exit;
- { make a cfstring from the original data }
- result:=CFStringCreateWithBytesNoCopy(nil,UnivPtr(data),len,cfcp,false,kCFAllocatorNull);
- end;
- function CFStringCreateFromAnsiDataOptionallyViaUnicodeString(data: pchar; len: SizeInt; cp: TSystemCodePage; out wtemp: UnicodeString): CFStringRef;
- begin
- result:=CFStringCreateFromAnsiData(data,len,cp);
- { failed -> translate via libiconv and then create using the unicodestring
- characters; since we use the no-copy constructor for performance
- reasons, the unicodestring has to survive this routine }
- if not assigned(result) then
- begin
- CWStringWideStringManager.Ansi2UnicodeMoveProc(data,cp,wtemp,len);
- result:=CFStringCreateWithCharactersNoCopy(nil,UniCharPtr(wtemp),len,kCFAllocatorNull);
- end;
- end;
- function CFStringCreateFromWideData(data: pwidechar; len: SizeInt): CFStringRef; inline;
- begin
- { make a cfstring from the utf-16 data }
- result:=CFStringCreateWithCharactersNoCopy(nil,UniCharPtr(data),len,kCFAllocatorNull);
- end;
- function CFStringCreateFromWideDataOptionallyViaUUTF8String(data: pwidechar; len: SizeInt; out temp: RawByteString): CFStringRef;
- begin
- result:=CFStringCreateFromWideData(data,len);
- { failed -> translate to UTF-8 via libiconv to filter out any
- potentially invalid characters and then create using the unicodestring
- characters; since we use the no-copy constructor for performance
- reasons, the unicodestring has to survive this routine }
- if not assigned(result) then
- begin
- CWStringWideStringManager.Unicode2AnsiMoveProc(data,temp,CP_UTF8,len);
- result:=CFStringCreateWithBytesNoCopy(nil,UnivPtr(temp),length(temp),kCFStringEncodingUTF8,false,kCFAllocatorNull);
- if not assigned(result) then
- runerror(231)
- end;
- end;
- procedure Wide2AnsiMove(source:pwidechar; var dest:RawByteString; cp:TSystemCodePage; len:SizeInt);
- var
- str: CFStringRef;
- strlen,estimatedlen: CFIndex;
- cfcp: CFStringEncoding;
- begin
- str:=nil;
- { get target cf codepage }
- cfcp:=get_cfencoding_for_cp(cp);
- { unsupported encoding -> default move }
- if cfcp<>kCFStringEncodingInvalidId then
- { make a cfstring from the utf-16 data }
- str:=CFStringCreateFromWideData(source,len);
- { You cannot create a CFString for a sequence that contains an error :/
- We want to replace the error positions with '?' -> fall back to libiconv
- }
- if not assigned(str) then
- begin
- CWStringWideStringManager.Unicode2AnsiMoveProc(source,dest,cp,len);
- exit;
- end;
- GetAnsiDataFromCFString(str,cfcp,len*3,dest);
- { set code page }
- SetCodePage(dest,cp,false);
- { release cfstring }
- CFRelease(str);
- end;
- procedure Ansi2WideMove(source:pchar; cp:TSystemCodePage; var dest:widestring; len:SizeInt);
- var
- str: CFStringRef;
- strlen,encodedlen: CFIndex;
- range: CFRange;
- cfcp: CFStringEncoding;
- begin
- str:=CFStringCreateFromAnsiData(source,len,cp);
- { You cannot create a CFString for a sequence that contains an error :/
- We want to replace the error positions with '?' -> fall back to libiconv
- }
- if not assigned(str) then
- begin
- CWStringWideStringManager.Ansi2UnicodeMoveProc(source,cp,dest,len);
- exit;
- end;
- { convert }
- range.location:=0;
- strlen:=CFStringGetLength(str);
- range.length:=strlen;
- setlength(dest,strlen);
- CFStringGetCharacters(str,range,UniCharPtr(dest));
- { release cfstring }
- CFRelease(str);
- end;
- function LowerWideString(const s : WideString) : WideString;
- var
- str: CFStringRef;
- mstr: CFMutableStringRef;
- range: CFRange;
- encodedlen: CFIndex;
- locale: CFLocaleRef;
- temp: RawByteString;
- begin
- { empty string -> exit }
- if s='' then
- begin
- result:='';
- exit;
- end;
- { create cfstring from the string data }
- str:=CFStringCreateFromWideDataOptionallyViaUUTF8String(pwidechar(s),length(s),temp);
- { convert to mutable cfstring }
- mstr:=CFStringCreateMutableCopy(nil,0,str);
- { lowercase }
- locale:=CFLocaleCopyCurrent;
- CFStringLowercase(mstr,locale);
- CFRelease(locale);
- { extract the data again }
- range.location:=0;
- range.length:=CFStringGetLength(CFStringRef(mstr));
- setlength(result,range.length);
- CFStringGetCharacters(mstr,range,UniCharPtr(result));
- CFRelease(mstr);
- CFRelease(str);
- end;
- function UpperWideString(const s : WideString) : WideString;
- var
- str: CFStringRef;
- mstr: CFMutableStringRef;
- range: CFRange;
- encodedlen: CFIndex;
- locale: CFLocaleRef;
- temp: RawByteString;
- begin
- { empty string -> exit }
- if s='' then
- begin
- result:='';
- exit;
- end;
- { create cfstring from the string data }
- str:=CFStringCreateFromWideDataOptionallyViaUUTF8String(pwidechar(s),length(s),temp);
- { convert to mutable cfstring }
- mstr:=CFStringCreateMutableCopy(nil,0,str);
- { lowercase }
- locale:=CFLocaleCopyCurrent;
- CFStringUppercase(mstr,locale);
- CFRelease(locale);
- { extract the data again }
- range.location:=0;
- range.length:=CFStringGetLength(CFStringRef(mstr));
- setlength(result,range.length);
- CFStringGetCharacters(mstr,range,UniCharPtr(result));
- CFRelease(mstr);
- CFRelease(str);
- end;
- function UpperLowerAnsiString(const s: AnsiString; upper: boolean): AnsiString;
- var
- str: CFStringRef;
- mstr: CFMutableStringRef;
- cfcp: CFStringEncoding;
- locale: CFLocaleRef;
- wtemp: UnicodeString;
- range: CFRange;
- begin
- if s='' then
- begin
- result:='';
- exit
- end;
- str:=CFStringCreateFromAnsiDataOptionallyViaUnicodeString(pchar(s),length(s),StringCodePage(s),wtemp);
- { unsupported encoding for either CF or iconv -> return original string }
- if not assigned(str) then
- begin
- result:=s;
- exit;
- end;
- { convert to mutable cfstring }
- mstr:=CFStringCreateMutableCopy(nil,0,str);
- CFRelease(str);
- { upper/lowercase }
- locale:=CFLocaleCopyCurrent;
- if upper then
- CFStringUppercase(mstr,locale)
- else
- CFStringLowercase(mstr,locale);
- CFRelease(locale);
- { convert back to ansistring }
- cfcp:=get_cfencoding_for_cp(StringCodePage(s));
- if cfcp<>kCFStringEncodingInvalidId then
- begin
- GetAnsiDataFromCFString(CFStringRef(mstr),cfcp,length(s),RawByteString(result));
- SetCodePage(RawByteString(result),StringCodePage(s),false);
- end
- else
- begin
- { unsupported encoding -> use libiconv instead via UTF-16
- intermediate }
- range.location:=0;
- range.length:=CFStringGetLength(mstr);
- SetLength(wtemp,range.length);
- CFStringGetCharacters(mstr,range,UniCharPtr(wtemp));
- CWStringWideStringManager.Wide2AnsiMoveProc(pwidechar(wtemp),RawByteString(result),StringCodePage(s),range.length);
- end;
- CFRelease(mstr);
- end;
- function LowerAnsiString(const s: AnsiString): AnsiString;
- begin
- result:=UpperLowerAnsiString(s,false);
- end;
- function UpperAnsiString(const s: AnsiString): AnsiString;
- begin
- result:=UpperLowerAnsiString(s,true);
- end;
- function CompareCFStrings(const s1, s2: CFStringRef; case_insensitive: boolean): longint;
- var
- flags: CFStringCompareFlags;
- begin
- flags:=0;
- if case_insensitive then
- flags:=flags or kCFCompareCaseInsensitive;
- result:=CFStringCompare(s1,s2,flags)
- end;
- function CompareWideString(const s1, s2 : WideString) : PtrInt;
- var
- cfstr1, cfstr2: CFStringRef;
- temp1, temp2: RawByteString;
- begin
- cfstr1:=CFStringCreateFromWideDataOptionallyViaUUTF8String(pwidechar(s1),length(s1),temp1);
- cfstr2:=CFStringCreateFromWideDataOptionallyViaUUTF8String(pwidechar(s2),length(s2),temp2);
- result:=CompareCFStrings(cfstr1,cfstr2,false);
- CFRelease(cfstr1);
- CFRelease(cfstr2);
- end;
- function CompareTextWideString(const s1, s2 : WideString): PtrInt;
- var
- cfstr1, cfstr2: CFStringRef;
- temp1, temp2: RawByteString;
- begin
- cfstr1:=CFStringCreateFromWideDataOptionallyViaUUTF8String(pwidechar(s1),length(s1),temp1);
- cfstr2:=CFStringCreateFromWideDataOptionallyViaUUTF8String(pwidechar(s2),length(s2),temp2);
- result:=CompareCFStrings(cfstr1,cfstr2,true);
- CFRelease(cfstr1);
- CFRelease(cfstr2);
- end;
- function InternalCodePointLength(const Str: PChar; cfcp: CFStringEncoding; maxlookahead: ptrint): PtrInt;
- var
- cfstr: CFStringRef;
- begin
- result:=0;
- { try creating a string with the first 1, 2, ... bytes until we find a
- valid one }
- while (str[result]<>#0) and
- (result<maxlookahead) do
- begin
- inc(result);
- cfstr:=CFStringCreateWithBytesNoCopy(nil,UnivPtr(Str),result,cfcp,false,kCFAllocatorNull);
- if assigned(cfstr) then
- begin
- CFRelease(cfstr);
- exit;
- end;
- end;
- result:=-1;
- end;
- function CharLengthPChar(const Str: PChar): PtrInt;
- var
- cfstr: CFStringRef;
- cfcp: CFStringEncoding;
- s: PChar;
- tmplen: PtrInt;
- begin
- result:=0;
- if str[0]=#0 then
- exit;
- cfcp:=get_cfencoding_for_cp(DefaultSystemCodePage);
- if cfcp=kCFStringEncodingInvalidId then
- begin
- { or -1? }
- result:=strlen(Str);
- exit
- end;
- s:=str;
- repeat
- tmplen:=InternalCodePointLength(s,cfcp,8);
- { invalid -> skip }
- if tmplen=-1 then
- tmplen:=1;
- inc(s,tmplen);
- inc(result);
- until s[0]=#0;
- end;
- function CodePointLength(const Str: PChar; maxlookahead: ptrint): PtrInt;
- var
- cfstr: CFStringRef;
- cfcp: CFStringEncoding;
- begin
- result:=0;
- if str[0]=#0 then
- exit;
- cfcp:=get_cfencoding_for_cp(DefaultSystemCodePage);
- if cfcp=kCFStringEncodingInvalidId then
- begin
- { if we would return -1, then the caller would keep trying with
- longer and longer sequences, but that wouldn't change anything }
- result:=1;
- exit
- end;
- result:=InternalCodePointLength(str,cfcp,maxlookahead);
- end;
- function CompareStrAnsiString(const s1, s2: ansistring): PtrInt;
- var
- cfstr1, cfstr2: CFStringRef;
- wtemp1, wtemp2: UnicodeString;
- begin
- cfstr1:=CFStringCreateFromAnsiDataOptionallyViaUnicodeString(pchar(s1),length(s1),StringCodePage(s1),wtemp1);
- cfstr2:=CFStringCreateFromAnsiDataOptionallyViaUnicodeString(pchar(s2),length(s2),StringCodePage(s2),wtemp2);
- result:=CompareCFStrings(cfstr1,cfstr2,false);
- CFRelease(cfstr1);
- CFRelease(cfstr2);
- end;
- function StrCompAnsi(s1,s2 : PChar): PtrInt;
- var
- cfstr1, cfstr2: CFStringRef;
- wtemp1, wtemp2: UnicodeString;
- begin
- cfstr1:=CFStringCreateFromAnsiDataOptionallyViaUnicodeString(s1,strlen(s1),DefaultSystemCodePage,wtemp1);
- cfstr2:=CFStringCreateFromAnsiDataOptionallyViaUnicodeString(s2,strlen(s2),DefaultSystemCodePage,wtemp2);
- result:=CompareCFStrings(cfstr1,cfstr2,false);
- CFRelease(cfstr1);
- CFRelease(cfstr2);
- end;
- function AnsiCompareText(const S1, S2: ansistring): PtrInt;
- var
- cfstr1, cfstr2: CFStringRef;
- wtemp1, wtemp2: UnicodeString;
- begin
- cfstr1:=CFStringCreateFromAnsiDataOptionallyViaUnicodeString(pchar(s1),length(s1),DefaultSystemCodePage,wtemp1);
- cfstr2:=CFStringCreateFromAnsiDataOptionallyViaUnicodeString(pchar(s2),length(s2),DefaultSystemCodePage,wtemp2);
- result:=CompareCFStrings(cfstr1,cfstr2,true);
- CFRelease(cfstr1);
- CFRelease(cfstr2);
- end;
- function AnsiStrIComp(S1, S2: PChar): PtrInt;
- var
- cfstr1, cfstr2: CFStringRef;
- wtemp1, wtemp2: UnicodeString;
- begin
- cfstr1:=CFStringCreateFromAnsiDataOptionallyViaUnicodeString(s1,strlen(s1),DefaultSystemCodePage,wtemp1);
- cfstr2:=CFStringCreateFromAnsiDataOptionallyViaUnicodeString(s2,strlen(s2),DefaultSystemCodePage,wtemp2);
- result:=CompareCFStrings(cfstr1,cfstr2,true);
- CFRelease(cfstr1);
- CFRelease(cfstr2);
- end;
- function AnsiStrLComp(S1, S2: PChar; MaxLen: PtrUInt): PtrInt;
- var
- cfstr1, cfstr2: CFStringRef;
- wtemp1, wtemp2: UnicodeString;
- begin
- cfstr1:=CFStringCreateFromAnsiDataOptionallyViaUnicodeString(s1,MaxLen,StringCodePage(s1),wtemp1);
- cfstr2:=CFStringCreateFromAnsiDataOptionallyViaUnicodeString(s2,MaxLen,StringCodePage(s2),wtemp2);
- result:=CompareCFStrings(cfstr1,cfstr2,false);
- CFRelease(cfstr1);
- CFRelease(cfstr2);
- end;
- function AnsiStrLIComp(S1, S2: PChar; MaxLen: PtrUInt): PtrInt;
- var
- cfstr1, cfstr2: CFStringRef;
- wtemp1, wtemp2: UnicodeString;
- begin
- cfstr1:=CFStringCreateFromAnsiDataOptionallyViaUnicodeString(s1,MaxLen,StringCodePage(s1),wtemp1);
- cfstr2:=CFStringCreateFromAnsiDataOptionallyViaUnicodeString(s2,MaxLen,StringCodePage(s2),wtemp2);
- result:=CompareCFStrings(cfstr1,cfstr2,true);
- CFRelease(cfstr1);
- CFRelease(cfstr2);
- end;
- procedure ansi2pchar(const s: ansistring; const orgp: pchar; out p: pchar);
- var
- newlen: sizeint;
- begin
- newlen:=length(s);
- if newlen>strlen(orgp) then
- fpc_rangeerror;
- p:=orgp;
- if (newlen>0) then
- move(s[1],p[0],newlen);
- p[newlen]:=#0;
- end;
- function AnsiStrLower(Str: PChar): PChar;
- var
- temp: ansistring;
- begin
- temp:=loweransistring(str);
- ansi2pchar(temp,str,result);
- end;
- function AnsiStrUpper(Str: PChar): PChar;
- var
- temp: ansistring;
- begin
- temp:=upperansistring(str);
- ansi2pchar(temp,str,result);
- end;
- function GetStandardCodePage(const stdcp: TStandardCodePageEnum): TSystemCodePage;
- var
- langinfo: pchar;
- begin
- { don't use CFStringGetSystemEncoding, that one returns MacRoman on e.g.
- an English system, which is definitely not what we want. Since there are
- no "ansi" interfaces on OS X and all APIs support all characters, always
- use UTF-8. Additionally, Darwin always uses UTF-8 for file system
- operations }
- result:=CP_UTF8;
- end;
- procedure SetStdIOCodePage(var T: Text); inline;
- begin
- case TextRec(T).Mode of
- fmInput:TextRec(T).CodePage:=GetStandardCodePage(scpConsoleInput);
- fmOutput:TextRec(T).CodePage:=GetStandardCodePage(scpConsoleOutput);
- end;
- end;
- procedure SetStdIOCodePages; inline;
- begin
- SetStdIOCodePage(Input);
- SetStdIOCodePage(Output);
- SetStdIOCodePage(ErrOutput);
- SetStdIOCodePage(StdOut);
- SetStdIOCodePage(StdErr);
- end;
- procedure SetCFWideStringManager;
- var
- CFWideStringManager : TUnicodeStringManager;
- begin
- GetUnicodeStringManager(CWStringWideStringManager);
- CFWideStringManager:=CWStringWideStringManager;
- with CFWideStringManager do
- begin
- Wide2AnsiMoveProc:=@Wide2AnsiMove;
- Ansi2WideMoveProc:=@Ansi2WideMove;
- UpperWideStringProc:=@UpperWideString;
- LowerWideStringProc:=@LowerWideString;
- CompareWideStringProc:=@CompareWideString;
- CompareTextWideStringProc:=@CompareTextWideString;
- CharLengthPCharProc:=@CharLengthPChar;
- CodePointLengthProc:=@CodePointLength;
- UpperAnsiStringProc:=@UpperAnsiString;
- LowerAnsiStringProc:=@LowerAnsiString;
- CompareStrAnsiStringProc:=@CompareStrAnsiString;
- CompareTextAnsiStringProc:=@AnsiCompareText;
- StrCompAnsiStringProc:=@StrCompAnsi;
- StrICompAnsiStringProc:=@AnsiStrIComp;
- StrLCompAnsiStringProc:=@AnsiStrLComp;
- StrLICompAnsiStringProc:=@AnsiStrLIComp;
- StrLowerAnsiStringProc:=@AnsiStrLower;
- StrUpperAnsiStringProc:=@AnsiStrUpper;
- ThreadInitProc:=@InitThread;
- ThreadFiniProc:=@FiniThread;
- { Unicode }
- Unicode2AnsiMoveProc:=@Wide2AnsiMove;
- Ansi2UnicodeMoveProc:=@Ansi2WideMove;
- UpperUnicodeStringProc:=@UpperWideString;
- LowerUnicodeStringProc:=@LowerWideString;
- CompareUnicodeStringProc:=@CompareWideString;
- CompareTextUnicodeStringProc:=@CompareTextWideString;
- { CodePage }
- GetStandardCodePageProc:=@GetStandardCodePage;
- end;
- SetUnicodeStringManager(CFWideStringManager);
- end;
- initialization
- SetCFWideStringManager;
- { set the DefaultSystemCodePage }
- DefaultSystemCodePage:=GetStandardCodePage(scpAnsi);
- DefaultFileSystemCodePage:=GetStandardCodePage(scpFileSystemSingleByte);
- DefaultRTLFileSystemCodePage:=DefaultFileSystemCodePage;
- SetStdIOCodePages;
- { don't call init, we don't need to do anything and the cwstring routine we
- call through has already been called from the init code of cwstring itself
- InitThread;
- }
- finalization
- { don't call for the same reason as not calling FiniThread
- FiniThread;
- }
- { restore previous widestring manager so that subsequent calls
- into the widestring manager won't trigger the finalized functionality }
- SetWideStringManager(CWStringWideStringManager);
- end.
|