1 | """
|
2 | format.py -- Pretty print an ASDL data structure.
|
3 |
|
4 | TODO: replace ad hoc line wrapper, e.g. _TrySingleLine
|
5 |
|
6 | - auto-abbreviation of single field things (minus location)
|
7 | - option to omit spaces for SQ, SQ, W? It's all one thing.
|
8 |
|
9 | Where we try wrap to a single line:
|
10 | - arrays
|
11 | - objects with name fields
|
12 | - abbreviated, unnamed fields
|
13 | """
|
14 | from typing import Tuple, List
|
15 |
|
16 | from _devbuild.gen.hnode_asdl import (hnode, hnode_e, hnode_t, hnode_str,
|
17 | color_e, color_t)
|
18 | from data_lang import j8_lite
|
19 | from display import ansi
|
20 | from display import pretty
|
21 | from pylib import cgi
|
22 | from mycpp import mylib
|
23 |
|
24 | from typing import cast, Any, Optional
|
25 |
|
26 | if mylib.PYTHON:
|
27 |
|
28 | def PrettyPrint(obj, f=None):
|
29 | # type: (Any, Optional[mylib.Writer]) -> None
|
30 | """Print abbreviated tree in color. For unit tests."""
|
31 | f = f if f else mylib.Stdout()
|
32 |
|
33 | ast_f = DetectConsoleOutput(f)
|
34 | tree = obj.AbbreviatedTree()
|
35 | PrintTree(tree, ast_f)
|
36 |
|
37 |
|
38 | def DetectConsoleOutput(f):
|
39 | # type: (mylib.Writer) -> ColorOutput
|
40 | """Wrapped to auto-detect."""
|
41 | if f.isatty():
|
42 | return AnsiOutput(f)
|
43 | else:
|
44 | return TextOutput(f)
|
45 |
|
46 |
|
47 | class ColorOutput(object):
|
48 | """Abstract base class for plain text, ANSI color, and HTML color."""
|
49 |
|
50 | def __init__(self, f):
|
51 | # type: (mylib.Writer) -> None
|
52 | self.f = f
|
53 | self.num_chars = 0
|
54 |
|
55 | def NewTempBuffer(self):
|
56 | # type: () -> ColorOutput
|
57 | """Return a temporary buffer for the line wrapping calculation."""
|
58 | raise NotImplementedError()
|
59 |
|
60 | def FileHeader(self):
|
61 | # type: () -> None
|
62 | """Hook for printing a full file."""
|
63 | pass
|
64 |
|
65 | def FileFooter(self):
|
66 | # type: () -> None
|
67 | """Hook for printing a full file."""
|
68 | pass
|
69 |
|
70 | def PushColor(self, e_color):
|
71 | # type: (color_t) -> None
|
72 | raise NotImplementedError()
|
73 |
|
74 | def PopColor(self):
|
75 | # type: () -> None
|
76 | raise NotImplementedError()
|
77 |
|
78 | def write(self, s):
|
79 | # type: (str) -> None
|
80 | self.f.write(s)
|
81 | self.num_chars += len(s) # Only count visible characters!
|
82 |
|
83 | def WriteRaw(self, raw):
|
84 | # type: (Tuple[str, int]) -> None
|
85 | """Write raw data without escaping, and without counting control codes
|
86 | in the length."""
|
87 | s, num_chars = raw
|
88 | self.f.write(s)
|
89 | self.num_chars += num_chars
|
90 |
|
91 | def NumChars(self):
|
92 | # type: () -> int
|
93 | return self.num_chars
|
94 |
|
95 | def GetRaw(self):
|
96 | # type: () -> Tuple[str, int]
|
97 |
|
98 | # NOTE: Ensured by NewTempBuffer()
|
99 | f = cast(mylib.BufWriter, self.f)
|
100 | return f.getvalue(), self.num_chars
|
101 |
|
102 |
|
103 | class TextOutput(ColorOutput):
|
104 | """TextOutput put obeys the color interface, but outputs nothing."""
|
105 |
|
106 | def __init__(self, f):
|
107 | # type: (mylib.Writer) -> None
|
108 | ColorOutput.__init__(self, f)
|
109 |
|
110 | def NewTempBuffer(self):
|
111 | # type: () -> TextOutput
|
112 | return TextOutput(mylib.BufWriter())
|
113 |
|
114 | def PushColor(self, e_color):
|
115 | # type: (color_t) -> None
|
116 | pass # ignore color
|
117 |
|
118 | def PopColor(self):
|
119 | # type: () -> None
|
120 | pass # ignore color
|
121 |
|
122 |
|
123 | class HtmlOutput(ColorOutput):
|
124 | """HTML one can have wider columns. Maybe not even fixed-width font. Hm
|
125 | yeah indentation should be logical then?
|
126 |
|
127 | Color: HTML spans
|
128 | """
|
129 |
|
130 | def __init__(self, f):
|
131 | # type: (mylib.Writer) -> None
|
132 | ColorOutput.__init__(self, f)
|
133 |
|
134 | def NewTempBuffer(self):
|
135 | # type: () -> HtmlOutput
|
136 | return HtmlOutput(mylib.BufWriter())
|
137 |
|
138 | def FileHeader(self):
|
139 | # type: () -> None
|
140 | # TODO: Use a different CSS file to make the colors match. I like string
|
141 | # literals as yellow, etc.
|
142 | #<link rel="stylesheet" type="text/css" href="/css/code.css" />
|
143 | self.f.write("""
|
144 | <html>
|
145 | <head>
|
146 | <title>Oils AST</title>
|
147 | <style>
|
148 | .n { color: brown }
|
149 | .s { font-weight: bold }
|
150 | .o { color: darkgreen }
|
151 | </style>
|
152 | </head>
|
153 | <body>
|
154 | <pre>
|
155 | """)
|
156 |
|
157 | def FileFooter(self):
|
158 | # type: () -> None
|
159 | self.f.write("""
|
160 | </pre>
|
161 | </body>
|
162 | </html>
|
163 | """)
|
164 |
|
165 | def PushColor(self, e_color):
|
166 | # type: (color_t) -> None
|
167 | # To save bandwidth, use single character CSS names.
|
168 | if e_color == color_e.TypeName:
|
169 | css_class = 'n'
|
170 | elif e_color == color_e.StringConst:
|
171 | css_class = 's'
|
172 | elif e_color == color_e.OtherConst:
|
173 | css_class = 'o'
|
174 | elif e_color == color_e.External:
|
175 | css_class = 'o'
|
176 | elif e_color == color_e.UserType:
|
177 | css_class = 'o'
|
178 | else:
|
179 | raise AssertionError(e_color)
|
180 | self.f.write('<span class="%s">' % css_class)
|
181 |
|
182 | def PopColor(self):
|
183 | # type: () -> None
|
184 | self.f.write('</span>')
|
185 |
|
186 | def write(self, s):
|
187 | # type: (str) -> None
|
188 |
|
189 | # PROBLEM: Double escaping!
|
190 | self.f.write(cgi.escape(s))
|
191 | self.num_chars += len(s) # Only count visible characters!
|
192 |
|
193 |
|
194 | class AnsiOutput(ColorOutput):
|
195 | """For the console."""
|
196 |
|
197 | def __init__(self, f):
|
198 | # type: (mylib.Writer) -> None
|
199 | ColorOutput.__init__(self, f)
|
200 |
|
201 | def NewTempBuffer(self):
|
202 | # type: () -> AnsiOutput
|
203 | return AnsiOutput(mylib.BufWriter())
|
204 |
|
205 | def PushColor(self, e_color):
|
206 | # type: (color_t) -> None
|
207 | if e_color == color_e.TypeName:
|
208 | self.f.write(ansi.YELLOW)
|
209 | elif e_color == color_e.StringConst:
|
210 | self.f.write(ansi.BOLD)
|
211 | elif e_color == color_e.OtherConst:
|
212 | self.f.write(ansi.GREEN)
|
213 | elif e_color == color_e.External:
|
214 | self.f.write(ansi.BOLD + ansi.BLUE)
|
215 | elif e_color == color_e.UserType:
|
216 | self.f.write(ansi.GREEN) # Same color as other literals for now
|
217 | else:
|
218 | raise AssertionError(e_color)
|
219 |
|
220 | def PopColor(self):
|
221 | # type: () -> None
|
222 | self.f.write(ansi.RESET)
|
223 |
|
224 |
|
225 | INDENT = 2
|
226 |
|
227 |
|
228 | class _PrettyPrinter(object):
|
229 |
|
230 | def __init__(self, max_col):
|
231 | # type: (int) -> None
|
232 | self.max_col = max_col
|
233 |
|
234 | def _PrintWrappedArray(self, array, prefix_len, f, indent):
|
235 | # type: (List[hnode_t], int, ColorOutput, int) -> bool
|
236 | """Print an array of objects with line wrapping.
|
237 |
|
238 | Returns whether they all fit on a single line, so you can print
|
239 | the closing brace properly.
|
240 | """
|
241 | all_fit = True
|
242 | chars_so_far = prefix_len
|
243 |
|
244 | for i, val in enumerate(array):
|
245 | if i != 0:
|
246 | f.write(' ')
|
247 |
|
248 | single_f = f.NewTempBuffer()
|
249 | if _TrySingleLine(val, single_f, self.max_col - chars_so_far):
|
250 | s, num_chars = single_f.GetRaw() # extra unpacking for mycpp
|
251 | f.WriteRaw((s, num_chars))
|
252 | chars_so_far += single_f.NumChars()
|
253 | else: # WRAP THE LINE
|
254 | f.write('\n')
|
255 | self.PrintNode(val, f, indent + INDENT)
|
256 |
|
257 | chars_so_far = 0 # allow more
|
258 | all_fit = False
|
259 | return all_fit
|
260 |
|
261 | def _PrintWholeArray(self, array, prefix_len, f, indent):
|
262 | # type: (List[hnode_t], int, ColorOutput, int) -> bool
|
263 |
|
264 | # This is UNLIKE the abbreviated case above, where we do WRAPPING.
|
265 | # Here, ALL children must fit on a single line, or else we separate
|
266 | # each one onto a separate line. This is to avoid the following:
|
267 | #
|
268 | # children: [(C ...)
|
269 | # (C ...)
|
270 | # ]
|
271 | # The first child is out of line. The abbreviated objects have a
|
272 | # small header like C or DQ so it doesn't matter as much.
|
273 | all_fit = True
|
274 | pieces = [] # type: List[Tuple[str, int]]
|
275 | chars_so_far = prefix_len
|
276 | for item in array:
|
277 | single_f = f.NewTempBuffer()
|
278 | if _TrySingleLine(item, single_f, self.max_col - chars_so_far):
|
279 | s, num_chars = single_f.GetRaw() # extra unpacking for mycpp
|
280 | pieces.append((s, num_chars))
|
281 | chars_so_far += single_f.NumChars()
|
282 | else:
|
283 | all_fit = False
|
284 | break
|
285 |
|
286 | if all_fit:
|
287 | for i, p in enumerate(pieces):
|
288 | if i != 0:
|
289 | f.write(' ')
|
290 | f.WriteRaw(p)
|
291 | f.write(']')
|
292 | return all_fit
|
293 |
|
294 | def _PrintRecord(self, node, f, indent):
|
295 | # type: (hnode.Record, ColorOutput, int) -> None
|
296 | """Print a CompoundObj in abbreviated or normal form."""
|
297 | ind = ' ' * indent
|
298 |
|
299 | if node.abbrev: # abbreviated
|
300 | prefix = ind + node.left
|
301 | f.write(prefix)
|
302 | if len(node.node_type):
|
303 | f.PushColor(color_e.TypeName)
|
304 | f.write(node.node_type)
|
305 | f.PopColor()
|
306 | f.write(' ')
|
307 |
|
308 | prefix_len = len(prefix) + len(node.node_type) + 1
|
309 | all_fit = self._PrintWrappedArray(node.unnamed_fields, prefix_len,
|
310 | f, indent)
|
311 |
|
312 | if not all_fit:
|
313 | f.write('\n')
|
314 | f.write(ind)
|
315 | f.write(node.right)
|
316 |
|
317 | else: # full form like (SimpleCommand ...)
|
318 | f.write(ind + node.left)
|
319 |
|
320 | f.PushColor(color_e.TypeName)
|
321 | f.write(node.node_type)
|
322 | f.PopColor()
|
323 |
|
324 | f.write('\n')
|
325 | for field in node.fields:
|
326 | name = field.name
|
327 | val = field.val
|
328 |
|
329 | ind1 = ' ' * (indent + INDENT)
|
330 | UP_val = val # for mycpp
|
331 | tag = val.tag()
|
332 | if tag == hnode_e.Array:
|
333 | val = cast(hnode.Array, UP_val)
|
334 |
|
335 | name_str = '%s%s: [' % (ind1, name)
|
336 | f.write(name_str)
|
337 | prefix_len = len(name_str)
|
338 |
|
339 | if not self._PrintWholeArray(val.children, prefix_len, f,
|
340 | indent):
|
341 | f.write('\n')
|
342 | for child in val.children:
|
343 | self.PrintNode(child, f, indent + INDENT + INDENT)
|
344 | f.write('\n')
|
345 | f.write('%s]' % ind1)
|
346 |
|
347 | else: # primitive field
|
348 | name_str = '%s%s: ' % (ind1, name)
|
349 | f.write(name_str)
|
350 | prefix_len = len(name_str)
|
351 |
|
352 | # Try to print it on the same line as the field name; otherwise print
|
353 | # it on a separate line.
|
354 | single_f = f.NewTempBuffer()
|
355 | if _TrySingleLine(val, single_f,
|
356 | self.max_col - prefix_len):
|
357 | s, num_chars = single_f.GetRaw(
|
358 | ) # extra unpacking for mycpp
|
359 | f.WriteRaw((s, num_chars))
|
360 | else:
|
361 | f.write('\n')
|
362 | self.PrintNode(val, f, indent + INDENT + INDENT)
|
363 |
|
364 | f.write('\n') # separate fields
|
365 |
|
366 | f.write(ind + node.right)
|
367 |
|
368 | def PrintNode(self, node, f, indent):
|
369 | # type: (hnode_t, ColorOutput, int) -> None
|
370 | """Second step of printing: turn homogeneous tree into a colored
|
371 | string.
|
372 |
|
373 | Args:
|
374 | node: homogeneous tree node
|
375 | f: ColorOutput instance.
|
376 | max_col: don't print past this column number on ANY line
|
377 | NOTE: See asdl/run.sh line-length-hist for a test of this. It's
|
378 | approximate.
|
379 | TODO: Use the terminal width.
|
380 | """
|
381 | ind = ' ' * indent
|
382 |
|
383 | # Try printing on a single line
|
384 | single_f = f.NewTempBuffer()
|
385 | single_f.write(ind)
|
386 | if _TrySingleLine(node, single_f, self.max_col - indent):
|
387 | s, num_chars = single_f.GetRaw() # extra unpacking for mycpp
|
388 | f.WriteRaw((s, num_chars))
|
389 | return
|
390 |
|
391 | UP_node = node # for mycpp
|
392 | tag = node.tag()
|
393 | if tag == hnode_e.Leaf:
|
394 | node = cast(hnode.Leaf, UP_node)
|
395 | f.PushColor(node.color)
|
396 | f.write(j8_lite.EncodeString(node.s, unquoted_ok=True))
|
397 | f.PopColor()
|
398 |
|
399 | elif tag == hnode_e.External:
|
400 | node = cast(hnode.External, UP_node)
|
401 | f.PushColor(color_e.External)
|
402 | if mylib.PYTHON:
|
403 | f.write(repr(node.obj))
|
404 | else:
|
405 | f.write('UNTYPED any')
|
406 | f.PopColor()
|
407 |
|
408 | elif tag == hnode_e.Record:
|
409 | node = cast(hnode.Record, UP_node)
|
410 | self._PrintRecord(node, f, indent)
|
411 |
|
412 | elif tag == hnode_e.AlreadySeen:
|
413 | node = cast(hnode.AlreadySeen, UP_node)
|
414 | # ... means omitting second reference, while --- means a cycle
|
415 | f.write('...0x%s' % mylib.hex_lower(node.heap_id))
|
416 |
|
417 | else:
|
418 | raise AssertionError(node)
|
419 |
|
420 |
|
421 | def _TrySingleLineObj(node, f, max_chars):
|
422 | # type: (hnode.Record, ColorOutput, int) -> bool
|
423 | """Print an object on a single line."""
|
424 | f.write(node.left)
|
425 | if node.abbrev:
|
426 | if len(node.node_type):
|
427 | f.PushColor(color_e.TypeName)
|
428 | f.write(node.node_type)
|
429 | f.PopColor()
|
430 | f.write(' ')
|
431 |
|
432 | for i, val in enumerate(node.unnamed_fields):
|
433 | if i != 0:
|
434 | f.write(' ')
|
435 | if not _TrySingleLine(val, f, max_chars):
|
436 | return False
|
437 | else:
|
438 | f.PushColor(color_e.TypeName)
|
439 | f.write(node.node_type)
|
440 | f.PopColor()
|
441 |
|
442 | for field in node.fields:
|
443 | f.write(' %s:' % field.name)
|
444 | if not _TrySingleLine(field.val, f, max_chars):
|
445 | return False
|
446 |
|
447 | f.write(node.right)
|
448 | return True
|
449 |
|
450 |
|
451 | def _TrySingleLine(node, f, max_chars):
|
452 | # type: (hnode_t, ColorOutput, int) -> bool
|
453 | """Try printing on a single line.
|
454 |
|
455 | Args:
|
456 | node: homogeneous tree node
|
457 | f: ColorOutput instance
|
458 | max_chars: maximum number of characters to print on THIS line
|
459 | indent: current indent level
|
460 |
|
461 | Returns:
|
462 | ok: whether it fit on the line of the given size.
|
463 | If False, you can't use the value of f.
|
464 | """
|
465 | UP_node = node # for mycpp
|
466 | tag = node.tag()
|
467 | if tag == hnode_e.Leaf:
|
468 | node = cast(hnode.Leaf, UP_node)
|
469 | f.PushColor(node.color)
|
470 | f.write(j8_lite.EncodeString(node.s, unquoted_ok=True))
|
471 | f.PopColor()
|
472 |
|
473 | elif tag == hnode_e.External:
|
474 | node = cast(hnode.External, UP_node)
|
475 |
|
476 | f.PushColor(color_e.External)
|
477 | if mylib.PYTHON:
|
478 | f.write(repr(node.obj))
|
479 | else:
|
480 | f.write('UNTYPED any')
|
481 | f.PopColor()
|
482 |
|
483 | elif tag == hnode_e.Array:
|
484 | node = cast(hnode.Array, UP_node)
|
485 |
|
486 | # Can we fit the WHOLE array on the line?
|
487 | f.write('[')
|
488 | for i, item in enumerate(node.children):
|
489 | if i != 0:
|
490 | f.write(' ')
|
491 | if not _TrySingleLine(item, f, max_chars):
|
492 | return False
|
493 | f.write(']')
|
494 |
|
495 | elif tag == hnode_e.Record:
|
496 | node = cast(hnode.Record, UP_node)
|
497 |
|
498 | return _TrySingleLineObj(node, f, max_chars)
|
499 |
|
500 | elif tag == hnode_e.AlreadySeen:
|
501 | node = cast(hnode.AlreadySeen, UP_node)
|
502 | # ... means omitting second reference, while --- means a cycle
|
503 | f.write('...0x%s' % mylib.hex_lower(node.heap_id))
|
504 |
|
505 | else:
|
506 | raise AssertionError(hnode_str(node.tag()))
|
507 |
|
508 | # Take into account the last char.
|
509 | num_chars_so_far = f.NumChars()
|
510 | if num_chars_so_far > max_chars:
|
511 | return False
|
512 |
|
513 | return True
|
514 |
|
515 |
|
516 | def PrintTree(node, f):
|
517 | # type: (hnode_t, ColorOutput) -> None
|
518 | pp = _PrettyPrinter(100) # max_col
|
519 | pp.PrintNode(node, f, 0) # indent
|
520 |
|
521 |
|
522 | def PrintTree2(node, f):
|
523 | # type: (hnode_t, ColorOutput) -> None
|
524 | """
|
525 | Make sure dependencies aren't a problem
|
526 |
|
527 | TODO: asdl/pp_hnode.py, which is like display/pp_value.py
|
528 | """
|
529 | doc = pretty.AsciiText('foo')
|
530 | printer = pretty.PrettyPrinter(20)
|
531 |
|
532 | buf = mylib.BufWriter()
|
533 | printer.PrintDoc(doc, buf)
|
534 | f.write(buf.getvalue())
|
535 | f.write('\n')
|