Skip to content

Commit 5fece00

Browse files
committed
fix: preserve primitive namespace and Unicode semantics
1 parent 1b25422 commit 5fece00

9 files changed

Lines changed: 38 additions & 6 deletions

File tree

‎src/JavaScript/_array.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ def of(cls, *values):
105105

106106
@classmethod
107107
def from_(cls, source, mapFn=undefined):
108+
if isinstance(source, str):
109+
from ._string import String
110+
source = String(source)
108111
transform = callback(mapFn) if mapFn is not undefined else None
109112
result = cls()
110113
for index, value in enumerate(source):

‎src/JavaScript/_math.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from ._values import to_number, undefined
99

1010

11-
class Math:
11+
class _Math:
1212
PI = math.pi
1313
E = math.e
1414
LN2 = math.log(2)
@@ -104,3 +104,6 @@ def pow(base=undefined, exponent=undefined):
104104
@staticmethod
105105
def random():
106106
return random.random()
107+
108+
109+
Math = _Math()

‎src/JavaScript/_number.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ def parseInt(value=undefined, radix=undefined):
7676
digits = "0123456789abcdefghijklmnopqrstuvwxyz"[:base]
7777
result = 0
7878
count = 0
79-
for char in text.lower():
79+
for char in text:
80+
if "A" <= char <= "Z":
81+
char = char.lower()
8082
if char not in digits:
8183
break
8284
result = result * base + digits.index(char)

‎src/JavaScript/_object.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ def fromEntries(entries):
169169
def hasOwn(value, key):
170170
if value is None or value is undefined:
171171
raise TypeError("Cannot convert undefined or null to object")
172+
if isinstance(value, str) and to_string(key) == "length":
173+
return True
172174
method = getattr(type(value), "_js_has_own", None)
173175
if method is not None:
174176
return method(value, to_string(key))

‎src/JavaScript/_values.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def to_string(value=undefined) -> str:
118118
if value is False:
119119
return "false"
120120
if isinstance(value, str):
121-
return str(value)
121+
return value.encode("utf-16-le", "surrogatepass").decode("utf-16-le", "surrogatepass")
122122
return number_string(value)
123123

124124

‎tests/fixtures/conformance.mjs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ const values = [undefined, null, false, true, 0, -0, NaN, Infinity, -Infinity,
2424
cases.conversion = values.map(value => [Boolean(value), Number(value), String(value), typeof value]);
2525
cases.equality = [1 === true, NaN === NaN, 0 === -0, Object.is(0, -0),
2626
Object.is(NaN, NaN), {} === {}, "😀" === "\ud83d\ude00"];
27+
cases.namespace = [typeof Math, typeof JSON, typeof Number, Object.hasOwn("😀", "length")];
28+
const unicodeObject = {"😀": 1}; unicodeObject["\ud83d\ude00"] = 2;
29+
cases.unicodeKeys = [Object.keys(unicodeObject), unicodeObject["😀"], Array.from("\ud83d\ude00")];
2730
const obj = {b: 1, 10: 2, 2: 3, "01": 4, 0: 5, "4294967295": 6};
2831
cases.objectOrder = Object.entries(obj);
2932
delete obj.b; obj.b = 7;
@@ -78,7 +81,7 @@ cases.stringBoundary = ["abc".lastIndexOf("", 0), "abc".lastIndexOf("", -1), "ab
7881
const nums = [undefined, null, true, "1", NaN, Infinity, -0, 3.5, 3, 2**53 - 1, 2**53];
7982
cases.numberPredicates = nums.map(value => [Number.isNaN(value), Number.isFinite(value), Number.isInteger(value), Number.isSafeInteger(value)]);
8083
cases.parseInt = ["12px", "0xff", "010", "1e3", "-0", "bad", "0x"].map(value => parseInt(value));
81-
cases.parseRadix = [["ffz", 16], ["11", 2], ["12", 1], ["z", 36], ["0b11", 0]].map(([text, radix]) => parseInt(text, radix));
84+
cases.parseRadix = [["ffz", 16], ["11", 2], ["12", 1], ["z", 36], ["0b11", 0], ["K", 36], ["K", 36]].map(([text, radix]) => parseInt(text, radix));
8285
cases.parseFloat = ["-1.2e2px", "1e+", "0xff", "Infinity!", "bad"].map(value => parseFloat(value));
8386
const rounding = [-Infinity, -2.5, -1.5, -0.5, -0.2, -0, 0, 0.49999999999999994, 0.5, 1.5, Infinity, NaN];
8487
cases.mathRounding = rounding.map(value => [Math.floor(value), Math.ceil(value), Math.trunc(value), Math.round(value), Math.sign(value)]);

‎tests/test_conformance.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ def observations():
4343
"inf", "\x851", "12", Array(), Array.of(1), Array.of(1, 2), Object()]
4444
cases["conversion"] = [[Boolean(value), Number(value), String(value), typeof(value)] for value in values]
4545
cases["equality"] = [strictEqual(1, True), strictEqual(math.nan, math.nan), strictEqual(0, -0.0), Object.is_(0, -0.0), Object.is_(math.nan, math.nan), strictEqual(Object(), Object()), strictEqual("😀", "\ud83d\ude00")]
46+
cases["namespace"] = [typeof(Math), typeof(JSON), typeof(Number), Object.hasOwn("😀", "length")]
47+
unicode_object = Object({"😀": 1})
48+
unicode_object["\ud83d\ude00"] = 2
49+
cases["unicodeKeys"] = [Object.keys(unicode_object), unicode_object["😀"], Array.from_("\ud83d\ude00")]
4650
obj = Object({"b": 1, "10": 2, "2": 3, "01": 4, "0": 5, "4294967295": 6})
4751
cases["objectOrder"] = Object.entries(obj)
4852
del obj.b
@@ -91,7 +95,7 @@ def visit(value, index, array):
9195
nums = [undefined, None, True, "1", math.nan, math.inf, -0.0, 3.5, 3, 2**53 - 1, 2**53]
9296
cases["numberPredicates"] = [[Number.isNaN(value), Number.isFinite(value), Number.isInteger(value), Number.isSafeInteger(value)] for value in nums]
9397
cases["parseInt"] = [parseInt(value) for value in ["12px", "0xff", "010", "1e3", "-0", "bad", "0x"]]
94-
cases["parseRadix"] = [parseInt(text, radix) for text, radix in [("ffz", 16), ("11", 2), ("12", 1), ("z", 36), ("0b11", 0)]]
98+
cases["parseRadix"] = [parseInt(text, radix) for text, radix in [("ffz", 16), ("11", 2), ("12", 1), ("z", 36), ("0b11", 0), ("K", 36), ("K", 36)]]
9599
cases["parseFloat"] = [parseFloat(value) for value in ["-1.2e2px", "1e+", "0xff", "Infinity!", "bad"]]
96100
rounding = [-math.inf, -2.5, -1.5, -0.5, -0.2, -0.0, 0, 0.49999999999999994, 0.5, 1.5, math.inf, math.nan]
97101
cases["mathRounding"] = [[Math.floor(value), Math.ceil(value), Math.trunc(value), Math.round(value), Math.sign(value)] for value in rounding]

‎tests/test_object.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ def test_array_and_string_own_properties(self):
6969
self.assertTrue(Object.hasOwn(value, 1))
7070
self.assertEqual(list(Object.keys("😀")), ["0", "1"])
7171
self.assertEqual(list(Object.values("😀")), ["\ud83d", "\ude00"])
72+
self.assertTrue(Object.hasOwn("😀", "length"))
73+
74+
def test_equivalent_utf16_strings_share_one_property_key(self):
75+
value = Object({"😀": 1})
76+
value["\ud83d\ude00"] = 2
77+
self.assertEqual(list(Object.keys(value)), ["😀"])
78+
self.assertEqual(value["😀"], 2)
7279

7380
def test_null_errors_and_same_value(self):
7481
for value in (None, undefined):

‎tests/test_primitives.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import math
22
import unittest
33

4-
from JavaScript import Array, Boolean, Math, Number, String, isFinite, parseFloat, parseInt, strictEqual, undefined
4+
from JavaScript import Array, Boolean, Math, Number, String, isFinite, parseFloat, parseInt, strictEqual, typeof, undefined
55

66

77
class StringTests(unittest.TestCase):
@@ -20,6 +20,7 @@ def test_conversion_and_utf16_indexing(self):
2020
self.assertIs(value.codePointAt(4), undefined)
2121
self.assertEqual(list(value), ["a", "😀", "b"])
2222
self.assertEqual(list(Array.from_(value)), ["a", "😀", "b"])
23+
self.assertEqual(list(Array.from_("\ud83d\ude00")), ["😀"])
2324
self.assertTrue(strictEqual("😀", "\ud83d\ude00"))
2425
self.assertEqual(String(), "")
2526
self.assertEqual(String(undefined), "undefined")
@@ -87,6 +88,8 @@ def test_prefix_parsers(self):
8788
self.assertEqual(Number.parseInt("11", 2), 3)
8889
self.assertTrue(math.isnan(parseInt("12", 1)))
8990
self.assertTrue(math.isnan(parseInt("0x")))
91+
self.assertTrue(math.isnan(parseInt("K", 36)))
92+
self.assertEqual(parseInt("K", 36), 20)
9093
self.assertEqual(math.copysign(1, parseInt("-0")), -1)
9194
self.assertEqual(parseFloat("-1.2e2px"), -120)
9295
self.assertEqual(parseFloat("1e+"), 1)
@@ -96,6 +99,11 @@ def test_prefix_parsers(self):
9699

97100

98101
class MathTests(unittest.TestCase):
102+
def test_math_is_a_noncallable_namespace(self):
103+
self.assertEqual(typeof(Math), "object")
104+
with self.assertRaises(TypeError):
105+
Math()
106+
99107
def test_rounding_and_negative_zero(self):
100108
self.assertEqual(Math.round(2.5), 3)
101109
self.assertEqual(Math.round(-1.5), -1)

0 commit comments

Comments
 (0)