-
Notifications
You must be signed in to change notification settings - Fork 175
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HTML-Based Visual Debugger #48
Open
marrable
wants to merge
20
commits into
google:master
Choose a base branch
from
marrable:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
28a9796
adding basic foundations of debugger. HTML builder, plus ParseHistory
marrable afe089a
wip
marrable ed42dd4
wip, getting the basic classes sketched out
marrable 34e8eca
wip, working on CSS building
marrable 0c3397e
line state highlighting functional. working on indexing matches
marrable 0620b17
wip, adding match highlighting. Still needs work
marrable 5ef1a0e
wip, highlighting working, need to add regex matches
marrable 885f7d4
added state header
marrable 492715a
wip need to add regex on hover
marrable 724e115
wip
marrable a85b476
wip regex on hover working
marrable 847aae6
added handling for negative indices
marrable 854bfcb
added shadowed header
marrable 4c774e9
tweaked indents, added stripping of regex pattern (P?... etc to make …
marrable 3df9e1b
cleaned up html generation to contain less linting errors
marrable 71ba4d5
added command line argument control of v-debug mode
marrable e24da2b
added formating to be consistent with project style. Added comments
marrable 0459b81
fixed dropping of border-radius style
marrable d6f7dc8
added fix for when 'continue' action causes overlapping matches
marrable 708ec9f
fixed HTML double backslash rendering in list of values matching
marrable File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,335 @@ | ||
#!/usr/bin/python | ||
# | ||
# Copyright 2011 Google Inc. All Rights Reserved. | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
|
||
""" Visual Debugger | ||
|
||
Provides a HTML-based debugging tool that allows authors of templates | ||
to view the behavior of templates when applied to some example CLI text. | ||
State changes are represented with color coding such that state | ||
transitions are clearly represented during parsing. | ||
|
||
Matches on lines are highlighted to show extracted values and hovering | ||
over a match shows the value and corresponding regex that was matched. | ||
""" | ||
from collections import namedtuple | ||
from textwrap import dedent | ||
|
||
import re | ||
|
||
LINE_SATURATION = 40 | ||
LINE_LIGHTNESS = 60 | ||
MATCH_SATURATION = 100 | ||
MATCH_LIGHTNESS = 30 | ||
|
||
|
||
class LineHistory(namedtuple('LineHistory', ['line', 'state', 'matches', 'match_index_pairs'])): | ||
"""" The match history for a given line when parsed using the FSM. | ||
|
||
Contains the regex match objects for that line, | ||
which are converted to indices for highlighting | ||
""" | ||
|
||
|
||
class MatchedPair(namedtuple('MatchPair', ['match_obj', 'rule'])): | ||
"""" Stores the line history when parsed using the FSM.""" | ||
|
||
|
||
class StartStopIndex(namedtuple('StartStopIndex', ['start', 'end', 'value'])): | ||
"""Represents the start and stop indices of a match for a given template value.""" | ||
def __eq__(self, other): | ||
return self.start == other.start and self.end == other.end | ||
|
||
def __gt__(self, other): | ||
return self.start > other.start | ||
|
||
|
||
class VisualDebugger(object): | ||
"""Responsible for building the parse history of a TextFSM object into a visual html doc. """ | ||
|
||
def __init__(self, fsm, cli_text): | ||
self.fsm = fsm | ||
self.cli_text = cli_text | ||
self.state_colormap = {} | ||
|
||
@staticmethod | ||
def add_prelude_boilerplate(html_file): | ||
prelude_lines = dedent(''' | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset='UTF-8'> | ||
<title>visual debugger</title> | ||
''') | ||
|
||
html_file.write(prelude_lines) | ||
|
||
def build_state_colors(self): | ||
"""Basic colour wheel selection for state highlighting""" | ||
cntr = 1 | ||
for state_name in self.fsm.states.keys(): | ||
self.state_colormap[state_name] = (67 * cntr) % 360 | ||
cntr += 1 | ||
|
||
@staticmethod | ||
def hsl_css(h, s, l): | ||
"""Return the CSS string for HSL background color.""" | ||
return " background-color: hsl({},{}%,{}%);\n".format(h, s, l) | ||
|
||
def add_css_styling(self, html_file): | ||
css_prelude_lines = dedent(''' | ||
<style type='text/css'> | ||
body { | ||
font-family: Arial, Helvetica, sans-serif; | ||
background-color: hsl(40, 1%, 25%); | ||
margin: 0; | ||
padding: 0; | ||
} | ||
h4 { | ||
font-family: Arial, Helvetica, sans-serif; | ||
color: white; | ||
margin-top: 0; | ||
} | ||
.regex { | ||
background-color: silver; | ||
border: 2px; | ||
border-style: solid; | ||
border-color: black; | ||
display: none; | ||
border-radius: 5px; | ||
padding: 0 10px; | ||
} | ||
.cli-title{ | ||
padding-top: 100px; | ||
} | ||
.states{ | ||
position: fixed; | ||
background-color: dimgray; | ||
width: 100%; | ||
padding: 10px; | ||
margin-top: 0; | ||
box-shadow: 0 3px 8px #000000; | ||
} | ||
''') | ||
|
||
html_file.writelines(css_prelude_lines) | ||
|
||
# Build and write state styling CSS | ||
for state_name in self.fsm.states.keys(): | ||
state_block = [ | ||
".{}{{\n".format(state_name), | ||
self.hsl_css( | ||
self.state_colormap[state_name], | ||
LINE_SATURATION, | ||
LINE_LIGHTNESS | ||
), | ||
" border-radius: 5px;\n", | ||
" padding: 0 10px;\n", | ||
"}\n" | ||
] | ||
html_file.writelines(state_block) | ||
|
||
# Build and write state match styling CSS | ||
new_parse_history = [] | ||
l_count = 0 | ||
for line in self.fsm.parse_history: | ||
|
||
match_index_pairs = [] | ||
|
||
# Flatten match index structure | ||
for match in line.matches: | ||
for key in match.match_obj.groupdict().keys(): | ||
match_index_pairs.append( | ||
StartStopIndex( | ||
match.match_obj.start(key), | ||
match.match_obj.end(key), | ||
key | ||
) | ||
) | ||
|
||
# Merge indexes that overlap due to multiple rule matches for a single line. | ||
self.merge_indexes(match_index_pairs) | ||
match_index_pairs.sort() | ||
|
||
# Overwrite named tuple data member | ||
line = line._replace(match_index_pairs=match_index_pairs) | ||
new_parse_history.append(line) | ||
|
||
# Generate CSS for match highlighting and on-hover regex display | ||
if line.match_index_pairs: | ||
match_count = 0 | ||
for index_pair in line.match_index_pairs: | ||
match_block = [ | ||
".{}-match-{}-{}{{\n".format(line.state, l_count, match_count), | ||
self.hsl_css( | ||
self.state_colormap[line.state], | ||
MATCH_SATURATION, | ||
MATCH_LIGHTNESS | ||
), | ||
" border-radius: 5 px;\n", | ||
" font-weight: bold;\n" | ||
" color: white;\n", | ||
" padding: 0 5px;\n", | ||
"}\n", | ||
".{}-match-{}-{}:hover + .regex {{\n".format(line.state, l_count, match_count), | ||
" display: inline;\n", | ||
"}\n" | ||
] | ||
html_file.writelines(match_block) | ||
match_count += 1 | ||
l_count += 1 | ||
|
||
# Overwrite parse history from FSM with newly processed history | ||
self.fsm.parse_history = new_parse_history | ||
|
||
css_closing_lines = [ | ||
"</style>\n" | ||
] | ||
|
||
html_file.writelines(css_closing_lines) | ||
|
||
def merge_indexes(self, match_index_pairs): | ||
"""Merge overlapping index pairs that may occur due to multiple rule matches.""" | ||
|
||
def overlapping(index_a, index_b): | ||
if index_a.end > index_b.start and index_a.start < index_b.end: | ||
return True | ||
if index_a.start < index_b.end and index_b.start < index_a.end: | ||
return True | ||
if index_a.start < index_b.start and index_a.end > index_b.end: | ||
return True | ||
if index_b.start < index_a.start and index_b.end > index_a.end: | ||
return True | ||
|
||
def merge_pairs(index_a, index_b): | ||
start = 0 | ||
if index_a.start < index_b.start: | ||
start = index_a.start | ||
else: | ||
start = index_b.start | ||
if index_a.end < index_b.end: | ||
end = index_b.end | ||
else: | ||
end = index_a.end | ||
return StartStopIndex(start, end, [index_a.value, index_b.value]) | ||
|
||
for pair in match_index_pairs: | ||
overlap = False | ||
match_index_pairs.remove(pair) | ||
for check_pair in match_index_pairs: | ||
if overlapping(pair, check_pair): | ||
overlap = True | ||
match_index_pairs.remove(check_pair) | ||
match_index_pairs.append(merge_pairs(pair, check_pair)) | ||
break | ||
if not overlap: | ||
match_index_pairs.append(pair) | ||
|
||
def add_cli_text(self, html_file): | ||
"""Builds the HTML elements of the debug page including: | ||
- Colored States Header Bar | ||
- Highlighted CLI Text | ||
""" | ||
|
||
cli_text_prelude = [ | ||
"</head>\n", | ||
"<header class='states'>", | ||
"<h4>States:</h4>\n" | ||
] | ||
|
||
for state in self.state_colormap.keys(): | ||
cli_text_prelude += [ | ||
"<button style='font-weight: bold;' class='{}'>{}</button>\n".format(state, state) | ||
] | ||
|
||
cli_text_prelude += [ | ||
"</header>\n", | ||
"<body>\n", | ||
"<h4 class='cli-title'>CLI Text:</h4>\n", | ||
"<pre>\n" | ||
] | ||
|
||
html_file.writelines(cli_text_prelude) | ||
|
||
lines = self.cli_text.splitlines() | ||
lines = [line + '\n' for line in lines] | ||
|
||
# Process each line history and add highlighting where matches occur. | ||
l_count = 0 | ||
for line_history in self.fsm.parse_history: | ||
# Only process highlights where matches occur. | ||
if line_history.match_index_pairs: | ||
built_line = "" | ||
prev_end = 0 | ||
match_count = 0 | ||
|
||
for index in line_history.match_index_pairs: | ||
if index.start < 0 or index.end < 0: | ||
continue | ||
|
||
# Strip out useless pattern format characters and value label. | ||
# Escape chevrons in regex pattern. | ||
re_patterns = [] | ||
values = [] | ||
if type(index.value) is list: | ||
values = index.value | ||
for v in index.value: | ||
value_pattern = self.fsm.value_map[v] | ||
re_patterns.append(re.sub('\?P<.*?>', '', value_pattern).replace('<', '<').replace('>', '>')) | ||
else: | ||
values.append(index.value) | ||
value_pattern = self.fsm.value_map[index.value] | ||
re_patterns.append(re.sub('\?P<.*?>', '', value_pattern).replace('<', '<').replace('>', '>')) | ||
|
||
# Build section of match and escape non HTML chevrons if present | ||
built_line += ( | ||
lines[l_count][prev_end:index.start].replace('<', '<').replace('>', '>') | ||
+ "<span class='{}-match-{}-{}'>".format(line_history.state, l_count, match_count) | ||
+ lines[l_count][index.start:index.end].replace('<', '<').replace('>', '>') | ||
+ "</span><span class='regex'>{} >> {}</span>".format(', '.join(re_patterns), ', '.join(values)) | ||
) | ||
prev_end = index.end | ||
match_count += 1 | ||
|
||
built_line += lines[l_count][line_history.match_index_pairs[-1].end:].replace('<', '<').replace('>', '>') | ||
lines[l_count] = built_line | ||
else: | ||
# Escape non HTML tag chevrons if present | ||
lines[l_count] = lines[l_count].replace('<', '<').replace('>', '>') | ||
|
||
# Add final span wrapping tag for line state color | ||
lines[l_count] = ("<span class='{}'>".format(line_history.state) | ||
+ lines[l_count] + "</span>") | ||
l_count += 1 | ||
|
||
# Close off document | ||
end_body_end_html = dedent(''' | ||
</pre> | ||
</body> | ||
</html> | ||
''') | ||
|
||
html_file.writelines(lines) | ||
|
||
html_file.write(end_body_end_html) | ||
|
||
def build_debug_html(self): | ||
"""Calls HTML building procedures in sequence to create debug HTML doc.""" | ||
with open("debug.html", "w+") as f: | ||
self.add_prelude_boilerplate(f) | ||
self.build_state_colors() | ||
self.add_css_styling(f) | ||
self.add_cli_text(f) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Writing to a file 'debug.html' in the current directory is potentially harmful. Consider prompting for the target destination/name (with a sensible default) would be preferable.
Another possibility is to pipe to a cli tool that can render HTML in a terminal such as html2text or w3m