Add content view syntax highlighting for JSON and msgpack (#5648)

* feat(contentview): add json syntax highlighting

* feat(contentview): add msgpack syntax highlighting

* fix(contentview): address pipeline failures

* feat(contentview): switch to pygment-like token names

* feat(mitmproxy): update cli syntax highlighting

* refactor: respond to review comments

* nits

Co-authored-by: Maximilian Hils <git@maximilianhils.com>
Co-authored-by: Maximilian Hils <github@maximilianhils.com>
This commit is contained in:
Jay Malhotra 2022-10-19 22:51:03 +01:00 committed by GitHub
parent 42df073f7e
commit 0bd36e7313
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 151 additions and 41 deletions

View file

@ -45,6 +45,8 @@
([#5562](https://github.com/mitmproxy/mitmproxy/pull/5562), @decathorpe, @mhils)
* Fix mitmweb not properly opening a browser and being stuck on some Linux.
([#5522](https://github.com/mitmproxy/mitmproxy/issues/5522), @Prinzhorn)
* Add syntax highlighting to JSON and msgpack content view.
([#5623](https://github.com/mitmproxy/mitmproxy/issues/5623), @SapiensAnatis)
* Fix race condition when updating mitmweb WebSocket connections that are closing.
([#5405](https://github.com/mitmproxy/mitmproxy/issues/5405), @mhils)

View file

@ -28,11 +28,14 @@ def format_json(data: Any) -> Iterator[base.TViewLine]:
yield current_line
current_line = []
if re.match(r'\s*"', chunk):
current_line.append(("json_string", chunk))
if len(current_line) == 1 and current_line[0][0] == "text" and current_line[0][1].isspace():
current_line.append(("Token_Name_Tag", chunk))
else:
current_line.append(("Token_Literal_String", chunk))
elif re.match(r"\s*\d", chunk):
current_line.append(("json_number", chunk))
current_line.append(("Token_Literal_Number", chunk))
elif re.match(r"\s*(true|null|false)", chunk):
current_line.append(("json_boolean", chunk))
current_line.append(("Token_Keyword_Constant", chunk))
else:
current_line.append(("text", chunk))
yield current_line

View file

@ -15,23 +15,62 @@ def parse_msgpack(s: bytes) -> Any:
return PARSE_ERROR
def pretty(value, htchar=" ", lfchar="\n", indent=0):
nlch = lfchar + htchar * (indent + 1)
if type(value) is dict:
items = [
nlch + repr(key) + ": " + pretty(value[key], htchar, lfchar, indent + 1)
for key in value
]
return "{%s}" % (",".join(items) + lfchar + htchar * indent)
elif type(value) is list:
items = [nlch + pretty(item, htchar, lfchar, indent + 1) for item in value]
return "[%s]" % (",".join(items) + lfchar + htchar * indent)
def format_msgpack(data: Any, output = None, indent_count: int = 0) -> list[base.TViewLine]:
if output is None:
output = [[]]
indent = ("text", " " * indent_count)
if type(data) is str:
token = [("Token_Literal_String", f"\"{data}\"")]
output[-1] += token
# Need to return if single value, but return is discarded in dict/list loop
return output
elif type(data) is float or type(data) is int:
token = [("Token_Literal_Number", repr(data))]
output[-1] += token
return output
elif type(data) is bool:
token = [("Token_Keyword_Constant", repr(data))]
output[-1] += token
return output
elif type(data) is dict:
output[-1] += [("text", "{")]
for key in data:
output.append([indent, ("text", " "), ("Token_Name_Tag", f'"{key}"'), ("text", ": ")])
format_msgpack(data[key], output, indent_count + 1)
if key != list(data)[-1]:
output[-1] += [("text", ",")]
output.append([indent, ("text", "}")])
return output
elif type(data) is list:
output[-1] += [("text", "[")]
for item in data:
output.append([indent, ("text", " ")])
format_msgpack(item, output, indent_count + 1)
if item != data[-1]:
output[-1] += [("text", ",")]
output.append([indent, ("text", "]")])
return output
else:
return repr(value)
token = [("text", repr(data))]
output[-1] += token
def format_msgpack(data):
return base.format_text(pretty(data))
return output
class ViewMsgPack(base.View):

View file

@ -69,10 +69,11 @@ class Palette:
"mark",
# Hex view
"offset",
# JSON view
"json_string",
"json_number",
"json_boolean",
# JSON/msgpack view
"Token_Name_Tag",
"Token_Literal_String",
"Token_Literal_Number",
"Token_Keyword_Constant",
# TCP flow details
"from_client",
"to_client",
@ -207,10 +208,11 @@ class LowDark(Palette):
mark=("light red", "default"),
# Hex view
offset=("dark cyan", "default"),
# JSON view
json_string=("dark blue", "default"),
json_number=("light magenta", "default"),
json_boolean=("dark magenta", "default"),
# JSON/msgpack view
Token_Name_Tag=("dark green", "default"),
Token_Literal_String=("dark blue", "default"),
Token_Literal_Number=("light magenta", "default"),
Token_Keyword_Constant=("dark magenta", "default"),
# TCP flow details
from_client=("light blue", "default"),
to_client=("light red", "default"),
@ -306,10 +308,11 @@ class LowLight(Palette):
mark=("dark red", "default"),
# Hex view
offset=("dark blue", "default"),
# JSON view
json_string=("dark blue", "default"),
json_number=("light magenta", "default"),
json_boolean=("dark magenta", "default"),
# JSON/msgpack view
Token_Name_Tag=("dark green", "default"),
Token_Literal_String=("dark blue", "default"),
Token_Literal_Number=("light magenta", "default"),
Token_Keyword_Constant=("dark magenta", "default"),
# TCP flow details
from_client=("dark blue", "default"),
to_client=("dark red", "default"),
@ -427,10 +430,11 @@ class SolarizedLight(LowLight):
),
# Hex view
offset=(sol_cyan, "default"),
# JSON view
json_string=(sol_cyan, "default"),
json_number=(sol_blue, "default"),
json_boolean=(sol_magenta, "default"),
# JSON/msgpack view
Token_Name_Tag=(sol_green, "default"),
Token_Literal_String=(sol_cyan, "default"),
Token_Literal_Number=(sol_blue, "default"),
Token_Keyword_Constant=(sol_magenta, "default"),
# TCP flow details
from_client=(sol_blue, "default"),
to_client=(sol_red, "default"),
@ -506,10 +510,11 @@ class SolarizedDark(LowDark):
),
# Hex view
offset=(sol_cyan, "default"),
# JSON view
json_string=(sol_cyan, "default"),
json_number=(sol_blue, "default"),
json_boolean=(sol_magenta, "default"),
# JSON/msgpack view
Token_Name_Tag=(sol_green, "default"),
Token_Literal_String=(sol_cyan, "default"),
Token_Literal_Number=(sol_blue, "default"),
Token_Keyword_Constant=(sol_magenta, "default"),
# TCP flow details
from_client=(sol_blue, "default"),
to_client=(sol_red, "default"),

View file

@ -17,6 +17,32 @@ def test_parse_json():
def test_format_json():
assert list(json.format_json({"data": ["str", 42, True, False, None, {}, []]}))
assert list(json.format_json({"string": "test"})) == [
[('text', '{'), ('text', '')],
[('text', ' '), ('Token_Name_Tag', '"string"'), ('text', ': '), ('Token_Literal_String', '"test"'), ('text', '')],
[('text', ''), ('text', '}')]]
assert list(json.format_json({"num": 4})) == [
[('text', '{'), ('text', '')],
[('text', ' '), ('Token_Name_Tag', '"num"'), ('text', ': '), ('Token_Literal_Number', '4'), ('text', '')],
[('text', ''), ('text', '}')]]
assert list(json.format_json({"bool": True})) == [
[('text', '{'), ('text', '')],
[('text', ' '), ('Token_Name_Tag', '"bool"'), ('text', ': '), ('Token_Keyword_Constant', 'true'), ('text', '')],
[('text', ''), ('text', '}')]]
assert list(json.format_json({"object": {"int": 1}})) == [
[('text', '{'), ('text', '')],
[('text', ' '), ('Token_Name_Tag', '"object"'), ('text', ': '), ('text', '{'), ('text', '')],
[('text', ' '), ('Token_Name_Tag', '"int"'), ('text', ': '), ('Token_Literal_Number', '1'), ('text', '')],
[('text', ' '), ('text', '}'), ('text', '')],
[('text', ''), ('text', '}')]]
assert list(json.format_json({"list": ["string", 1, True]})) == [
[('text', '{'), ('text', '')],
[('text', ' '), ('Token_Name_Tag', '"list"'), ('text', ': '), ('text', '[')],
[('Token_Literal_String', ' "string"'), ('text', ',')],
[('Token_Literal_Number', ' 1'), ('text', ',')],
[('Token_Keyword_Constant', ' true'), ('text', '')],
[('text', ' '), ('text', ']'), ('text', '')],
[('text', ''), ('text', '}')]]
def test_view_json():

View file

@ -18,9 +18,32 @@ def test_parse_msgpack():
def test_format_msgpack():
assert list(
msgpack.format_msgpack({"data": ["str", 42, True, False, None, {}, []]})
)
assert list(msgpack.format_msgpack({"string": "test", "int": 1, "float": 1.44, "bool": True})) == [
[('text', '{')],
[('text', ''), ('text', ' '), ('Token_Name_Tag', '"string"'), ('text', ': '), ('Token_Literal_String', '"test"'), ('text', ',')],
[('text', ''), ('text', ' '), ('Token_Name_Tag', '"int"'), ('text', ': '), ('Token_Literal_Number', '1'), ('text', ',')],
[('text', ''), ('text', ' '), ('Token_Name_Tag', '"float"'), ('text', ': '), ('Token_Literal_Number', '1.44'), ('text', ',')],
[('text', ''), ('text', ' '), ('Token_Name_Tag', '"bool"'), ('text', ': '), ('Token_Keyword_Constant', 'True')],
[('text', ''), ('text', '}')]
]
assert list(msgpack.format_msgpack({"object": {"key": "value"}, "list": [1]})) == [
[('text', '{')],
[('text', ''), ('text', ' '), ('Token_Name_Tag', '"object"'), ('text', ': '), ('text', '{')],
[('text', ' '), ('text', ' '), ('Token_Name_Tag', '"key"'), ('text', ': '), ('Token_Literal_String', '"value"')],
[('text', ' '), ('text', '}'), ('text', ',')],
[('text', ''), ('text', ' '), ('Token_Name_Tag', '"list"'), ('text', ': '), ('text', '[')],
[('text', ' '), ('text', ' '), ('Token_Literal_Number', '1')],
[('text', ' '), ('text', ']')],
[('text', ''), ('text', '}')]]
assert list(msgpack.format_msgpack('string')) == [[('Token_Literal_String', '"string"')]]
assert list(msgpack.format_msgpack(1.2)) == [[('Token_Literal_Number', '1.2')]]
assert list(msgpack.format_msgpack(True)) == [[('Token_Keyword_Constant', 'True')]]
assert list(msgpack.format_msgpack(b'\x01\x02\x03')) == [[('text', "b'\\x01\\x02\\x03'")]]
def test_view_msgpack():

View file

@ -14,4 +14,16 @@
.codeeditor{
margin-bottom: 12px;
}
.Token_Name_Tag{
color: darkgreen;
}
.Token_Literal_String{
color: firebrick;
}
.Token_Literal_Number{
color: purple;
}
.Token_Keyword_Constant{
color: blue;
}
}