Syntax Highlighter
This example shows how to use the event stream to make a WDL syntax highlighter that emits HTML.
sprocket_bio/examples/syntax_highlighter.py
import argparse
import html
import os
import pathlib
import sys
import webbrowser
from sprocket_bio.diagnostics import Mode, emit_diagnostics
from sprocket_bio.grammar import Severity, SupportedVersion, SyntaxKind
from sprocket_bio.grammar.grammar import document
from sprocket_bio.grammar.parser import Event
from sprocket_bio.grammar.version import V1
def parse_args() -> argparse.Namespace:
"""Parses command-line arguments and returns them."""
parser = argparse.ArgumentParser(
description="Syntax highlights a WDL document, outputting HTML that can be viewed in your browser"
)
parser.add_argument(
"source_file",
type=pathlib.Path,
help="path to WDL document",
metavar="<source_file>",
)
parser.add_argument(
"-o",
"--output",
type=pathlib.Path,
help="the path to write to, defaults to <source_file>.html",
metavar="<OUTPUT_FILE>",
)
parser.add_argument(
"--open", action="store_true", help="opens the output in a web browser"
)
args = parser.parse_args()
# If the output is not specified, set it to the source_file with an ".html" suffix.
if args.output is None:
args.output = args.source_file.with_suffix(args.source_file.suffix + ".html")
return args
def syntax_kind_color(kind: SyntaxKind) -> str | None:
"""Returns the color a token of a specific `kind` should be highlighted, or `None` if it shouldn't."""
match kind:
# Keywords
case _ if kind.is_keyword():
return "#99A7F1"
# Types
case _ if kind.is_type():
return "#BA9CFF"
# Operators
case _ if kind.is_operator():
return "#9CB2FF"
# Literals
case (
SyntaxKind.INTEGER
| SyntaxKind.FLOAT
| SyntaxKind.DOUBLE_QUOTE
| SyntaxKind.SINGLE_QUOTE
| SyntaxKind.LITERAL_STRING_TEXT
| SyntaxKind.LITERAL_COMMAND_TEXT
| SyntaxKind.TRUE_KEYWORD
| SyntaxKind.FALSE_KEYWORD
| SyntaxKind.NONE_KEYWORD
| SyntaxKind.VERSION
):
return "#E59CFF"
# Comments
case SyntaxKind.COMMENT:
return "#7780A3"
# Punctuation
case (
SyntaxKind.OPEN_BRACE
| SyntaxKind.CLOSE_BRACE
| SyntaxKind.OPEN_HEREDOC
| SyntaxKind.CLOSE_HEREDOC
| SyntaxKind.OPEN_BRACKET
| SyntaxKind.CLOSE_BRACKET
| SyntaxKind.OPEN_PAREN
| SyntaxKind.CLOSE_PAREN
| SyntaxKind.PLACEHOLDER_OPEN
| SyntaxKind.COLON
):
return "#BBBBBB"
# Everything else
case _:
return None
def main() -> None:
args = parse_args()
with open(args.source_file, mode="rt", encoding="utf-8") as f:
source = f.read()
# Parse the source string into a list of parser events.
events, diagnostics = document(
source, fallback_version=SupportedVersion.V1(V1.ZERO)
)
# Emit diagnostics, if there are any.
emit_diagnostics(
str(args.source_file), source, diagnostics, report_mode=Mode.FULL, colorize=True
)
# If any of the diagnostics are errors, exit.
if any([d.severity is Severity.ERROR for d in diagnostics]):
sys.exit(1)
# Do not overwrite the source file.
if os.path.exists(args.output) and os.path.samefile(args.source_file, args.output):
sys.exit(
f"source and output files are the same, refusing to overwrite it: {args.output}"
)
with open(args.output, mode="wt", encoding="utf-8") as f:
# Write the prelude of the HTML document and configure the base style.
f.write(
'<!DOCTYPE html><html><body style="background: #070A19; color: white"><pre>'
)
# Get the byte representation of the source code.
source_bytes = source.encode("utf-8")
for event in events:
# Filter the parser events for tokens, which represent individual pieces of syntax.
if isinstance(event, Event.Token):
# Get the text in the source code represented by this span. `Span`s index by bytes,
# not Unicode code points, which is why we slice the byte representation of the
# source. We also escape the text, so it is safe to include in HTML.
token_text = html.escape(
source_bytes[event.span.start : event.span.end].decode("utf-8")
)
# If this token should be colored then write it inside of a styled `<span>`, else
# just write the plain text.
if color := syntax_kind_color(event.kind):
f.write(f'<span style="color: {color}">{token_text}</span>')
else:
f.write(token_text)
# Wrap up the HTML document.
f.write("</pre></body></html>")
# Open the HTML document in the default browser if `--open` if specified.
if args.open:
webbrowser.open(args.output.absolute().as_uri())
if __name__ == "__main__":
main()
If you have the Python bindings installed, you can run the example yourself:
$ python -m sprocket_bio.examples.syntax_highlighter example.wdl
The emitted HTML renders as the following:
version 1.3 task say_hello { input { String greeting } command <<< echo "~{greeting}, world!" >>> output { String out = read_string(stdout()) } requirements { container: "ubuntu:latest" } }