Hide keyboard shortcuts

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 

3 

4class StringBuilder: 

5 """Easily create a string from strings and numbers. 

6 

7 """ 

8 

9 _data: [char] 

10 

11 func __init__(self): 

12 self._data = [] 

13 

14 func +=(self, data: string): 

15 """Append given string. 

16 

17 """ 

18 

19 for ch in data: 

20 self += ch 

21 

22 func +=(self, data: char): 

23 """Append given character. 

24 

25 """ 

26 

27 self._data.append(data) 

28 

29 func +=(self, data: i8): 

30 """Append given interger. 

31 

32 """ 

33 

34 self += str(data) 

35 

36 func +=(self, data: i16): 

37 """Append given interger. 

38 

39 """ 

40 

41 self += str(data) 

42 

43 func +=(self, data: i32): 

44 """Append given interger. 

45 

46 """ 

47 

48 self += str(data) 

49 

50 func +=(self, data: i64): 

51 """Append given interger. 

52 

53 """ 

54 

55 self += str(data) 

56 

57 func +=(self, data: u8): 

58 """Append given unsigned interger. 

59 

60 """ 

61 

62 self += str(data) 

63 

64 func +=(self, data: u16): 

65 """Append given unsigned interger. 

66 

67 """ 

68 

69 self += str(data) 

70 

71 func +=(self, data: u32): 

72 """Append given unsigned interger. 

73 

74 """ 

75 

76 self += str(data) 

77 

78 func +=(self, data: u64): 

79 """Append given unsigned interger. 

80 

81 """ 

82 

83 self += str(data) 

84 

85 func to_string(self) -> string: 

86 """Returns a string of current data. 

87 

88 """ 

89 

90 a = [str(ch) for ch in self._data] 

91 

92 return "".join(a) 

93 

94 func length(self) -> i64: 

95 """Get current string length. 

96 

97 """ 

98 

99 return self._data.length() 

100 

101 func clear(self): 

102 """Clear everything. 

103 

104 """ 

105 

106 self._data = [] 

107 

108 func clear_from(self, offset: i64): 

109 """Clear everything after given offset. 

110 

111 """ 

112 

113 while self.length() > offset: 

114 self._data.pop() 

115 

116class StringReader: 

117 """A string reader. 

118 

119 """ 

120 

121 _data: string 

122 _pos: i64 

123 

124 func __init__(self, data: string): 

125 self._data = data 

126 self._pos = 0 

127 

128 func available(self) -> i64: 

129 """Returns number of available characters. 

130 

131 """ 

132 

133 return self._data.length() - self._pos 

134 

135 func seek(self, pos: i64): 

136 """Seek given position relative to the beginning of the string. 

137 

138 """ 

139 

140 if pos > self._data.length(): 

141 pos = self._data.length() 

142 

143 self._pos = pos 

144 

145 func tell(self) -> i64: 

146 """Tell current position in the string. 

147 

148 """ 

149 

150 return self._pos 

151 

152 func get(self) -> char: 

153 """Get the next character. Returns '' if there are no more characters 

154 available. 

155 

156 """ 

157 

158 if self.available() == 0: 

159 return '' 

160 

161 self._pos += 1 

162 

163 return self._data[self._pos - 1] 

164 

165 func peek(self) -> char: 

166 """Peek at the next character. 

167 

168 """ 

169 

170 if self.available() == 0: 

171 return '' 

172 

173 return self._data[self._pos] 

174 

175 func read(self, size: i64 = -1) -> string: 

176 """Read zero or more characters. Give size as -1 to read everything. 

177 

178 """ 

179 

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() 

188 

189 return data 

190 

191 func unget(self): 

192 """Unget last character. 

193 

194 """ 

195 

196 if self._pos > 0: 

197 self._pos -= 1 

198 

199class _Pretty: 

200 _builder: StringBuilder 

201 _reader: StringReader? 

202 _width: i64 

203 _indent: i64 

204 _current_line_length: i64 

205 

206 func __init__(self, width: i64 = 80): 

207 self._builder = StringBuilder() 

208 self._reader = None 

209 self._width = width 

210 

211 func _find_string_and_char_end(self, begin_ch: char): 

212 while True: 

213 ch = self._reader.get() 

214 

215 if ch == begin_ch: 

216 break 

217 

218 if ch == '\\': 

219 ch = self._reader.get() 

220 

221 if ch == '': 

222 raise StringError(f"out of data when searching for '{begin_ch}'") 

223 

224 func _find_end(self, begin_ch: char, end_ch: char) -> i64: 

225 begin_pos = self._reader.tell() 

226 level = 1 

227 

228 while True: 

229 ch = self._reader.get() 

230 

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}'") 

241 

242 if level == 0 and ch == end_ch: 

243 break 

244 

245 end_pos = self._reader.tell() 

246 self._reader.seek(begin_pos) 

247 

248 return end_pos - begin_pos 

249 

250 func _process_bracket_and_paren_begin(self, begin_ch: char, end_ch: char): 

251 width = self._find_end(begin_ch, end_ch) 

252 

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))]) 

263 

264 func _process_bracket_begin(self): 

265 self._process_bracket_and_paren_begin('[', ']') 

266 

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 

273 

274 func _process_paren_begin(self): 

275 self._process_bracket_and_paren_begin('(', ')') 

276 

277 func _process_paren_end(self): 

278 self._indent -= 4 

279 self._builder += ')' 

280 self._current_line_length += 1 

281 

282 func _process_string_and_char(self, begin_ch: char): 

283 self._builder += begin_ch 

284 

285 while True: 

286 ch = self._reader.get() 

287 

288 if ch == begin_ch: 

289 self._builder += ch 

290 self._current_line_length += 1 

291 break 

292 

293 if ch == '\\': 

294 self._builder += ch 

295 self._current_line_length += 1 

296 ch = self._reader.get() 

297 

298 if ch == '': 

299 raise StringError(f"out of data when searching for '{begin_ch}'") 

300 

301 self._builder += ch 

302 self._current_line_length += 1 

303 

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))]) 

308 

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 

314 

315 while True: 

316 ch = self._reader.get() 

317 

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 

340 

341 return self._builder.to_string() 

342 

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. 

347 

348 """ 

349 

350 return _Pretty().process(data) 

351 

352func indent(text: string, prefix: string = " ") -> string: 

353 """Add given prefix to the beginning of given lines. 

354 

355 The prefix is added to all lines that do not consist solely of 

356 whitespace. 

357 

358 """ 

359 

360 lines: [string] = [] 

361 

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) 

367 

368 return "\n".join(lines) 

369 

370func join_or(items: [string]) -> string: 

371 """Join all items in given list as "item1, item2, item3 or item4". 

372 

373 """ 

374 

375 return _join_delimited(items, "or") 

376 

377func join_and(items: [string]) -> string: 

378 """Join all items in given list as "item1, item2, item3 and item4". 

379 

380 """ 

381 

382 return _join_delimited(items, "and") 

383 

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() 

391 

392 for i, item in enumerate(slice(items, 0, -1)): 

393 if i > 0: 

394 joined += ", " 

395 

396 joined += item 

397 

398 joined += ' ' 

399 joined += last_delim 

400 joined += ' ' 

401 joined += items[-1] 

402 

403 return joined.to_string() 

404 

405test empty(): 

406 assert StringBuilder().to_string() == "" 

407 

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' 

429 

430 assert numbers.to_string() == "string -1 -2 -3 -4 1 2 3 4 C" 

431 

432test clear(): 

433 numbers = StringBuilder() 

434 numbers += "foo" 

435 assert numbers.to_string() == "foo" 

436 

437 numbers.clear() 

438 assert numbers.to_string() == "" 

439 

440test clear_from_and_length(): 

441 numbers = StringBuilder() 

442 assert numbers.length() == 0 

443 numbers.clear_from(1) 

444 assert numbers.length() == 0 

445 

446 numbers += "foo" 

447 assert numbers.to_string() == "foo" 

448 assert numbers.length() == 3 

449 

450 numbers.clear_from(1) 

451 assert numbers.to_string() == "f" 

452 assert numbers.length() == 1 

453 

454 numbers += "a" 

455 assert numbers.to_string() == "fa" 

456 

457 numbers.clear_from(0) 

458 assert numbers.to_string() == "" 

459 assert numbers.length() == 0 

460 

461 numbers += "apa" 

462 assert numbers.to_string() == "apa" 

463 assert numbers.length() == 3 

464 

465test empty_string_reader(): 

466 reader = StringReader("") 

467 assert reader.get() == '' 

468 assert reader.read(1) == "" 

469 assert reader.read() == "" 

470 

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() == '' 

487 

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 "]") 

504 

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 "]") 

521 

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 " ])") 

541 

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 " ])") 

558 

559test pretty_long_string(): 

560 data = ( 

561 "\"12345678901234567890123456789012345678901234567890123456789012345678901" 

562 "23456789012345678901234567890\"") 

563 assert pretty(data) == data 

564 

565test pretty_long_name(): 

566 assert pretty( 

567 "12345678901234567890123456789012345678901234567890123456789012345678901" 

568 "23456789012345678901234567890()") == ( 

569 "1234567890123456789012345678901234567890123456789012345678901234567" 

570 "890123456789012345678901234567890(\n" 

571 " )") 

572 

573test pretty_short(): 

574 data = "a(b=(1, 3, 'a'), c=[])" 

575 assert pretty(data) == data 

576 

577test pretty_char_error(): 

578 try: 

579 message = "" 

580 pretty("'''") 

581 except StringError as e: 

582 message = e.message 

583 

584 assert message == "out of data when searching for '''" 

585 

586test indent_empty_string(): 

587 assert indent("", " ") == "" 

588 

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") 

598 

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") 

608 

609test big(): 

610 s = StringBuilder() 

611 

612 for _ in range(1000): 

613 s += "0123456789" 

614 

615 print(s.length(), flush=True) 

616 print(s.to_string().length(), flush=True) 

617 

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" 

625 

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"