Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1class StringError(Error):
2 message: string
4class StringBuilder:
5 """Easily create a string from strings and numbers.
7 """
9 _data: [char]
11 func __init__(self):
12 self._data = []
14 func +=(self, data: string):
15 """Append given string.
17 """
19 for ch in data:
20 self += ch
22 func +=(self, data: char):
23 """Append given character.
25 """
27 self._data.append(data)
29 func +=(self, data: i8):
30 """Append given interger.
32 """
34 self += str(data)
36 func +=(self, data: i16):
37 """Append given interger.
39 """
41 self += str(data)
43 func +=(self, data: i32):
44 """Append given interger.
46 """
48 self += str(data)
50 func +=(self, data: i64):
51 """Append given interger.
53 """
55 self += str(data)
57 func +=(self, data: u8):
58 """Append given unsigned interger.
60 """
62 self += str(data)
64 func +=(self, data: u16):
65 """Append given unsigned interger.
67 """
69 self += str(data)
71 func +=(self, data: u32):
72 """Append given unsigned interger.
74 """
76 self += str(data)
78 func +=(self, data: u64):
79 """Append given unsigned interger.
81 """
83 self += str(data)
85 func to_string(self) -> string:
86 """Returns a string of current data.
88 """
90 a = [str(ch) for ch in self._data]
92 return "".join(a)
94 func length(self) -> i64:
95 """Get current string length.
97 """
99 return self._data.length()
101 func clear(self):
102 """Clear everything.
104 """
106 self._data = []
108 func clear_from(self, offset: i64):
109 """Clear everything after given offset.
111 """
113 while self.length() > offset:
114 self._data.pop()
116class StringReader:
117 """A string reader.
119 """
121 _data: string
122 _pos: i64
124 func __init__(self, data: string):
125 self._data = data
126 self._pos = 0
128 func available(self) -> i64:
129 """Returns number of available characters.
131 """
133 return self._data.length() - self._pos
135 func seek(self, pos: i64):
136 """Seek given position relative to the beginning of the string.
138 """
140 if pos > self._data.length():
141 pos = self._data.length()
143 self._pos = pos
145 func tell(self) -> i64:
146 """Tell current position in the string.
148 """
150 return self._pos
152 func get(self) -> char:
153 """Get the next character. Returns '' if there are no more characters
154 available.
156 """
158 if self.available() == 0:
159 return ''
161 self._pos += 1
163 return self._data[self._pos - 1]
165 func peek(self) -> char:
166 """Peek at the next character.
168 """
170 if self.available() == 0:
171 return ''
173 return self._data[self._pos]
175 func read(self, size: i64 = -1) -> string:
176 """Read zero or more characters. Give size as -1 to read everything.
178 """
180 if self.available() == 0:
181 data = ""
182 elif size == -1:
183 data = self._data[self._pos:]
184 self._pos = self._data.length()
185 else:
186 data = self._data[self._pos:self._pos + size]
187 self._pos += data.length()
189 return data
191 func unget(self):
192 """Unget last character.
194 """
196 if self._pos > 0:
197 self._pos -= 1
199class _Pretty:
200 _builder: StringBuilder
201 _reader: StringReader?
202 _width: i64
203 _indent: i64
204 _current_line_length: i64
206 func __init__(self, width: i64 = 80):
207 self._builder = StringBuilder()
208 self._reader = None
209 self._width = width
211 func _find_string_and_char_end(self, begin_ch: char):
212 while True:
213 ch = self._reader.get()
215 if ch == begin_ch:
216 break
218 if ch == '\\':
219 ch = self._reader.get()
221 if ch == '':
222 raise StringError(f"out of data when searching for '{begin_ch}'")
224 func _find_end(self, begin_ch: char, end_ch: char) -> i64:
225 begin_pos = self._reader.tell()
226 level = 1
228 while True:
229 ch = self._reader.get()
231 if ch == begin_ch:
232 level += 1
233 elif ch == end_ch:
234 level -= 1
235 elif ch == '"':
236 self._find_string_and_char_end(ch)
237 elif ch == '\'':
238 self._find_string_and_char_end(ch)
239 elif ch == '':
240 raise StringError(f"out of data when searching for '{end_ch}'")
242 if level == 0 and ch == end_ch:
243 break
245 end_pos = self._reader.tell()
246 self._reader.seek(begin_pos)
248 return end_pos - begin_pos
250 func _process_bracket_and_paren_begin(self, begin_ch: char, end_ch: char):
251 width = self._find_end(begin_ch, end_ch)
253 if (width + self._indent + self._current_line_length) < 80:
254 self._builder += begin_ch
255 self._builder += self._reader.read(i64(width))
256 self._current_line_length += width
257 else:
258 self._builder += begin_ch
259 self._builder += '\n'
260 self._current_line_length = 0
261 self._indent += 4
262 self._builder += "".join([" " for _ in range(i64(self._indent))])
264 func _process_bracket_begin(self):
265 self._process_bracket_and_paren_begin('[', ']')
267 func _process_bracket_end(self):
268 self._indent -= 4
269 self._builder += '\n'
270 self._builder += "".join([" " for _ in range(i64(self._indent))])
271 self._builder += ']'
272 self._current_line_length = 1
274 func _process_paren_begin(self):
275 self._process_bracket_and_paren_begin('(', ')')
277 func _process_paren_end(self):
278 self._indent -= 4
279 self._builder += ')'
280 self._current_line_length += 1
282 func _process_string_and_char(self, begin_ch: char):
283 self._builder += begin_ch
285 while True:
286 ch = self._reader.get()
288 if ch == begin_ch:
289 self._builder += ch
290 self._current_line_length += 1
291 break
293 if ch == '\\':
294 self._builder += ch
295 self._current_line_length += 1
296 ch = self._reader.get()
298 if ch == '':
299 raise StringError(f"out of data when searching for '{begin_ch}'")
301 self._builder += ch
302 self._current_line_length += 1
304 func _process_comma(self):
305 self._builder += ",\n"
306 self._current_line_length = 0
307 self._builder += "".join([" " for _ in range(i64(self._indent))])
309 func process(self, data: string) -> string:
310 self._builder.clear()
311 self._reader = StringReader(data)
312 self._indent = 0
313 self._current_line_length = 0
315 while True:
316 ch = self._reader.get()
318 match ch:
319 case '[':
320 self._process_bracket_begin()
321 case ']':
322 self._process_bracket_end()
323 case '(':
324 self._process_paren_begin()
325 case ')':
326 self._process_paren_end()
327 case '"':
328 self._process_string_and_char(ch)
329 case '\'':
330 self._process_string_and_char(ch)
331 case ',':
332 self._process_comma()
333 case ' ':
334 pass
335 case '':
336 break
337 case _:
338 self._builder += ch
339 self._current_line_length += 1
341 return self._builder.to_string()
343func pretty(data: string) -> string:
344 """Try to make given object dump string pretty by inserting newlines
345 and indentations based on brackets, parentheses, strings,
346 characters and commas.
348 """
350 return _Pretty().process(data)
352func indent(text: string, prefix: string = " ") -> string:
353 """Add given prefix to the beginning of given lines.
355 The prefix is added to all lines that do not consist solely of
356 whitespace.
358 """
360 lines: [string] = []
362 for line in text.split("\n"):
363 if line.is_space() or line == "":
364 lines.append(line)
365 else:
366 lines.append(prefix + line)
368 return "\n".join(lines)
370func join_or(items: [string]) -> string:
371 """Join all items in given list as "item1, item2, item3 or item4".
373 """
375 return _join_delimited(items, "or")
377func join_and(items: [string]) -> string:
378 """Join all items in given list as "item1, item2, item3 and item4".
380 """
382 return _join_delimited(items, "and")
384func _join_delimited(items: [string], last_delim: string) -> string:
385 if items.length() == 0:
386 return ""
387 elif items.length() == 1:
388 return items[0]
389 else:
390 joined = StringBuilder()
392 for i, item in enumerate(slice(items, 0, -1)):
393 if i > 0:
394 joined += ", "
396 joined += item
398 joined += ' '
399 joined += last_delim
400 joined += ' '
401 joined += items[-1]
403 return joined.to_string()
405test empty():
406 assert StringBuilder().to_string() == ""
408test append():
409 numbers = StringBuilder()
410 numbers += "string"
411 numbers += " "
412 numbers += i8(-1)
413 numbers += " "
414 numbers += i16(-2)
415 numbers += " "
416 numbers += i32(-3)
417 numbers += " "
418 numbers += i64(-4)
419 numbers += " "
420 numbers += u8(1)
421 numbers += " "
422 numbers += u16(2)
423 numbers += " "
424 numbers += u32(3)
425 numbers += " "
426 numbers += u64(4)
427 numbers += " "
428 numbers += 'C'
430 assert numbers.to_string() == "string -1 -2 -3 -4 1 2 3 4 C"
432test clear():
433 numbers = StringBuilder()
434 numbers += "foo"
435 assert numbers.to_string() == "foo"
437 numbers.clear()
438 assert numbers.to_string() == ""
440test clear_from_and_length():
441 numbers = StringBuilder()
442 assert numbers.length() == 0
443 numbers.clear_from(1)
444 assert numbers.length() == 0
446 numbers += "foo"
447 assert numbers.to_string() == "foo"
448 assert numbers.length() == 3
450 numbers.clear_from(1)
451 assert numbers.to_string() == "f"
452 assert numbers.length() == 1
454 numbers += "a"
455 assert numbers.to_string() == "fa"
457 numbers.clear_from(0)
458 assert numbers.to_string() == ""
459 assert numbers.length() == 0
461 numbers += "apa"
462 assert numbers.to_string() == "apa"
463 assert numbers.length() == 3
465test empty_string_reader():
466 reader = StringReader("")
467 assert reader.get() == ''
468 assert reader.read(1) == ""
469 assert reader.read() == ""
471test string_reader():
472 reader = StringReader("kalle kula")
473 assert reader.peek() == 'k'
474 assert reader.get() == 'k'
475 assert reader.get() == 'a'
476 assert reader.read(3) == "lle"
477 assert reader.get() == ' '
478 assert reader.read() == "kula"
479 assert reader.get() == ''
480 assert reader.peek() == ''
481 assert reader.read() == ""
482 assert reader.read(100) == ""
483 reader.unget()
484 assert reader.available() == 1
485 assert reader.get() == 'a'
486 assert reader.get() == ''
488test pretty_tokens():
489 assert pretty(
490 "[ParenToken(value='('), NameToken(value=\"add\"), NumberToken(value=\"2\"),"
491 " ParenToken(value='('), NameToken(value=\"subtract\"), NumberToken(value=\""
492 "4\"), NumberToken(value=\"2\"), ParenToken(value=')'), ParenToken(value=')'"
493 ")]") == ("[\n"
494 " ParenToken(value='('),\n"
495 " NameToken(value=\"add\"),\n"
496 " NumberToken(value=\"2\"),\n"
497 " ParenToken(value='('),\n"
498 " NameToken(value=\"subtract\"),\n"
499 " NumberToken(value=\"4\"),\n"
500 " NumberToken(value=\"2\"),\n"
501 " ParenToken(value=')'),\n"
502 " ParenToken(value=')')\n"
503 "]")
505test pretty_tokens_2():
506 assert pretty(
507 "[ParenToken(value='\\''), NameToken(value=\"add\"), NumberToken(value=\"2\"),"
508 " ParenToken(value='('), NameToken(value=\"subtract\"), NumberToken(value=\""
509 "4\"), NumberToken(value=\"2\"), ParenToken(value=')'), ParenToken(value=')'"
510 ")]") == ("[\n"
511 " ParenToken(value='\\''),\n"
512 " NameToken(value=\"add\"),\n"
513 " NumberToken(value=\"2\"),\n"
514 " ParenToken(value='('),\n"
515 " NameToken(value=\"subtract\"),\n"
516 " NumberToken(value=\"4\"),\n"
517 " NumberToken(value=\"2\"),\n"
518 " ParenToken(value=')'),\n"
519 " ParenToken(value=')')\n"
520 "]")
522test pretty_ast():
523 assert pretty(
524 "ProgramNode(body=[CallExpressionNode(name=\"add\", params=[NumberLiteralNod"
525 "e(value=\"2\"), CallExpressionNode(name=\"subtract\", params=[NumberLiteral"
526 "Node(value=\"4\"), NumberLiteralNode(value=\"2\")])])])") == (
527 "ProgramNode(\n"
528 " body=[\n"
529 " CallExpressionNode(\n"
530 " name=\"add\",\n"
531 " params=[\n"
532 " NumberLiteralNode(value=\"2\"),\n"
533 " CallExpressionNode(\n"
534 " name=\"subtract\",\n"
535 " params=[\n"
536 " NumberLiteralNode(value=\"4\"),\n"
537 " NumberLiteralNode(value=\"2\")\n"
538 " ])\n"
539 " ])\n"
540 " ])")
542test pretty_ast_2():
543 assert pretty(
544 "ProgramNode(body=[CallExpressionNode(name=\"a[)\", params=[NumberLiteralNod"
545 "e(value=\"2\"), CallExpressionNode(name=\"subt'act\", params=[NumberLiteral"
546 "Node(value=\"4\")])])])") == (
547 "ProgramNode(\n"
548 " body=[\n"
549 " CallExpressionNode(\n"
550 " name=\"a[)\",\n"
551 " params=[\n"
552 " NumberLiteralNode(value=\"2\"),\n"
553 " CallExpressionNode(\n"
554 " name=\"subt'act\",\n"
555 " params=[NumberLiteralNode(value=\"4\")])\n"
556 " ])\n"
557 " ])")
559test pretty_long_string():
560 data = (
561 "\"12345678901234567890123456789012345678901234567890123456789012345678901"
562 "23456789012345678901234567890\"")
563 assert pretty(data) == data
565test pretty_long_name():
566 assert pretty(
567 "12345678901234567890123456789012345678901234567890123456789012345678901"
568 "23456789012345678901234567890()") == (
569 "1234567890123456789012345678901234567890123456789012345678901234567"
570 "890123456789012345678901234567890(\n"
571 " )")
573test pretty_short():
574 data = "a(b=(1, 3, 'a'), c=[])"
575 assert pretty(data) == data
577test pretty_char_error():
578 try:
579 message = ""
580 pretty("'''")
581 except StringError as e:
582 message = e.message
584 assert message == "out of data when searching for '''"
586test indent_empty_string():
587 assert indent("", " ") == ""
589test indent_ending_with_newline():
590 text = ("1\n"
591 "2\n"
592 "\n"
593 "4\n")
594 assert indent(text, " ") == (" 1\n"
595 " 2\n"
596 "\n"
597 " 4\n")
599test indent_whitespace_in_line():
600 text = ("\t\n"
601 "1\n"
602 " \n"
603 "4")
604 assert indent(text, "XXX") == ("\t\n"
605 "XXX1\n"
606 " \n"
607 "XXX4")
609test big():
610 s = StringBuilder()
612 for _ in range(1000):
613 s += "0123456789"
615 print(s.length(), flush=True)
616 print(s.to_string().length(), flush=True)
618test join_or():
619 assert join_or([]) == ""
620 assert join_or([""]) == ""
621 assert join_or(["a"]) == "a"
622 assert join_or(["a", "b"]) == "a or b"
623 assert join_or(["1", "2", "3"]) == "1, 2 or 3"
624 assert join_or(["1", "2", "3", "4"]) == "1, 2, 3 or 4"
626test join_and():
627 assert join_and([]) == ""
628 assert join_and([""]) == ""
629 assert join_and(["a"]) == "a"
630 assert join_and(["a", "b"]) == "a and b"
631 assert join_and(["1", "2", "3"]) == "1, 2 and 3"
632 assert join_and(["1", "2", "3", "4"]) == "1, 2, 3 and 4"