JsonhPy 2.2__tar.gz → 2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: JsonhPy
3
- Version: 2.2
3
+ Version: 2.4
4
4
  Summary: JSON for Humans in Python.
5
5
  Author-email: Joyless <joyless.mod@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "JsonhPy"
3
- version = "2.2"
3
+ version = "2.4"
4
4
  authors = [
5
5
  { name="Joyless", email="joyless.mod@gmail.com" },
6
6
  ]
@@ -1,5 +1,7 @@
1
+ import math
1
2
  from enum import Enum
2
3
  from typing import Iterator, Iterable
4
+ import json
3
5
 
4
6
  class JsonhResult[T, E]:
5
7
  is_error: bool
@@ -223,7 +225,15 @@ class JsonhNumberParser:
223
225
  return exponent
224
226
 
225
227
  # Multiply mantissa by 10 ^ exponent
226
- return JsonhResult.from_value(mantissa.value() * (10 ** exponent.value()))
228
+ try:
229
+ return JsonhResult.from_value(mantissa.value() * (10.0 ** exponent.value()))
230
+ except:
231
+ if mantissa.value() > 0:
232
+ return JsonhResult.from_value(math.inf)
233
+ elif mantissa.value() < 0:
234
+ return JsonhResult.from_value(-math.inf)
235
+ else:
236
+ return JsonhResult.from_value(0.0)
227
237
 
228
238
  @staticmethod
229
239
  def _parse_fractional_number(digits: str, base_digits: str) -> JsonhResult[float, str]:
@@ -238,29 +248,32 @@ class JsonhNumberParser:
238
248
 
239
249
  # Get parts of number
240
250
  whole_part: str = digits[:dot_index]
241
- fractional_part: str = digits[(dot_index + 1):]
251
+ fraction_part: str = digits[(dot_index + 1):]
242
252
 
243
253
  # Parse parts of number
244
- whole: JsonhResult[int, str] = JsonhNumberParser._parse_whole_number(whole_part, base_digits)
254
+ whole: JsonhResult[float, str] = JsonhNumberParser._parse_whole_number(whole_part, base_digits)
245
255
  if whole.is_error:
246
256
  return whole
247
- fraction: JsonhResult[int, str] = JsonhNumberParser._parse_whole_number(fractional_part, base_digits)
248
- if fraction.is_error:
249
- return fraction
250
-
251
- # Get fraction leading zeroes
252
- fraction_leading_zeroes: str = ""
253
- for index in range(0, len(fractional_part)):
254
- if fractional_part[index] == '0':
255
- fraction_leading_zeroes += '0'
256
- else:
257
- break
257
+
258
+ # Add each column of fraction digits
259
+ fraction: float = 0.0
260
+ for index in range(len(fraction_part) - 1, -1, -1):
261
+ # Get current digit
262
+ digit_char: str = fraction_part[index]
263
+ digit_int: int = base_digits.find(digit_char.lower())
264
+
265
+ # Ensure digit is valid
266
+ if digit_int < 0:
267
+ return JsonhResult.from_error(f"Invalid digit: '{digit_char}'")
268
+
269
+ # Add value of column
270
+ fraction = (fraction + digit_int) / len(base_digits)
258
271
 
259
272
  # Combine whole and fraction
260
- return JsonhResult.from_value(float(str(whole.value()) + "." + fraction_leading_zeroes + str(fraction.value())))
273
+ return JsonhResult.from_value(whole.value() + fraction)
261
274
 
262
275
  @staticmethod
263
- def _parse_whole_number(digits: str, base_digits: str) -> JsonhResult[int, str]:
276
+ def _parse_whole_number(digits: str, base_digits: str) -> JsonhResult[float, str]:
264
277
  """
265
278
  Converts a whole number (e.g. `12345`) from the given base (e.g. `01234567`) to a base-10 integer.
266
279
  """
@@ -274,7 +287,7 @@ class JsonhNumberParser:
274
287
  digits = digits[1:]
275
288
 
276
289
  # Add each column of digits
277
- integer: int = 0
290
+ integer: float = 0.0
278
291
  for index in range(0, len(digits)):
279
292
  # Get current digit
280
293
  digit_char: str = digits[index]
@@ -284,12 +297,8 @@ class JsonhNumberParser:
284
297
  if digit_int < 0:
285
298
  return JsonhResult.from_error(f"Invalid digit: '{digit_char}'")
286
299
 
287
- # Get magnitude of current digit column
288
- column_number: int = len(digits) - 1 - index
289
- column_magnitude: int = len(base_digits) ** column_number
290
-
291
300
  # Add value of column
292
- integer += digit_int * column_magnitude
301
+ integer = (integer * len(base_digits)) + digit_int
293
302
 
294
303
  # Apply sign
295
304
  if sign != 1:
@@ -549,6 +558,132 @@ class JsonhReader:
549
558
 
550
559
  return next_element
551
560
 
561
+ def parse_json(self, include_comments: bool = False, indent: str | None = None) -> JsonhResult[str, str]:
562
+ """
563
+ Parses a single element as JSON from the reader.
564
+
565
+ If `include_comments` is true, comments are included (`/*` and `*/` are escaped as `/ *` and `* /`).
566
+
567
+ If `indent` is not null, the output is pretty-printed with the given indentation.
568
+
569
+ The result is not safe to embed in HTML.
570
+ """
571
+
572
+ current_depth: int = 0
573
+ isStartOfStructure: bool = True
574
+ is_property_value: bool = False
575
+
576
+ resultBuilder: str = ""
577
+
578
+ for token_result in self.read_element():
579
+ # Check error
580
+ if (token_result.is_error):
581
+ return JsonhResult.from_error(token_result.error())
582
+ token: JsonhToken = token_result.value()
583
+
584
+ # Add comments and indents
585
+ if not is_property_value:
586
+ # Add comma before property/item
587
+ if (token.json_type not in [JsonTokenType.NONE, JsonTokenType.COMMENT]) and (current_depth > 0) and (not isStartOfStructure):
588
+ # Don't add trailing comma
589
+ if token.json_type not in [JsonTokenType.END_OBJECT, JsonTokenType.END_ARRAY]:
590
+ resultBuilder += ','
591
+
592
+ # Apply indentation
593
+ if indent != None:
594
+ # Don't indent inside empty structures
595
+ if not (token.json_type in [JsonTokenType.END_OBJECT, JsonTokenType.END_ARRAY] and isStartOfStructure):
596
+ # Don't indent comment if not included
597
+ if (not (token.json_type == JsonTokenType.COMMENT and (not include_comments))):
598
+ # Don't indent root elements
599
+ if current_depth > 0:
600
+ # Add newline before element
601
+ resultBuilder += '\n'
602
+
603
+ # Get current indent count
604
+ indent_count: int = current_depth
605
+ if token.json_type in [JsonTokenType.END_OBJECT, JsonTokenType.END_ARRAY]:
606
+ indent_count -= 1
607
+
608
+ # Add indent
609
+ for _ in range(0, indent_count):
610
+ resultBuilder += indent
611
+ # Track start of structure to avoid adding leading comma
612
+ if token.json_type not in [JsonTokenType.NONE, JsonTokenType.COMMENT]:
613
+ isStartOfStructure = False
614
+ if token.json_type in [JsonTokenType.START_OBJECT, JsonTokenType.START_ARRAY]:
615
+ isStartOfStructure = True
616
+
617
+ match token.json_type:
618
+ # Null
619
+ case JsonTokenType.NONE:
620
+ resultBuilder += "null"
621
+ if current_depth == 0:
622
+ return JsonhResult.from_value(resultBuilder)
623
+ # True
624
+ case JsonTokenType.TRUE:
625
+ resultBuilder += "true"
626
+ if current_depth == 0:
627
+ return JsonhResult.from_value(resultBuilder)
628
+ # False
629
+ case JsonTokenType.FALSE:
630
+ resultBuilder += "false"
631
+ if current_depth == 0:
632
+ return JsonhResult.from_value(resultBuilder)
633
+ # String
634
+ case JsonTokenType.STRING:
635
+ resultBuilder += json.dumps(token.value, ensure_ascii=False)
636
+ if current_depth == 0:
637
+ return JsonhResult.from_value(resultBuilder)
638
+ # Number
639
+ case JsonTokenType.NUMBER:
640
+ result: JsonhResult[float] = JsonhNumberParser.parse(token_result.value().value)
641
+ if (result.is_error):
642
+ return JsonhResult.from_error(result.error())
643
+ resultBuilder += str(result.value())
644
+ if current_depth == 0:
645
+ return JsonhResult.from_value(resultBuilder)
646
+ # Start Object
647
+ case JsonTokenType.START_OBJECT:
648
+ resultBuilder += '{'
649
+ current_depth += 1
650
+ # Start Array
651
+ case JsonTokenType.START_ARRAY:
652
+ resultBuilder += '['
653
+ current_depth += 1
654
+ # End Object
655
+ case JsonTokenType.END_OBJECT:
656
+ resultBuilder += '}'
657
+ current_depth -= 1
658
+ if current_depth == 0:
659
+ return JsonhResult.from_value(resultBuilder)
660
+ # End Array
661
+ case JsonTokenType.END_ARRAY:
662
+ resultBuilder += ']'
663
+ current_depth -= 1
664
+ if current_depth == 0:
665
+ return JsonhResult.from_value(resultBuilder)
666
+ # Property Name
667
+ case JsonTokenType.PROPERTY_NAME:
668
+ resultBuilder += json.dumps(token.value, ensure_ascii=False)
669
+ resultBuilder += ':'
670
+ if indent != None:
671
+ resultBuilder += ' '
672
+ # Comment
673
+ case JsonTokenType.COMMENT:
674
+ if (include_comments):
675
+ resultBuilder += "/*"
676
+ resultBuilder += token.value.replace("/*", "/ *").replace("*/", "* /")
677
+ resultBuilder += "*/"
678
+ # Not implemented
679
+ case _:
680
+ return JsonhResult.from_error("Token type not implemented")
681
+
682
+ is_property_value = token.json_type == JsonTokenType.PROPERTY_NAME
683
+
684
+ # End of input
685
+ return JsonhResult.from_error("Expected token, got end of input")
686
+
552
687
  def find_property_value(self, property_name: str) -> bool:
553
688
  """
554
689
  Tries to find the given property name in the reader.
@@ -1263,7 +1398,7 @@ class JsonhReader:
1263
1398
  is_empty = False
1264
1399
  # Dot
1265
1400
  elif next == '.':
1266
- # Disallow dot preceding underscore
1401
+ # Disallow dot following underscore
1267
1402
  if len(number_builder.ref) >= 1 and number_builder.ref[-1] == '_':
1268
1403
  return JsonhResult.from_error("`.` must not follow `_` in number")
1269
1404
 
@@ -1437,26 +1572,40 @@ class JsonhReader:
1437
1572
  return
1438
1573
 
1439
1574
  def _read_hex_sequence(self, length: int) -> JsonhResult[int, str]:
1440
- hex_chars: str = ""
1575
+ assert(length <= 8)
1441
1576
 
1442
- for index in range(0, length):
1577
+ value: int = 0
1578
+
1579
+ for _ in range(0, length):
1443
1580
  next: str | None = self._read()
1444
1581
 
1445
1582
  # Hex digit
1446
1583
  if next != None and ((ord('0') <= ord(next) <= ord('9')) or (ord('A') <= ord(next) <= ord('F')) or (ord('a') <= ord(next) <= ord('f'))):
1447
- hex_chars += next
1584
+ # Get hex digit
1585
+ digit: int = ord(next)
1586
+ # Convert hex digit to integer
1587
+ integer: int = \
1588
+ digit - ord('A') + 10 if (digit >= ord('A') and digit <= ord('F')) else \
1589
+ digit - ord('a') + 10 if (digit >= ord('a') and digit <= ord('f')) else \
1590
+ digit - ord('0')
1591
+ # Aggregate digit into value
1592
+ value = (value * 16) + integer
1448
1593
  # Unexpected char
1449
1594
  else:
1450
1595
  return JsonhResult.from_error("Incorrect number of hexadecimal digits in unicode escape sequence")
1451
1596
 
1452
- # Parse unicode character from hex digits
1453
- return JsonhResult.from_value(int(hex_chars, base=16))
1597
+ # Return aggregated value
1598
+ return JsonhResult.from_value(value)
1454
1599
 
1455
- def _read_escape_sequence(self) -> JsonhResult[str, str]:
1600
+ def _read_escape_sequence(self, high_surrogate: int | None = None) -> JsonhResult[str, str]:
1456
1601
  escape_char: str | None = self._read()
1457
1602
  if escape_char == None:
1458
1603
  return JsonhResult.from_error("Expected escape sequence, got end of input")
1459
1604
 
1605
+ # Ensure high surrogates are completed
1606
+ if high_surrogate != None and escape_char not in ['u', 'x', 'U']:
1607
+ return JsonhResult.from_error("Expected low surrogate after high surrogate")
1608
+
1460
1609
  match escape_char:
1461
1610
  # Reverse solidus
1462
1611
  case '\\':
@@ -1490,13 +1639,13 @@ class JsonhReader:
1490
1639
  return JsonhResult.from_value('\u001b')
1491
1640
  # Unicode hex sequence
1492
1641
  case 'u':
1493
- return self._read_hex_escape_sequence(4)
1642
+ return self._read_hex_escape_sequence(4, high_surrogate)
1494
1643
  # Short unicode hex sequence
1495
1644
  case 'x':
1496
- return self._read_hex_escape_sequence(2)
1645
+ return self._read_hex_escape_sequence(2, high_surrogate)
1497
1646
  # Long unicode hex sequence
1498
1647
  case 'U':
1499
- return self._read_hex_escape_sequence(8)
1648
+ return self._read_hex_escape_sequence(8, high_surrogate)
1500
1649
  # Escaped newline
1501
1650
  case self._NEWLINE_CHARS:
1502
1651
  # Join CR LF
@@ -1507,51 +1656,43 @@ class JsonhReader:
1507
1656
  case _:
1508
1657
  return JsonhResult.from_value(escape_char)
1509
1658
 
1510
- def _read_hex_escape_sequence(self, length: int) -> JsonhResult[str, str]:
1511
- # This method is used to combine escaped UTF-16 surrogate pairs (e.g. "\uD83D\uDC7D" -> "👽")
1512
-
1513
- # Read hex digits & convert to uint
1659
+ def _read_hex_escape_sequence(self, length: int, high_surrogate: int | None) -> JsonhResult[str, str]:
1514
1660
  code_point: JsonhResult[int, str] = self._read_hex_sequence(length)
1515
1661
  if code_point.is_error:
1516
1662
  return JsonhResult.from_error(code_point.error())
1517
1663
 
1518
- # High surrogate
1519
- if (self._is_utf16_high_surrogate(code_point.value())):
1520
- original_position: int = self.index
1521
- # Escape sequence
1522
- if self._read_one('\\'):
1523
- next: str | None = self._read_any('u', 'x', 'U')
1524
- # Low surrogate escape sequence
1525
- if next:
1526
- # Read hex sequence
1527
- low_code_point: JsonhResult[int, str]
1528
- match next:
1529
- case 'u':
1530
- low_code_point = self._read_hex_sequence(4)
1531
- case 'x':
1532
- low_code_point = self._read_hex_sequence(2)
1533
- case 'U':
1534
- low_code_point = self._read_hex_sequence(8)
1535
- # Ensure hex sequence read successfully
1536
- if low_code_point.is_error:
1537
- return JsonhResult.from_error(low_code_point.error())
1538
- # Combine high and low surrogates
1539
- code_point.value_or_none = self._utf16_surrogates_to_code_point(code_point.value(), low_code_point.value())
1540
- # Other escape sequence
1541
- else:
1542
- self.index = original_position
1543
-
1544
- # Rune
1545
- return JsonhResult.from_value(chr(code_point.value()))
1664
+ # Low surrogate
1665
+ if high_surrogate != None:
1666
+ combined: JsonhResult[int, str] = self._utf16_surrogates_to_code_point(high_surrogate, code_point.value())
1667
+ if combined.is_error:
1668
+ return JsonhResult.from_error(combined.error())
1669
+ return JsonhResult.from_value(chr(combined.value()))
1670
+ else:
1671
+ # High surrogate followed by low surrogate
1672
+ if self._is_utf16_high_surrogate(code_point.value()) and self._read_one('\\'):
1673
+ return self._read_escape_sequence(code_point.value())
1674
+ # Standalone character
1675
+ else:
1676
+ return JsonhResult.from_value(chr(code_point.value()))
1546
1677
 
1547
1678
  @staticmethod
1548
- def _utf16_surrogates_to_code_point(high_surrogate: int, low_surrogate: int) -> int:
1549
- return 0x10000 + (((high_surrogate - 0xD800) << 10) | (low_surrogate - 0xDC00))
1679
+ def _utf16_surrogates_to_code_point(high_surrogate: int, low_surrogate: int) -> JsonhResult[int, str]:
1680
+ if not JsonhReader._is_utf16_high_surrogate(high_surrogate):
1681
+ return JsonhResult.from_error("High surrogate out of range")
1682
+
1683
+ if not JsonhReader._is_utf16_low_surrogate(low_surrogate):
1684
+ return JsonhResult.from_error("Low surrogate out of range")
1685
+
1686
+ return JsonhResult.from_value(0x10000 + (((high_surrogate - 0xD800) << 10) | (low_surrogate - 0xDC00)))
1550
1687
 
1551
1688
  @staticmethod
1552
1689
  def _is_utf16_high_surrogate(code_point: int) -> bool:
1553
1690
  return code_point >= 0xD800 and code_point <= 0xDBFF
1554
1691
 
1692
+ @staticmethod
1693
+ def _is_utf16_low_surrogate(code_point: int) -> bool:
1694
+ return code_point >= 0xDC00 and code_point <= 0xDFFF
1695
+
1555
1696
  def _peek(self) -> str | None:
1556
1697
  if self.index >= len(self.string):
1557
1698
  return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: JsonhPy
3
- Version: 2.2
3
+ Version: 2.4
4
4
  Summary: JSON for Humans in Python.
5
5
  Author-email: Joyless <joyless.mod@gmail.com>
6
6
  License-Expression: MIT
File without changes
File without changes
File without changes
File without changes