mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-11-03 21:58:59 +00:00
Partial implementation, gaps and impossibilities noted internally as TODO comments. * CPProgress.j: Introduce CPProgress, modelled on NSProgress. - Count units of work, with fraction completed, finished and indeterminate states. - Compose progress trees through explicit children and the current progress object. - Cancel, pause and resume, with handlers, propagating to children. - Announce unit-count changes through key-value observing across the tree. * CPError.j: Add CPUserCancelledError. * CPItemProvider.j: - Return a CPProgress from each load, counting bytes read for files and aborting the read on cancellation. - Pass file contents to CPData as a binary string rather than a byte array. - Implement -canLoadObjectOfClass: and -loadObjectOfClass:completionHandler:. - Add the CPString (CPItemProvider) reading category. * CPString+Encoding.j: Decode data held as a binary string through a Uint8Array, without building a byte array. * Foundation.j: Import CPProgress.j. * CPProgressTest.j: Add unit tests covering counts, children, the current progress object, cancellation, pausing, observation and user information. * CPItemProviderTest.j: Add unit tests covering read progress, cancellation, completion and string loading.
1076 lines
35 KiB
Plaintext
1076 lines
35 KiB
Plaintext
/*
|
|
* CPString+Encoding.j
|
|
* Foundation
|
|
*
|
|
* Created by David Richardson on 2026-11-02.
|
|
* Copyright David Richardson 2026.
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library 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. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
@import "CPArray.j"
|
|
@import "CPData.j"
|
|
@import "CPDictionary.j"
|
|
@import "CPString.j"
|
|
|
|
@typedef CPStringEncoding
|
|
|
|
@global TextDecoder
|
|
|
|
/*
|
|
String encodings. The values are Cocoa's, so that archived values and values exchanged with
|
|
Cocoa applications agree.
|
|
*/
|
|
CPASCIIStringEncoding = 1;
|
|
CPNEXTSTEPStringEncoding = 2;
|
|
CPJapaneseEUCStringEncoding = 3;
|
|
CPUTF8StringEncoding = 4;
|
|
CPISOLatin1StringEncoding = 5;
|
|
CPSymbolStringEncoding = 6;
|
|
CPNonLossyASCIIStringEncoding = 7;
|
|
CPShiftJISStringEncoding = 8;
|
|
CPISOLatin2StringEncoding = 9;
|
|
CPUnicodeStringEncoding = 10;
|
|
CPWindowsCP1251StringEncoding = 11;
|
|
CPWindowsCP1252StringEncoding = 12;
|
|
CPWindowsCP1253StringEncoding = 13;
|
|
CPWindowsCP1254StringEncoding = 14;
|
|
CPWindowsCP1250StringEncoding = 15;
|
|
CPISO2022JPStringEncoding = 21;
|
|
CPMacOSRomanStringEncoding = 30;
|
|
CPUTF16StringEncoding = CPUnicodeStringEncoding;
|
|
CPUTF16BigEndianStringEncoding = 0x90000100;
|
|
CPUTF16LittleEndianStringEncoding = 0x94000100;
|
|
CPUTF32StringEncoding = 0x8c000100;
|
|
CPUTF32BigEndianStringEncoding = 0x98000100;
|
|
CPUTF32LittleEndianStringEncoding = 0x9c000100;
|
|
|
|
/*
|
|
TODO: Cocoa defines three more detection option keys, which can be supported:
|
|
CPStringEncodingDetectionFromWindowsKey, to prefer the Windows code pages;
|
|
CPStringEncodingDetectionLossySubstitutionKey, the text that replaces bytes which cannot be
|
|
decoded; and CPStringEncodingDetectionLikelyLanguageKey, a language that orders the candidate
|
|
encodings.
|
|
*/
|
|
/*
|
|
Keys for the options of +stringEncodingForData:encodingOptions:convertedString:usedLossyConversion:.
|
|
*/
|
|
CPStringEncodingDetectionSuggestedEncodingsKey = @"CPStringEncodingDetectionSuggestedEncodingsKey";
|
|
CPStringEncodingDetectionUseOnlySuggestedEncodingsKey = @"CPStringEncodingDetectionUseOnlySuggestedEncodingsKey";
|
|
CPStringEncodingDetectionAllowLossyKey = @"CPStringEncodingDetectionAllowLossyKey";
|
|
|
|
var CPStringEncodingChunkSize = 8192,
|
|
CPStringEncodingReplacementCharacter = 0xFFFD,
|
|
CPStringEncodingSingleByteMaps = {};
|
|
|
|
// MARK: Decoding
|
|
|
|
/*
|
|
Returns a string made from an array of UTF-16 code units. The array is converted in chunks,
|
|
because a very long argument list exceeds the JavaScript engine's limit.
|
|
*/
|
|
var CPStringEncodingStringFromCodeUnits = function(codeUnits)
|
|
{
|
|
var output = "";
|
|
|
|
for (var i = 0, count = codeUnits.length; i < count; i += CPStringEncodingChunkSize)
|
|
output += String.fromCharCode.apply(null, codeUnits.slice(i, i + CPStringEncodingChunkSize));
|
|
|
|
return output;
|
|
};
|
|
|
|
/*
|
|
Adds a code point to an array of UTF-16 code units, as a surrogate pair when needed.
|
|
*/
|
|
var CPStringEncodingPushCodePoint = function(codeUnits, codePoint)
|
|
{
|
|
if (codePoint < 0x10000)
|
|
codeUnits.push(codePoint);
|
|
else
|
|
{
|
|
codePoint -= 0x10000;
|
|
codeUnits.push(0xD800 + (codePoint >> 10), 0xDC00 + (codePoint & 0x3FF));
|
|
}
|
|
};
|
|
|
|
/*
|
|
Decodes bytes with the browser's TextDecoder, which knows the encodings of the WHATWG Encoding
|
|
Standard. Returns nil when the bytes are not valid in the encoding, or when there is no
|
|
TextDecoder. With isLossy, invalid bytes become U+FFFD instead.
|
|
*/
|
|
var CPStringEncodingDecodeWithTextDecoder = function(aLabel, bytes, isLossy)
|
|
{
|
|
if (typeof TextDecoder === "undefined")
|
|
return nil;
|
|
|
|
try
|
|
{
|
|
return new TextDecoder(aLabel, { fatal: !isLossy }).decode(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
|
|
}
|
|
catch (anException)
|
|
{
|
|
return nil;
|
|
}
|
|
};
|
|
|
|
/*
|
|
Decodes ISO Latin 1, in which each byte is the code point of the same value. The browser's
|
|
TextDecoder cannot be used: the Encoding Standard treats ISO Latin 1 as Windows-1252.
|
|
*/
|
|
var CPStringEncodingDecodeLatin1 = function(bytes)
|
|
{
|
|
return CPStringEncodingStringFromCodeUnits(bytes);
|
|
};
|
|
|
|
var CPStringEncodingDecodeASCII = function(bytes, isLossy)
|
|
{
|
|
for (var i = 0, count = bytes.length; i < count; i++)
|
|
if (bytes[i] > 0x7F)
|
|
{
|
|
if (!isLossy)
|
|
return nil;
|
|
|
|
bytes = Array.prototype.map.call(bytes, function(aByte) { return aByte > 0x7F ? CPStringEncodingReplacementCharacter : aByte; });
|
|
break;
|
|
}
|
|
|
|
return CPStringEncodingStringFromCodeUnits(bytes);
|
|
};
|
|
|
|
/*
|
|
Decodes UTF-16. A byte order mark, when allowed and present, sets the byte order and is
|
|
removed. Unpaired surrogates are invalid.
|
|
*/
|
|
var CPStringEncodingDecodeUTF16 = function(bytes, isLittleEndian, readsByteOrderMark, isLossy)
|
|
{
|
|
var start = 0,
|
|
count = bytes.length;
|
|
|
|
if (count % 2 && !isLossy)
|
|
return nil;
|
|
|
|
if (readsByteOrderMark && count >= 2)
|
|
{
|
|
if (bytes[0] === 0xFE && bytes[1] === 0xFF)
|
|
{
|
|
isLittleEndian = NO;
|
|
start = 2;
|
|
}
|
|
else if (bytes[0] === 0xFF && bytes[1] === 0xFE)
|
|
{
|
|
isLittleEndian = YES;
|
|
start = 2;
|
|
}
|
|
}
|
|
|
|
var codeUnits = [];
|
|
|
|
for (var i = start; i + 1 < count; i += 2)
|
|
codeUnits.push(isLittleEndian ? bytes[i] | (bytes[i + 1] << 8) : (bytes[i] << 8) | bytes[i + 1]);
|
|
|
|
for (var i = 0, unitCount = codeUnits.length; i < unitCount; i++)
|
|
{
|
|
var unit = codeUnits[i];
|
|
|
|
if (unit >= 0xD800 && unit <= 0xDBFF && i + 1 < unitCount && codeUnits[i + 1] >= 0xDC00 && codeUnits[i + 1] <= 0xDFFF)
|
|
i++;
|
|
else if (unit >= 0xD800 && unit <= 0xDFFF)
|
|
{
|
|
if (!isLossy)
|
|
return nil;
|
|
|
|
codeUnits[i] = CPStringEncodingReplacementCharacter;
|
|
}
|
|
}
|
|
|
|
if (count % 2)
|
|
codeUnits.push(CPStringEncodingReplacementCharacter);
|
|
|
|
return CPStringEncodingStringFromCodeUnits(codeUnits);
|
|
};
|
|
|
|
/*
|
|
Decodes UTF-32, which the browser's TextDecoder does not know. A byte order mark, when
|
|
allowed and present, sets the byte order and is removed. Code points above U+10FFFF and
|
|
surrogate code points are invalid.
|
|
*/
|
|
var CPStringEncodingDecodeUTF32 = function(bytes, isLittleEndian, readsByteOrderMark, isLossy)
|
|
{
|
|
var start = 0,
|
|
count = bytes.length;
|
|
|
|
if (count % 4 && !isLossy)
|
|
return nil;
|
|
|
|
if (readsByteOrderMark && count >= 4)
|
|
{
|
|
if (bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0xFE && bytes[3] === 0xFF)
|
|
{
|
|
isLittleEndian = NO;
|
|
start = 4;
|
|
}
|
|
else if (bytes[0] === 0xFF && bytes[1] === 0xFE && bytes[2] === 0x00 && bytes[3] === 0x00)
|
|
{
|
|
isLittleEndian = YES;
|
|
start = 4;
|
|
}
|
|
}
|
|
|
|
var codeUnits = [];
|
|
|
|
for (var i = start; i + 3 < count; i += 4)
|
|
{
|
|
var codePoint = isLittleEndian ?
|
|
(bytes[i] | (bytes[i + 1] << 8) | (bytes[i + 2] << 16) | (bytes[i + 3] << 24)) >>> 0 :
|
|
((bytes[i] << 24) | (bytes[i + 1] << 16) | (bytes[i + 2] << 8) | bytes[i + 3]) >>> 0;
|
|
|
|
if (codePoint > 0x10FFFF || (codePoint >= 0xD800 && codePoint <= 0xDFFF))
|
|
{
|
|
if (!isLossy)
|
|
return nil;
|
|
|
|
codePoint = CPStringEncodingReplacementCharacter;
|
|
}
|
|
|
|
CPStringEncodingPushCodePoint(codeUnits, codePoint);
|
|
}
|
|
|
|
if (count % 4)
|
|
codeUnits.push(CPStringEncodingReplacementCharacter);
|
|
|
|
return CPStringEncodingStringFromCodeUnits(codeUnits);
|
|
};
|
|
|
|
/*
|
|
Decodes non-lossy ASCII: 7-bit ASCII in which a backslash starts an escape. "\\" is a
|
|
backslash, "\uXXXX" is a UTF-16 code unit in hexadecimal, and "\ddd" is a code unit up to
|
|
0xFF in octal.
|
|
*/
|
|
var CPStringEncodingDecodeNonLossyASCII = function(bytes, isLossy)
|
|
{
|
|
var codeUnits = [];
|
|
|
|
for (var i = 0, count = bytes.length; i < count; i++)
|
|
{
|
|
var aByte = bytes[i];
|
|
|
|
if (aByte > 0x7F)
|
|
{
|
|
if (!isLossy)
|
|
return nil;
|
|
|
|
codeUnits.push(CPStringEncodingReplacementCharacter);
|
|
continue;
|
|
}
|
|
|
|
if (aByte !== 0x5C)
|
|
{
|
|
codeUnits.push(aByte);
|
|
continue;
|
|
}
|
|
|
|
var rest = String.fromCharCode.apply(null, bytes.slice(i + 1, i + 6)),
|
|
match = nil;
|
|
|
|
if (rest.charAt(0) === "\\")
|
|
{
|
|
codeUnits.push(0x5C);
|
|
i += 1;
|
|
}
|
|
else if ((match = /^u([0-9A-Fa-f]{4})/.exec(rest)))
|
|
{
|
|
codeUnits.push(parseInt(match[1], 16));
|
|
i += 5;
|
|
}
|
|
else if ((match = /^([0-3][0-7]{2})/.exec(rest)))
|
|
{
|
|
codeUnits.push(parseInt(match[1], 8));
|
|
i += 3;
|
|
}
|
|
else if (isLossy)
|
|
codeUnits.push(0x5C);
|
|
else
|
|
return nil;
|
|
}
|
|
|
|
return CPStringEncodingStringFromCodeUnits(codeUnits);
|
|
};
|
|
|
|
// MARK: Encoding
|
|
|
|
/*
|
|
Calls aFunction with each code point of a string. An unpaired surrogate is passed as -1.
|
|
*/
|
|
var CPStringEncodingForEachCodePoint = function(aString, aFunction)
|
|
{
|
|
for (var i = 0, count = aString.length; i < count; i++)
|
|
{
|
|
var unit = aString.charCodeAt(i);
|
|
|
|
if (unit >= 0xD800 && unit <= 0xDBFF && i + 1 < count)
|
|
{
|
|
var next = aString.charCodeAt(i + 1);
|
|
|
|
if (next >= 0xDC00 && next <= 0xDFFF)
|
|
{
|
|
aFunction(0x10000 + ((unit - 0xD800) << 10) + (next - 0xDC00));
|
|
i++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
aFunction(unit >= 0xD800 && unit <= 0xDFFF ? -1 : unit);
|
|
}
|
|
};
|
|
|
|
/*
|
|
Encodes UTF-8. An unpaired surrogate cannot be encoded: it makes the result nil, or with
|
|
isLossy becomes U+FFFD.
|
|
*/
|
|
var CPStringEncodingEncodeUTF8 = function(aString, isLossy)
|
|
{
|
|
var bytes = [],
|
|
failed = NO;
|
|
|
|
CPStringEncodingForEachCodePoint(aString, function(codePoint)
|
|
{
|
|
if (codePoint < 0)
|
|
{
|
|
if (!isLossy)
|
|
failed = YES;
|
|
|
|
codePoint = CPStringEncodingReplacementCharacter;
|
|
}
|
|
|
|
if (codePoint < 0x80)
|
|
bytes.push(codePoint);
|
|
else if (codePoint < 0x800)
|
|
bytes.push(0xC0 | (codePoint >> 6), 0x80 | (codePoint & 0x3F));
|
|
else if (codePoint < 0x10000)
|
|
bytes.push(0xE0 | (codePoint >> 12), 0x80 | ((codePoint >> 6) & 0x3F), 0x80 | (codePoint & 0x3F));
|
|
else
|
|
bytes.push(0xF0 | (codePoint >> 18), 0x80 | ((codePoint >> 12) & 0x3F), 0x80 | ((codePoint >> 6) & 0x3F), 0x80 | (codePoint & 0x3F));
|
|
});
|
|
|
|
return failed ? nil : bytes;
|
|
};
|
|
|
|
var CPStringEncodingEncodeUTF16 = function(aString, isLittleEndian, writesByteOrderMark, isLossy)
|
|
{
|
|
var bytes = writesByteOrderMark ? (isLittleEndian ? [0xFF, 0xFE] : [0xFE, 0xFF]) : [],
|
|
failed = NO,
|
|
pushUnit = function(unit)
|
|
{
|
|
if (isLittleEndian)
|
|
bytes.push(unit & 0xFF, unit >> 8);
|
|
else
|
|
bytes.push(unit >> 8, unit & 0xFF);
|
|
};
|
|
|
|
CPStringEncodingForEachCodePoint(aString, function(codePoint)
|
|
{
|
|
if (codePoint < 0)
|
|
{
|
|
if (!isLossy)
|
|
failed = YES;
|
|
|
|
codePoint = CPStringEncodingReplacementCharacter;
|
|
}
|
|
|
|
if (codePoint < 0x10000)
|
|
pushUnit(codePoint);
|
|
else
|
|
{
|
|
pushUnit(0xD800 + ((codePoint - 0x10000) >> 10));
|
|
pushUnit(0xDC00 + ((codePoint - 0x10000) & 0x3FF));
|
|
}
|
|
});
|
|
|
|
return failed ? nil : bytes;
|
|
};
|
|
|
|
var CPStringEncodingEncodeUTF32 = function(aString, isLittleEndian, writesByteOrderMark, isLossy)
|
|
{
|
|
var bytes = writesByteOrderMark ? (isLittleEndian ? [0xFF, 0xFE, 0x00, 0x00] : [0x00, 0x00, 0xFE, 0xFF]) : [],
|
|
failed = NO;
|
|
|
|
CPStringEncodingForEachCodePoint(aString, function(codePoint)
|
|
{
|
|
if (codePoint < 0)
|
|
{
|
|
if (!isLossy)
|
|
failed = YES;
|
|
|
|
codePoint = CPStringEncodingReplacementCharacter;
|
|
}
|
|
|
|
if (isLittleEndian)
|
|
bytes.push(codePoint & 0xFF, (codePoint >> 8) & 0xFF, (codePoint >> 16) & 0xFF, 0x00);
|
|
else
|
|
bytes.push(0x00, (codePoint >> 16) & 0xFF, (codePoint >> 8) & 0xFF, codePoint & 0xFF);
|
|
});
|
|
|
|
return failed ? nil : bytes;
|
|
};
|
|
|
|
/*
|
|
TODO: Check the format against Cocoa's output. Cocoa may write characters from U+0080 to
|
|
U+00FF as octal escapes rather than "\uXXXX". Decoding accepts both forms.
|
|
*/
|
|
/*
|
|
Encodes non-lossy ASCII: ASCII characters stay as they are, a backslash becomes "\\", and
|
|
every other UTF-16 code unit becomes "\uXXXX". Every string can be encoded.
|
|
*/
|
|
var CPStringEncodingEncodeNonLossyASCII = function(aString)
|
|
{
|
|
var bytes = [];
|
|
|
|
for (var i = 0, count = aString.length; i < count; i++)
|
|
{
|
|
var unit = aString.charCodeAt(i);
|
|
|
|
if (unit === 0x5C)
|
|
bytes.push(0x5C, 0x5C);
|
|
else if (unit < 0x80)
|
|
bytes.push(unit);
|
|
else
|
|
{
|
|
var hex = ("000" + unit.toString(16)).slice(-4);
|
|
|
|
bytes.push(0x5C, 0x75);
|
|
|
|
for (var j = 0; j < 4; j++)
|
|
bytes.push(hex.charCodeAt(j));
|
|
}
|
|
}
|
|
|
|
return bytes;
|
|
};
|
|
|
|
/*
|
|
Returns a map from code point to byte for a single-byte encoding. ASCII and ISO Latin 1 map
|
|
code points to bytes of the same value. Other single-byte encodings are mapped by decoding
|
|
each byte with the browser's TextDecoder, so that encoding and decoding agree. Returns nil
|
|
when there is no TextDecoder.
|
|
*/
|
|
var CPStringEncodingSingleByteMap = function(aDescriptor)
|
|
{
|
|
var map = CPStringEncodingSingleByteMaps[aDescriptor.encoding];
|
|
|
|
if (map)
|
|
return map;
|
|
|
|
map = {};
|
|
|
|
if (aDescriptor.maximumByte)
|
|
{
|
|
for (var aByte = 0; aByte <= aDescriptor.maximumByte; aByte++)
|
|
map[aByte] = aByte;
|
|
}
|
|
else
|
|
{
|
|
if (typeof TextDecoder === "undefined")
|
|
return nil;
|
|
|
|
var decoder = new TextDecoder(aDescriptor.label);
|
|
|
|
for (var aByte = 0; aByte <= 0xFF; aByte++)
|
|
{
|
|
var decoded = decoder.decode(new Uint8Array([aByte]));
|
|
|
|
if (decoded.length === 1 && decoded.charCodeAt(0) !== CPStringEncodingReplacementCharacter)
|
|
map[decoded.charCodeAt(0)] = aByte;
|
|
}
|
|
}
|
|
|
|
CPStringEncodingSingleByteMaps[aDescriptor.encoding] = map;
|
|
|
|
return map;
|
|
};
|
|
|
|
/*
|
|
Encodes a single-byte encoding. A character with no byte makes the result nil. With isLossy,
|
|
it becomes its base letter when that has a byte, so that "é" becomes "e", and otherwise "?".
|
|
*/
|
|
var CPStringEncodingEncodeSingleByte = function(aDescriptor, aString, isLossy)
|
|
{
|
|
var map = CPStringEncodingSingleByteMap(aDescriptor),
|
|
bytes = [],
|
|
failed = NO;
|
|
|
|
if (!map)
|
|
return nil;
|
|
|
|
CPStringEncodingForEachCodePoint(aString, function(codePoint)
|
|
{
|
|
var aByte = map[codePoint];
|
|
|
|
if (aByte === undefined)
|
|
{
|
|
if (!isLossy)
|
|
{
|
|
failed = YES;
|
|
return;
|
|
}
|
|
|
|
var base = codePoint >= 0 ? String.fromCodePoint(codePoint).normalize("NFD").charCodeAt(0) : -1;
|
|
|
|
aByte = map[base];
|
|
|
|
if (aByte === undefined)
|
|
aByte = 0x3F;
|
|
}
|
|
|
|
bytes.push(aByte);
|
|
});
|
|
|
|
return failed ? nil : bytes;
|
|
};
|
|
|
|
// MARK: Encodings
|
|
|
|
/*
|
|
Describes each supported encoding: its name, how to decode and encode it, and the most bytes
|
|
one UTF-16 code unit can take. An encoding with a TextDecoder label needs the browser's
|
|
TextDecoder. The Japanese encodings can be decoded but not encoded, because the browser has
|
|
no encoder for them.
|
|
*/
|
|
var CPStringEncodingDescriptors = {},
|
|
CPStringEncodingOrder = [];
|
|
|
|
var CPStringEncodingAddDescriptor = function(anEncoding, aName, aDescriptor)
|
|
{
|
|
aDescriptor.encoding = anEncoding;
|
|
aDescriptor.name = aName;
|
|
|
|
CPStringEncodingDescriptors[anEncoding] = aDescriptor;
|
|
CPStringEncodingOrder.push(anEncoding);
|
|
};
|
|
|
|
var CPStringEncodingAddSingleByteDescriptor = function(anEncoding, aName, aLabel)
|
|
{
|
|
var descriptor = {
|
|
label: aLabel,
|
|
maximumLength: 1,
|
|
decode: function(bytes, isLossy) { return CPStringEncodingDecodeWithTextDecoder(aLabel, bytes, isLossy); },
|
|
encode: function(aString, isLossy) { return CPStringEncodingEncodeSingleByte(descriptor, aString, isLossy); }
|
|
};
|
|
|
|
CPStringEncodingAddDescriptor(anEncoding, aName, descriptor);
|
|
};
|
|
|
|
CPStringEncodingAddDescriptor(CPUTF8StringEncoding, @"Unicode (UTF-8)", {
|
|
label: "utf-8",
|
|
maximumLength: 3,
|
|
decode: function(bytes, isLossy) { return CPStringEncodingDecodeWithTextDecoder("utf-8", bytes, isLossy); },
|
|
encode: CPStringEncodingEncodeUTF8
|
|
});
|
|
|
|
CPStringEncodingAddDescriptor(CPASCIIStringEncoding, @"Western (ASCII)", {
|
|
maximumByte: 0x7F,
|
|
maximumLength: 1,
|
|
decode: CPStringEncodingDecodeASCII,
|
|
encode: function(aString, isLossy) { return CPStringEncodingEncodeSingleByte(CPStringEncodingDescriptors[CPASCIIStringEncoding], aString, isLossy); }
|
|
});
|
|
|
|
CPStringEncodingAddDescriptor(CPISOLatin1StringEncoding, @"Western (ISO Latin 1)", {
|
|
maximumByte: 0xFF,
|
|
maximumLength: 1,
|
|
decode: CPStringEncodingDecodeLatin1,
|
|
encode: function(aString, isLossy) { return CPStringEncodingEncodeSingleByte(CPStringEncodingDescriptors[CPISOLatin1StringEncoding], aString, isLossy); }
|
|
});
|
|
|
|
CPStringEncodingAddDescriptor(CPNonLossyASCIIStringEncoding, @"Non-lossy ASCII", {
|
|
maximumLength: 6,
|
|
decode: CPStringEncodingDecodeNonLossyASCII,
|
|
encode: CPStringEncodingEncodeNonLossyASCII
|
|
});
|
|
|
|
CPStringEncodingAddDescriptor(CPUnicodeStringEncoding, @"Unicode (UTF-16)", {
|
|
maximumLength: 2,
|
|
byteOrderMarkLength: 2,
|
|
decode: function(bytes, isLossy) { return CPStringEncodingDecodeUTF16(bytes, NO, YES, isLossy); },
|
|
encode: function(aString, isLossy) { return CPStringEncodingEncodeUTF16(aString, YES, YES, isLossy); }
|
|
});
|
|
|
|
CPStringEncodingAddDescriptor(CPUTF16BigEndianStringEncoding, @"Unicode (UTF-16BE)", {
|
|
maximumLength: 2,
|
|
decode: function(bytes, isLossy) { return CPStringEncodingDecodeUTF16(bytes, NO, NO, isLossy); },
|
|
encode: function(aString, isLossy) { return CPStringEncodingEncodeUTF16(aString, NO, NO, isLossy); }
|
|
});
|
|
|
|
CPStringEncodingAddDescriptor(CPUTF16LittleEndianStringEncoding, @"Unicode (UTF-16LE)", {
|
|
maximumLength: 2,
|
|
decode: function(bytes, isLossy) { return CPStringEncodingDecodeUTF16(bytes, YES, NO, isLossy); },
|
|
encode: function(aString, isLossy) { return CPStringEncodingEncodeUTF16(aString, YES, NO, isLossy); }
|
|
});
|
|
|
|
CPStringEncodingAddDescriptor(CPUTF32StringEncoding, @"Unicode (UTF-32)", {
|
|
maximumLength: 4,
|
|
byteOrderMarkLength: 4,
|
|
decode: function(bytes, isLossy) { return CPStringEncodingDecodeUTF32(bytes, NO, YES, isLossy); },
|
|
encode: function(aString, isLossy) { return CPStringEncodingEncodeUTF32(aString, YES, YES, isLossy); }
|
|
});
|
|
|
|
CPStringEncodingAddDescriptor(CPUTF32BigEndianStringEncoding, @"Unicode (UTF-32BE)", {
|
|
maximumLength: 4,
|
|
decode: function(bytes, isLossy) { return CPStringEncodingDecodeUTF32(bytes, NO, NO, isLossy); },
|
|
encode: function(aString, isLossy) { return CPStringEncodingEncodeUTF32(aString, NO, NO, isLossy); }
|
|
});
|
|
|
|
CPStringEncodingAddDescriptor(CPUTF32LittleEndianStringEncoding, @"Unicode (UTF-32LE)", {
|
|
maximumLength: 4,
|
|
decode: function(bytes, isLossy) { return CPStringEncodingDecodeUTF32(bytes, YES, NO, isLossy); },
|
|
encode: function(aString, isLossy) { return CPStringEncodingEncodeUTF32(aString, YES, NO, isLossy); }
|
|
});
|
|
|
|
CPStringEncodingAddSingleByteDescriptor(CPWindowsCP1252StringEncoding, @"Western (Windows Latin 1)", "windows-1252");
|
|
CPStringEncodingAddSingleByteDescriptor(CPMacOSRomanStringEncoding, @"Western (Mac OS Roman)", "macintosh");
|
|
CPStringEncodingAddSingleByteDescriptor(CPWindowsCP1250StringEncoding, @"Central European (Windows Latin 2)", "windows-1250");
|
|
CPStringEncodingAddSingleByteDescriptor(CPISOLatin2StringEncoding, @"Central European (ISO Latin 2)", "iso-8859-2");
|
|
CPStringEncodingAddSingleByteDescriptor(CPWindowsCP1251StringEncoding, @"Cyrillic (Windows)", "windows-1251");
|
|
CPStringEncodingAddSingleByteDescriptor(CPWindowsCP1253StringEncoding, @"Greek (Windows)", "windows-1253");
|
|
CPStringEncodingAddSingleByteDescriptor(CPWindowsCP1254StringEncoding, @"Turkish (Windows Latin 5)", "windows-1254");
|
|
|
|
/*
|
|
TODO: Encoders for EUC, Shift JIS and ISO 2022-JP. The browser has no encoder for them, so
|
|
they need Cappuccino's own tables, built from JIS X 0208 and JIS X 0212.
|
|
*/
|
|
CPStringEncodingAddDescriptor(CPJapaneseEUCStringEncoding, @"Japanese (EUC)", {
|
|
label: "euc-jp",
|
|
maximumLength: 3,
|
|
decode: function(bytes, isLossy) { return CPStringEncodingDecodeWithTextDecoder("euc-jp", bytes, isLossy); },
|
|
encode: nil
|
|
});
|
|
|
|
CPStringEncodingAddDescriptor(CPShiftJISStringEncoding, @"Japanese (Shift JIS)", {
|
|
label: "shift_jis",
|
|
maximumLength: 2,
|
|
decode: function(bytes, isLossy) { return CPStringEncodingDecodeWithTextDecoder("shift_jis", bytes, isLossy); },
|
|
encode: nil
|
|
});
|
|
|
|
CPStringEncodingAddDescriptor(CPISO2022JPStringEncoding, @"Japanese (ISO 2022-JP)", {
|
|
label: "iso-2022-jp",
|
|
maximumLength: 5,
|
|
extraLength: 3,
|
|
decode: function(bytes, isLossy) { return CPStringEncodingDecodeWithTextDecoder("iso-2022-jp", bytes, isLossy); },
|
|
encode: nil
|
|
});
|
|
|
|
/*
|
|
Names of the encodings that are defined for compatibility but not supported: the browser
|
|
has no decoder for them.
|
|
*/
|
|
var CPStringEncodingUnsupportedNames = {};
|
|
|
|
CPStringEncodingUnsupportedNames[CPNEXTSTEPStringEncoding] = @"Western (NextStep)";
|
|
CPStringEncodingUnsupportedNames[CPSymbolStringEncoding] = @"Symbol";
|
|
|
|
/*
|
|
Returns YES if an encoding can be decoded in this environment. An encoding that needs the
|
|
browser's TextDecoder is available only where it exists and knows the encoding.
|
|
*/
|
|
var CPStringEncodingIsAvailable = function(aDescriptor)
|
|
{
|
|
if (!aDescriptor)
|
|
return NO;
|
|
|
|
if (!aDescriptor.label)
|
|
return YES;
|
|
|
|
if (typeof TextDecoder === "undefined")
|
|
return NO;
|
|
|
|
try
|
|
{
|
|
new TextDecoder(aDescriptor.label);
|
|
return YES;
|
|
}
|
|
catch (anException)
|
|
{
|
|
return NO;
|
|
}
|
|
};
|
|
|
|
/*
|
|
Returns the bytes of data as an array of bytes. Data that holds a binary string, such as
|
|
file contents from CPItemProvider, is converted to a Uint8Array, which takes one byte for
|
|
each byte. Asking CPData for its bytes would build a plain array of JavaScript numbers, many
|
|
times the size, because CFData stores bytes that way, and keep it in the data.
|
|
*/
|
|
var CPStringEncodingBytesOfData = function(someData)
|
|
{
|
|
if (!someData)
|
|
return [];
|
|
|
|
if (someData._bytes)
|
|
return someData._bytes;
|
|
|
|
var string = someData.rawString(),
|
|
count = string.length,
|
|
bytes = new Uint8Array(count);
|
|
|
|
for (var i = 0; i < count; i++)
|
|
bytes[i] = string.charCodeAt(i) & 0xFF;
|
|
|
|
return bytes;
|
|
};
|
|
|
|
/*
|
|
Returns the bytes from anIndex to the end, without copying when the bytes are a Uint8Array.
|
|
*/
|
|
var CPStringEncodingBytesFromIndex = function(bytes, anIndex)
|
|
{
|
|
return bytes.subarray ? bytes.subarray(anIndex) : bytes.slice(anIndex);
|
|
};
|
|
|
|
var CPStringEncodingDecode = function(bytes, anEncoding, isLossy)
|
|
{
|
|
var descriptor = CPStringEncodingDescriptors[anEncoding];
|
|
|
|
if (!CPStringEncodingIsAvailable(descriptor))
|
|
return nil;
|
|
|
|
return descriptor.decode(bytes || [], isLossy);
|
|
};
|
|
|
|
/*
|
|
TODO: Gaps against NSString's encoding API that can be filled:
|
|
|
|
- -getBytes:maxLength:usedLength:encoding:options:range:remainingRange:, with the
|
|
CPStringEncodingConversionAllowLossy and CPStringEncodingConversionExternalRepresentation
|
|
options. The buffer would be a CPArray passed by reference.
|
|
- The CPProprietaryStringEncoding constant.
|
|
- The category names. Cocoa declares these methods in NSString (NSStringExtensionMethods)
|
|
and NSString (NSStringEncodingDetection). This category's name is the same as the
|
|
CPStringEncoding type.
|
|
*/
|
|
|
|
/*!
|
|
@class CPString
|
|
@ingroup foundation
|
|
|
|
String encodings convert between strings and bytes. The encodings and their values are
|
|
Cocoa's. In a browser:
|
|
|
|
- Every encoding in -availableStringEncodings can be decoded. The browser's TextDecoder
|
|
decodes most of them; ASCII, ISO Latin 1, non-lossy ASCII, UTF-16 and UTF-32 are decoded
|
|
directly.
|
|
- Every available encoding except the Japanese ones can also be encoded. The browser has no
|
|
encoder for EUC, Shift JIS or ISO 2022-JP, so a string cannot be converted to them.
|
|
- NEXTSTEP and Symbol are defined for compatibility, but not available.
|
|
- Encoding names are in English.
|
|
- C strings do not exist, so Cocoa's C string methods are not provided.
|
|
*/
|
|
@implementation CPString (CPStringEncoding)
|
|
|
|
// MARK: Encodings
|
|
|
|
/*!
|
|
Returns the encodings that can be decoded in this environment. The array ends with 0, as in
|
|
Cocoa, so that code which loops until it reaches 0 works unchanged:
|
|
|
|
<pre>
|
|
var encodings = [CPString availableStringEncodings];
|
|
|
|
for (var i = 0; encodings[i] !== 0; i++)
|
|
[self addEncoding:encodings[i]];
|
|
</pre>
|
|
|
|
The 0 is an element of the array, so -count includes it.
|
|
*/
|
|
+ (CPArray)availableStringEncodings
|
|
{
|
|
var encodings = [];
|
|
|
|
for (var i = 0, count = CPStringEncodingOrder.length; i < count; i++)
|
|
if (CPStringEncodingIsAvailable(CPStringEncodingDescriptors[CPStringEncodingOrder[i]]))
|
|
encodings.push(CPStringEncodingOrder[i]);
|
|
|
|
encodings.push(0);
|
|
|
|
return encodings;
|
|
}
|
|
|
|
/*!
|
|
Returns the name of an encoding, such as "Unicode (UTF-8)", or an empty string for an
|
|
unknown encoding.
|
|
*/
|
|
+ (CPString)localizedNameOfStringEncoding:(CPStringEncoding)anEncoding
|
|
{
|
|
var descriptor = CPStringEncodingDescriptors[anEncoding];
|
|
|
|
if (descriptor)
|
|
return descriptor.name;
|
|
|
|
return CPStringEncodingUnsupportedNames[anEncoding] || @"";
|
|
}
|
|
|
|
/*
|
|
TODO: Cocoa returns the system's C string encoding, usually Mac OS Roman on macOS. Decide
|
|
whether to match it.
|
|
*/
|
|
/*!
|
|
Returns the encoding used when no other is specified: UTF-8.
|
|
*/
|
|
+ (CPStringEncoding)defaultCStringEncoding
|
|
{
|
|
return CPUTF8StringEncoding;
|
|
}
|
|
|
|
// MARK: Creating Strings from Bytes
|
|
|
|
/*!
|
|
Returns a string decoded from data in an encoding, or nil when the data is not valid in the
|
|
encoding, or the encoding is not available.
|
|
*/
|
|
- (id)initWithData:(CPData)someData encoding:(CPStringEncoding)anEncoding
|
|
{
|
|
var decoded = CPStringEncodingDecode(CPStringEncodingBytesOfData(someData), anEncoding, NO);
|
|
|
|
if (decoded === nil)
|
|
return nil;
|
|
|
|
return [self initWithString:decoded];
|
|
}
|
|
|
|
/*!
|
|
Returns a string decoded from the first \c aLength bytes of an array of bytes, or nil when
|
|
the bytes are not valid in the encoding, or the encoding is not available.
|
|
*/
|
|
- (id)initWithBytes:(CPArray)someBytes length:(unsigned)aLength encoding:(CPStringEncoding)anEncoding
|
|
{
|
|
var bytes = (someBytes || []).slice(0, aLength),
|
|
decoded = CPStringEncodingDecode(bytes, anEncoding, NO);
|
|
|
|
if (decoded === nil)
|
|
return nil;
|
|
|
|
return [self initWithString:decoded];
|
|
}
|
|
|
|
/*
|
|
TODO: Cocoa scores candidate encodings statistically, so it can choose a different encoding
|
|
than these fixed rules do. Decide how close detection must come to Cocoa's results.
|
|
*/
|
|
/*!
|
|
Returns the encoding of data, and the string decoded from it.
|
|
|
|
The encoding is chosen in this order:
|
|
|
|
- A byte order mark for UTF-8, UTF-16 or UTF-32.
|
|
- UTF-8, when the data is valid UTF-8, unless only suggested encodings may be used.
|
|
- The suggested encodings, in their order.
|
|
- Windows Latin 1, which every byte sequence decodes in, unless only suggested encodings may
|
|
be used.
|
|
- When lossy conversion is allowed, the first candidate, with invalid bytes replaced.
|
|
|
|
The options are:
|
|
|
|
- CPStringEncodingDetectionSuggestedEncodingsKey: an array of encodings to try.
|
|
- CPStringEncodingDetectionUseOnlySuggestedEncodingsKey: YES to try only the suggested
|
|
encodings, after a byte order mark.
|
|
- CPStringEncodingDetectionAllowLossyKey: NO to refuse lossy conversion. The default is YES.
|
|
|
|
@param someData the data to decode
|
|
@param someOptions the options, or nil
|
|
@param aStringRef a reference that receives the decoded string, or nil
|
|
@param aLossyRef a reference that receives YES if the conversion was lossy, or nil
|
|
@return the encoding, or 0 when no encoding fits
|
|
*/
|
|
+ (CPStringEncoding)stringEncodingForData:(CPData)someData encodingOptions:(CPDictionary)someOptions convertedString:(CPStringRef)aStringRef usedLossyConversion:(BOOLRef)aLossyRef
|
|
{
|
|
var bytes = CPStringEncodingBytesOfData(someData),
|
|
suggested = [someOptions objectForKey:CPStringEncodingDetectionSuggestedEncodingsKey] || [],
|
|
usesOnlySuggested = !![someOptions objectForKey:CPStringEncodingDetectionUseOnlySuggestedEncodingsKey],
|
|
allowLossy = [someOptions objectForKey:CPStringEncodingDetectionAllowLossyKey],
|
|
result = function(anEncoding, aString, isLossy)
|
|
{
|
|
if (aStringRef)
|
|
@deref(aStringRef) = aString;
|
|
|
|
if (aLossyRef)
|
|
@deref(aLossyRef) = isLossy;
|
|
|
|
return anEncoding;
|
|
};
|
|
|
|
allowLossy = allowLossy === nil || allowLossy === undefined ? YES : !!allowLossy;
|
|
|
|
var byteOrderMarks = [
|
|
[[0xEF, 0xBB, 0xBF], CPUTF8StringEncoding],
|
|
[[0xFF, 0xFE, 0x00, 0x00], CPUTF32LittleEndianStringEncoding],
|
|
[[0x00, 0x00, 0xFE, 0xFF], CPUTF32BigEndianStringEncoding],
|
|
[[0xFE, 0xFF], CPUTF16BigEndianStringEncoding],
|
|
[[0xFF, 0xFE], CPUTF16LittleEndianStringEncoding]
|
|
];
|
|
|
|
for (var i = 0; i < byteOrderMarks.length; i++)
|
|
{
|
|
var mark = byteOrderMarks[i][0];
|
|
|
|
if (bytes.length >= mark.length && mark.every(function(aByte, anIndex) { return bytes[anIndex] === aByte; }))
|
|
{
|
|
var decoded = CPStringEncodingDecode(CPStringEncodingBytesFromIndex(bytes, mark.length), byteOrderMarks[i][1], NO);
|
|
|
|
if (decoded !== nil)
|
|
return result(byteOrderMarks[i][1], decoded, NO);
|
|
}
|
|
}
|
|
|
|
var candidates = usesOnlySuggested ? [] : [CPUTF8StringEncoding];
|
|
|
|
candidates = candidates.concat(suggested);
|
|
|
|
if (!usesOnlySuggested)
|
|
candidates.push(CPWindowsCP1252StringEncoding);
|
|
|
|
for (var i = 0; i < candidates.length; i++)
|
|
{
|
|
var decoded = CPStringEncodingDecode(bytes, candidates[i], NO);
|
|
|
|
if (decoded !== nil)
|
|
return result(candidates[i], decoded, NO);
|
|
}
|
|
|
|
if (allowLossy)
|
|
{
|
|
for (var i = 0; i < candidates.length; i++)
|
|
{
|
|
var decoded = CPStringEncodingDecode(bytes, candidates[i], YES);
|
|
|
|
if (decoded !== nil)
|
|
return result(candidates[i], decoded, YES);
|
|
}
|
|
}
|
|
|
|
result(0, nil, NO);
|
|
|
|
return 0;
|
|
}
|
|
|
|
// MARK: Converting Strings to Bytes
|
|
|
|
/*!
|
|
Returns the string encoded in an encoding, or nil when a character cannot be encoded in it.
|
|
*/
|
|
- (CPData)dataUsingEncoding:(CPStringEncoding)anEncoding
|
|
{
|
|
return [self dataUsingEncoding:anEncoding allowLossyConversion:NO];
|
|
}
|
|
|
|
/*!
|
|
Returns the string encoded in an encoding. With lossy conversion, a character that cannot be
|
|
encoded becomes its base letter when the encoding has it, and otherwise "?". Returns nil when
|
|
the encoding cannot be encoded in this environment, or, without lossy conversion, when a
|
|
character cannot be encoded. UTF-16 and UTF-32 without a stated byte order start with a
|
|
byte order mark, and are little-endian.
|
|
*/
|
|
- (CPData)dataUsingEncoding:(CPStringEncoding)anEncoding allowLossyConversion:(BOOL)isLossy
|
|
{
|
|
var bytes = [self _bytesUsingEncoding:anEncoding allowLossyConversion:isLossy];
|
|
|
|
return bytes ? [CPData dataWithBytes:bytes] : nil;
|
|
}
|
|
|
|
/*!
|
|
@ignore
|
|
*/
|
|
- (CPArray)_bytesUsingEncoding:(CPStringEncoding)anEncoding allowLossyConversion:(BOOL)isLossy
|
|
{
|
|
var descriptor = CPStringEncodingDescriptors[anEncoding];
|
|
|
|
if (!CPStringEncodingIsAvailable(descriptor) || !descriptor.encode)
|
|
return nil;
|
|
|
|
return descriptor.encode(String(self), !!isLossy);
|
|
}
|
|
|
|
/*!
|
|
Returns YES if every character of the string can be encoded in an encoding.
|
|
*/
|
|
- (BOOL)canBeConvertedToEncoding:(CPStringEncoding)anEncoding
|
|
{
|
|
return [self _bytesUsingEncoding:anEncoding allowLossyConversion:NO] !== nil;
|
|
}
|
|
|
|
/*!
|
|
Returns the number of bytes the string takes in an encoding, or 0 when it cannot be encoded
|
|
in it.
|
|
*/
|
|
- (unsigned)lengthOfBytesUsingEncoding:(CPStringEncoding)anEncoding
|
|
{
|
|
var bytes = [self _bytesUsingEncoding:anEncoding allowLossyConversion:NO];
|
|
|
|
return bytes ? bytes.length : 0;
|
|
}
|
|
|
|
/*
|
|
TODO: Check whether Cocoa counts the byte order mark for UTF-16 and UTF-32, as this method
|
|
does.
|
|
*/
|
|
/*!
|
|
Returns the most bytes the string can take in an encoding, without encoding it. The result
|
|
may be much larger than the real length. Returns 0 for an unknown encoding.
|
|
*/
|
|
- (unsigned)maximumLengthOfBytesUsingEncoding:(CPStringEncoding)anEncoding
|
|
{
|
|
var descriptor = CPStringEncodingDescriptors[anEncoding];
|
|
|
|
if (!descriptor)
|
|
return 0;
|
|
|
|
return self.length * descriptor.maximumLength + (descriptor.byteOrderMarkLength || 0) + (descriptor.extraLength || 0);
|
|
}
|
|
|
|
/*!
|
|
Returns the encoding in which the string converts fastest: UTF-16, the string's own form.
|
|
*/
|
|
- (CPStringEncoding)fastestEncoding
|
|
{
|
|
return CPUnicodeStringEncoding;
|
|
}
|
|
|
|
/*
|
|
TODO: Cocoa considers every encoding that represents the string without loss. This method
|
|
compares only ASCII, ISO Latin 1, UTF-8 and UTF-16.
|
|
*/
|
|
/*!
|
|
Returns the encoding, of ASCII, ISO Latin 1, UTF-8 and UTF-16, in which the string takes the
|
|
fewest bytes without loss.
|
|
*/
|
|
- (CPStringEncoding)smallestEncoding
|
|
{
|
|
var encodings = [CPASCIIStringEncoding, CPISOLatin1StringEncoding, CPUTF8StringEncoding, CPUTF16LittleEndianStringEncoding],
|
|
smallestEncoding = CPUnicodeStringEncoding,
|
|
smallestLength = Infinity;
|
|
|
|
for (var i = 0; i < encodings.length; i++)
|
|
{
|
|
var bytes = [self _bytesUsingEncoding:encodings[i] allowLossyConversion:NO];
|
|
|
|
if (bytes && bytes.length < smallestLength)
|
|
{
|
|
smallestEncoding = encodings[i] === CPUTF16LittleEndianStringEncoding ? CPUnicodeStringEncoding : encodings[i];
|
|
smallestLength = bytes.length;
|
|
}
|
|
}
|
|
|
|
return smallestEncoding;
|
|
}
|
|
|
|
@end
|