ckparser 0.2__tar.gz → 0.3__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: ckparser
3
- Version: 0.2
3
+ Version: 0.3
4
4
  Summary: Parse Paradox Jomini data files to Python/JSON and revert them back experimentally.
5
5
  Author: Marc Debureaux (debnet)
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ckparser
3
- Version: 0.2
3
+ Version: 0.3
4
4
  Summary: Parse Paradox Jomini data files to Python/JSON and revert them back experimentally.
5
5
  Author: Marc Debureaux (debnet)
6
6
  License-Expression: MIT
@@ -41,7 +41,7 @@ import re
41
41
  import time
42
42
 
43
43
  # Script version
44
- __version__ = "0.2"
44
+ __version__ = "0.3"
45
45
 
46
46
  # Logger (because logging is awesome)
47
47
  logger = logging.getLogger(__name__)
@@ -64,16 +64,26 @@ class JominiJSONEncoder(json.JSONEncoder):
64
64
 
65
65
  def jomini_object_hook(obj):
66
66
  # JSON specific decoder for dates
67
- for key, value in obj.items():
68
- if isinstance(value, str) and regex_date.match(value):
69
- obj[key] = convert_date(value) or value
67
+ if isinstance(obj, dict):
68
+ for key, value in obj.items():
69
+ if isinstance(value, str) and regex_date.fullmatch(value):
70
+ obj[key] = convert_date(value) or value
71
+ elif isinstance(value, list):
72
+ obj[key] = jomini_object_hook(value)
73
+ elif isinstance(obj, list):
74
+ for index in range(len(obj)):
75
+ item = obj[index]
76
+ if isinstance(item, str) and regex_date.fullmatch(item):
77
+ obj[index] = convert_date(item) or item
78
+ elif isinstance(item, list):
79
+ obj[index] = jomini_object_hook(item)
70
80
  return obj
71
81
 
72
82
 
73
83
  json_load = functools.partial(json.load, object_hook=jomini_object_hook)
74
84
  json_loads = functools.partial(json.loads, object_hook=jomini_object_hook)
75
- json_dump = functools.partial(json.dump, cls=JominiJSONEncoder, indent=4, ensure_ascii=False)
76
- json_dumps = functools.partial(json.dumps, cls=JominiJSONEncoder, indent=4, ensure_ascii=False)
85
+ json_dump = functools.partial(json.dump, cls=JominiJSONEncoder, indent=4)
86
+ json_dumps = functools.partial(json.dumps, cls=JominiJSONEncoder, indent=4)
77
87
 
78
88
  # Boolean transformation
79
89
  booleans = {"yes": True, "no": False}
@@ -92,7 +102,7 @@ regex_string_multiline = re.compile(r"\"[^\"]*\"", re.MULTILINE)
92
102
  # Regex for quoted strings inside quoted strings
93
103
  regex_inner_string = re.compile(r"\|(?P<index>\d+)\|")
94
104
  # Regex to remove comments in files
95
- regex_comment = re.compile(r"(?P<space>\s*)(?P<comment>#.*)$", re.MULTILINE)
105
+ regex_comment = re.compile(r"(?P<space>\s*)(?P<comment>#.*)")
96
106
  # Regex to fix blocks with no equal sign
97
107
  regex_block = re.compile(r"^([^\s\{\=]+)\s*\{\s*$", re.MULTILINE)
98
108
  # Regex to remove "list" prefix
@@ -101,8 +111,8 @@ regex_list = re.compile(r"\s*=\s*list\s+([\{\"\|])", re.MULTILINE)
101
111
  regex_color = re.compile(r"=\s*(?P<type>\w+)\s*{")
102
112
  # Regex to parse items with format key=value
103
113
  regex_inline = re.compile(r"([^\s\"]+\s*[?!<=>]+\s*(([^@\"]\[?[^\s]+\]?)|(\"[^\"]+\")|(@\[[^\]]+\]))|(@\w+))")
104
- # Regex to parse blocks with bracket below the key
105
- regex_values = re.compile(r"(([?!<=>]+)\s*\n+)|(\n+\s*([?!<=>]+))")
114
+ # Regex to parse blocks with bracket below the key/operator
115
+ regex_values = re.compile(r"(\s*([?!<=>]+)\s+\{)|(\s+\s*([?!<=>])+\s*\{)")
106
116
  # Regex to parse lines with format key=value
107
117
  regex_line = re.compile(r"\"?(?P<key>[^\s\"]+)\"?\s*(?P<operator>[?!<=>]+)\s*(list\s*)?(?P<value>.*)")
108
118
  # Regex to parse independent items in a list
@@ -115,6 +125,8 @@ regex_empty = re.compile(r"(\n\s*\n)+", re.MULTILINE)
115
125
  regex_locale = re.compile(r"^\s*(?P<key>[^\:#]+)\:(\d+)?\s\"(?P<value>.+)\".*$")
116
126
  # Regex for keywords
117
127
  regex_keyword = re.compile(r"(" + "|".join(map(re.escape, sorted(keywords, key=len, reverse=True))) + r") ")
128
+ # Regex for fixing line count when bracket is below the key/operator
129
+ regex_count = re.compile(r"([?!<=>]+)\s*§\n+\s*\{")
118
130
  # Regex for string indexes
119
131
  regex_index = re.compile(r"\|(\d+)\|")
120
132
  # Regex for variables
@@ -172,7 +184,7 @@ def read_file(path, encoding="utf_8_sig"):
172
184
  encoding = result["encoding"]
173
185
  logger.debug(f"Detected encoding: {result['encoding']} ({result['confidence']:0.0%})")
174
186
  del raw_data
175
- with open(path, encoding=encoding) as file:
187
+ with open(path, "r", encoding=encoding) as file:
176
188
  return file.read()
177
189
 
178
190
 
@@ -189,19 +201,19 @@ def parse_text(text, return_text_on_error=False, comments=False, dates=False, fi
189
201
  """
190
202
 
191
203
  def replace(match):
192
- nonlocal strings, index
193
- index = len(strings)
194
- strings[str(index)] = match.group(0).replace("\n", "\\n")
195
- return f"|{index}|"
204
+ nonlocal strings, strings_index
205
+ strings_index = len(strings)
206
+ strings[str(strings_index)] = match.group(0).replace("\n", "\\n")
207
+ return f"|{strings_index}|"
196
208
 
197
209
  def replace_comment(match):
198
- nonlocal strings, index
199
- index = len(strings)
210
+ nonlocal strings, strings_index
211
+ strings_index = len(strings)
200
212
  value, space = match.group("comment").replace('"', "'").strip(), match.group("space")
201
- strings[str(index)] = f'"{value}"'
213
+ strings[str(strings_index)] = f'"{value}"'
202
214
  if not value.strip():
203
215
  return ""
204
- return f"\n{space}#{index}=|{index}|\n"
216
+ return f"\n{space}#{strings_index}=|{strings_index}|"
205
217
 
206
218
  def set_variable(key, value, is_global=is_global):
207
219
  global global_variables
@@ -213,29 +225,35 @@ def parse_text(text, return_text_on_error=False, comments=False, dates=False, fi
213
225
 
214
226
  root = {}
215
227
  nodes = [("", root)]
216
- strings, index = {}, 0
228
+ strings, strings_index = {}, 0
217
229
  variables = global_variables.copy()
218
230
  # Cleaning document
231
+ text = text.replace("\n", "\n§\n")
219
232
  text = regex_string.sub(replace, text)
220
233
  if comments:
221
234
  text = regex_comment.sub(replace_comment, text)
222
235
  else:
223
236
  text = regex_comment.sub("", text)
237
+ text = text.replace("{", "\n{\n").replace("}", "\n}\n")
224
238
  text = regex_string_multiline.sub(replace, text)
225
239
  text = regex_list.sub(r"|list=\g<1>", text)
226
- text = regex_block.sub(r"\g<1>={", text)
227
- text = text.replace("{", "\n{\n").replace("}", "\n}\n")
228
240
  text = regex_color.sub(r"={\n\g<1>", text)
229
241
  text = regex_inline.sub(r"\g<1>\n", text)
230
- text = regex_values.sub(r"\g<2>\g<4>", text)
242
+ text = regex_values.sub(r"\g<2>\g<4>{", text)
231
243
  text = regex_empty.sub(r"\n", text)
232
244
  text = regex_keyword.sub(r"\1|", text)
245
+ text = regex_count.sub(r"\g<1>{\n§", text)
233
246
  text = regex_index.sub(lambda match: strings[match.group(1)], text)
234
247
 
235
248
  # Parsing document line by line
236
- for line_number, line_text in enumerate(text.splitlines(), start=1):
249
+ line_number = 1
250
+ for line_text in text.splitlines():
237
251
  try:
238
252
  line_text = line_text.strip()
253
+ # Line number
254
+ if count := line_text.count("§"):
255
+ line_number += count
256
+ line_text = line_text.rstrip("§")
239
257
  # Nothing to do if line is empty
240
258
  if not line_text:
241
259
  continue
@@ -277,7 +295,7 @@ def parse_text(text, return_text_on_error=False, comments=False, dates=False, fi
277
295
  value = val
278
296
  elif value:
279
297
  # Try to convert value to Python value
280
- if dates and regex_date.match(value):
298
+ if dates and regex_date.fullmatch(value):
281
299
  value = convert_date(value) or value
282
300
  else:
283
301
  try:
@@ -360,7 +378,7 @@ def parse_text(text, return_text_on_error=False, comments=False, dates=False, fi
360
378
  node[key] = {"@type": "variable", "@value": value, "@result": result}
361
379
  if result is not None and (key.startswith("@") or len(nodes) == 1):
362
380
  set_variable(key, result)
363
- elif result := variables.get(value):
381
+ elif (result := variables.get(value)) is not None:
364
382
  if not isinstance(result, str):
365
383
  node[key] = {"@type": "variable", "@value": value, "@result": result}
366
384
  if result is not None and (key.startswith("@") or len(nodes) == 1):
@@ -455,7 +473,7 @@ def parse_text(text, return_text_on_error=False, comments=False, dates=False, fi
455
473
  logger.warning(f"Filename: {filename}")
456
474
  logger.warning(f'Value for "{value}" cannot be found (line: {line_number})')
457
475
  node.append({"@type": "variable", "@value": item, "@result": result})
458
- elif dates and regex_date.match(item):
476
+ elif dates and regex_date.fullmatch(item):
459
477
  item = convert_date(item) or item
460
478
  node.append(item)
461
479
  else:
@@ -501,32 +519,32 @@ def parse_file(
501
519
  start_time = time.monotonic()
502
520
  if base_dir:
503
521
  base_dir = os.sep.join(str(base_dir).rstrip(os.sep).split(os.sep)[:-1]) + os.sep
504
- base_dir = os.path.dirname(path.replace(base_dir.replace(os.sep, "/"), ""))
522
+ base_dir = os.path.dirname(path.replace(base_dir, ""))
505
523
  base_dir = base_dir or "."
506
524
  text = read_file(path, encoding)
507
525
  if not text or not text.strip():
508
526
  return None
509
527
  for pattern, replacement in patch or []:
510
528
  text = re.sub(pattern, replacement, text)
511
- filename = os.path.join(base_dir, os.path.basename(path)).replace(os.sep, "/")
529
+ filename = os.path.join(base_dir, os.path.basename(path))
512
530
  logger.debug(f"Parsing {filename}")
513
531
  data = parse_text(
514
532
  text, return_text_on_error=True, comments=comments, dates=dates, filename=filename, is_global=is_global
515
533
  )
516
534
  if save:
517
535
  filename, _ = os.path.splitext(os.path.basename(path))
518
- directory = os.path.join(output_dir or "output", *base_dir.split("/")).replace(os.sep, "/")
536
+ directory = os.path.join(output_dir or "output", *base_dir.split(os.sep))
519
537
  os.makedirs(directory, exist_ok=True)
520
538
  if not isinstance(data, dict):
521
539
  filename = os.path.join(directory, filename + ".error")
522
540
  try:
523
- with open(filename, "w") as file:
541
+ with open(filename, "w", encoding=encoding) as file:
524
542
  file.write(data)
525
543
  except UnicodeEncodeError as error:
526
544
  logger.error(f"Unable to write file {filename}: {error}")
527
545
  else:
528
546
  filename = os.path.join(directory, filename + ".json")
529
- with open(filename, "w") as file:
547
+ with open(filename, "w", encoding="utf-8") as file:
530
548
  json_dump(data, file)
531
549
  total_time = time.monotonic() - start_time
532
550
  logger.debug(f"Elapsed time: {total_time:0.3f}s!")
@@ -579,7 +597,7 @@ def parse_all_files(
579
597
  for filename in all_files:
580
598
  if not filename.lower().endswith(".txt"):
581
599
  continue
582
- filepath = os.path.join(current_path, filename).replace(os.sep, "/")
600
+ filepath = os.path.join(current_path, filename)
583
601
  data = parse_file(
584
602
  filepath,
585
603
  output_dir=output_dir,
@@ -593,7 +611,7 @@ def parse_all_files(
593
611
  if isinstance(data, str):
594
612
  errors.append(filepath)
595
613
  continue
596
- filepath = filepath.replace(str(path), "").lstrip("/")
614
+ filepath = filepath.replace(str(path), "").lstrip(os.sep)
597
615
  success[filepath] = data if keep_data else True
598
616
  total_time = time.monotonic() - start_time
599
617
  logger.info(f"{len(success)} parsed file(s) and {len(errors)} errors in {total_time:0.3f}s!")
@@ -602,14 +620,14 @@ def parse_all_files(
602
620
  return success
603
621
 
604
622
 
605
- def parse_all_locales(path, encoding="utf_8_sig", language="english", save=False, filepath="_locales.json"):
623
+ def parse_all_locales(path, encoding="utf_8_sig", language="english", save=False, output="_locales.json"):
606
624
  """
607
625
  Parse all locales strings
608
626
  :param path: Path where to find locale files
609
627
  :param encoding: Encoding for reading files
610
628
  :param language: Target language
611
629
  :param save: (default false) save locales in file
612
- :param filepath: locales output file location
630
+ :param output: locales output file location
613
631
  :return: Locales in dictionary
614
632
  """
615
633
  locales = {}
@@ -637,7 +655,7 @@ def parse_all_locales(path, encoding="utf_8_sig", language="english", save=False
637
655
  key, _, value = match.groups()
638
656
  locales[key] = value
639
657
  if save:
640
- with open(filepath, "w") as file:
658
+ with open(output, "w", encoding="utf-8") as file:
641
659
  json_dump(locales, file, sort_keys=True)
642
660
  return locales
643
661
 
@@ -812,8 +830,8 @@ def revert_file(path, output_dir=None, encoding="utf_8_sig", base_dir=None, save
812
830
  base_dir = os.sep.join(str(base_dir).rstrip(os.sep).split(os.sep)[:-1]) + os.sep
813
831
  base_dir = os.path.dirname(path.replace(base_dir, ""))
814
832
  base_dir = base_dir or "."
815
- with open(path) as file:
816
- data = json_load(file)
833
+ with open(path, "r", encoding="utf-8") as file:
834
+ data = json.load(file)
817
835
  filename = os.path.join(str(base_dir), os.path.basename(path))
818
836
  logger.debug(f"Reverting {filename}")
819
837
  text = revert(data)
@@ -836,8 +854,8 @@ def load_variables(filepath="_variables.json"):
836
854
  global global_variables
837
855
  if not os.path.exists(filepath):
838
856
  return
839
- with open(filepath) as file:
840
- global_variables = json_load(file)
857
+ with open(filepath, "r", encoding="utf-8") as file:
858
+ global_variables = json.load(file)
841
859
 
842
860
 
843
861
  def save_variables(filepath="_variables.json"):
@@ -846,7 +864,7 @@ def save_variables(filepath="_variables.json"):
846
864
  """
847
865
  if not global_variables:
848
866
  return
849
- with open(filepath, "w") as file:
867
+ with open(filepath, "w", encoding="utf-8") as file:
850
868
  json_dump(global_variables, file, sort_keys=True)
851
869
 
852
870
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ckparser"
7
- version = "0.2"
7
+ version = "0.3"
8
8
  description = "Parse Paradox Jomini data files to Python/JSON and revert them back experimentally."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes
File without changes
File without changes