In this example program, we created a tool for annotating documents, especially source code. You can upload a file or type in some code, and then you can add comments to individual lines of the code. This would be a useful tool for code review, where one programmer reviews another programmer’s code and adds comments about how to improve it.
from drafter import *
from dataclasses import dataclass
add_website_css(
".comment-line",
"display: flex; justify-content: space-between; align-items: center; border-bottom: 1px dashed #ccc;",
)
@dataclass
class Line:
"""
A line of code with an optional comment.
Attributes:
original (str): The original line of code.
comment (str): The comment associated with the line of code.
index (int): The line number in the original text.
"""
original: str
comment: str
index: int
@dataclass
class State:
"""
The state of the code commenter application.
Attributes:
comments (list[Line]): A list of Line objects representing the code lines and their comments.
original_text (str): The original text of the code being commented on.
"""
comments: list[Line]
original_text: str
def parse_lines(text: str) -> list[Line]:
"""
Parses the input text into a list of Line objects.
Args:
text (str): The input text to parse.
Returns:
list[Line]: A list of Line objects.
"""
lines = []
for i, line in enumerate(text.split("\n")):
lines.append(Line(line.rstrip(), "", i))
return lines
@route
def index(state: State) -> Page:
"""
Display the main page of the code commenter application.
Args:
state (State): The current state of the application.
Returns:
Page: The rendered main page.
"""
return Page(
state,
[
Header("Code Commenter"),
"Upload a file or type some code to get started.",
FileUpload("code_file"),
TextArea("text_box", state.original_text),
Button("Start", "start_commenting"),
],
)
@route
def error_page(message: str) -> Page:
"""
Display an error page with the given message.
Args:
message (str): The error message to display.
Returns:
Page: The rendered error page.
"""
return Page(
None,
[
Header("Error"),
f"Error: {message}",
Button("Back", "index"),
],
)
@route
def start_commenting(state: State, code_file: str = "", text_box: str = "") -> Page:
"""
Starts the commenting process by parsing the input text or file.
Args:
state (State): The current state of the application.
code_file (str): The content of the uploaded file (if any).
text_box (str): The content of the text area (if any).
Returns:
Page: The page displaying the code lines for commenting.
"""
if code_file:
text = code_file
elif text_box:
text = text_box
else:
return error_page("Please upload a file or enter some text.")
lines = parse_lines(text)
state.comments = lines
state.original_text = text
return page_commenter(state)
@route
def page_commenter(state: State) -> Page:
"""
Displays the page for commenting on code lines.
Args:
state (State): The current state of the application.
Returns:
Page: The page displaying the code lines for commenting.
"""
content = [Header("Code Commenter")]
for line in state.comments:
if line.original:
content.append(
Div(
line.original,
Button("đź’¬", "comment", Argument("index", line.index)),
classes="comment-line",
)
)
else:
content.append(Div(classes="comment-line", style_height="1.5em"))
if line.comment:
content.append(Div(small_font(line.comment), classes="comment-line"))
content.append(Button("Reset", "index"))
return Page(state, content)
@route
def comment(state: State, index: int) -> Page:
"""
Displays the comment box for a specific line of code.
Args:
state (State): The current state of the application.
index (int): The index of the line to comment on.
Returns:
Page: The page displaying the comment box for the specified line.
"""
comment = state.comments[index]
return Page(
state,
[
Header("Add Comment"),
PreformattedText(comment.original),
TextArea("comment_box", comment.comment),
Button(
"Save Comment",
"save_comment",
arguments=Argument("index", index),
),
Button("Back", "page_commenter"),
],
)
@route
def save_comment(state: State, index: int, comment_box: str) -> Page:
"""
Saves the comment for a specific line of code.
Args:
state (State): The current state of the application.
index (int): The index of the line to save the comment for.
comment_box (str): The content of the comment box.
Returns:
Page: The updated page displaying the code lines for commenting.
"""
state.comments[index].comment = comment_box
return page_commenter(state)
start_server(State([], ""))
Unlike the food_lookup.py program from the previous example, this program is just a single file. However, it does allow you to upload a file using the FileUpload component. You can upload any text file, but the program is really designed for source code files. You can also just type or paste code into the text area. The start_commenting function checks to see if you uploaded a file or typed something in the text area. If you did neither, it shows an error message.
In order to know which line you are commenting on, the comment and save_comment functions take an index parameter. This is passed as an argument to the button that opens the comment box and the button that saves the comment. The Argument component is used to specify that the value of the index parameter should be taken from the index attribute of the Line object.
Because we’re dealing with a fixed set of lines, we can be sure that the index will always be valid. In a more complex application where you can add and delete lines, you might want to add error handling to ensure that the index is within the bounds of the list of comments.
The index parameter is originally calculated in the parse_lines function, which assigns each line a unique index based on its position in the original text. The enumerate function is used to get both the line and its index as we iterate through the lines of text.
add_website_css(css: str): Adds custom CSS to the entire website.add_website_css(selector: str, css: str): Adds custom CSS to a specific css selector.classes: A parameter for any component (e.g., Div) that allows you to add one or more CSS classes to the div.style_height: A parameter for any component (e.g., Div) that allows you to set the CSS height property of the div.small_font(text: str): A function that applies a smaller font size to the given text.The program uses some custom CSS to style the lines of code and their associated comment buttons. The add_website_css function is used to add CSS rules that make the code lines and buttons align nicely. The .comment-line class is applied to each line of code and its button, ensuring they are displayed in a flex container with space between them. Notice that we use the classes parameter of the Div component to apply the CSS class, without the dot prefix (that’s only used in the CSS definition to target the class).
The style_height parameter is used to set a minimum height for empty lines, so that they still take up some vertical space. This makes it easier to see where blank lines are in the text. We will learn more about these “named parameters” in a future chapter of the textbook.
The small_font function is used to make the comments appear in a smaller font size, which helps differentiate them from the code lines. This function wraps the text in a <span> element with inline CSS to set the CSS font-size property to a smaller value.
There are many other ways to style your Drafter applications. See the Styling section of the Drafter documentation for more information.