Before we get started, have you tried our new Python Code Assistant? It's like having an expert coder at your fingertips. Check it out!
In this tutorial, we will make a markdown editor using the GUI Library Tkinter and regular expressions. We will make it so there is a text area on the left where the user writes and a display area on the right where the markdown is slightly styled. To do this, we need to learn about regular expressions and the tkinter's Text
widget and how to style parts of these text widgets.
Let's get started!
As always, we import some modules and libraries to help us. First, we get everything from Tkinter by using the *
. With Tkinter, we will build our Graphical User Interface (GUI). By using this syntax with the asterisk (*
) we ensure that we can use the constants defined by Tkinter.
Because windows generated by Tkinter often don't look good, we also get ctypes
so we can enable high Dots Per Inch (DPI). You see this in action in the last line of the code block below.
Lastly, we get re
which we'll use to find patterns in our text. So we will also learn a little bit about regular expressions:
# Imports
from tkinter import *
import ctypes
import re
# Increas Dots Per inch so it looks sharper
ctypes.windll.shcore.SetProcessDpiAwareness(True)
Now we get to setting up Tkinter. We start by making a new Tk
object and saving it to a variable. Then we set the title, which will appear in the top left of our window. We then also set the window dimensions with the geometry()
function. Lastly, we set the font globally to be courier
in size 15:
# Setup
root = Tk()
root.title('Markdown Editor')
root.geometry('1000x600')
# Setting the Font globaly
root.option_add('*Font', 'Courier 15')
Now we make many variables representing style settings for our little program. We use a function to transform these RGB values to hex that we create ourselves. We later see how it is made up. The width
is used in the text widget later:
# Style Setup
editorBackground = rgbToHex((40, 40, 40))
editorTextColor = rgbToHex((230, 230, 230))
displayBackground = rgbToHex((60, 60, 60))
displayTextColor = rgbToHex((200, 200, 200))
caretColor = rgbToHex((255, 255, 255))
# Width of the Textareas in characters
width = 10
We also define some fonts. For the edit area, we set Courier, which is redundant, but we do it either way. We then define the display area font as Calibri; this may not work for you, so you change it to Arial or any other font installed on your computer. We also set the sizes and colors for headers from layers 1 to 3:
# Fonts
editorfontName = 'Courier'
displayFontName = 'Calibri'
# Font Sizes
normalSize = 15
h1Size = 40
h2Size = 30
h3Size = 20
# font Colors
h1Color = rgbToHex((240, 240, 240))
h2Color = rgbToHex((200, 200, 200))
h3Color = rgbToHex((160, 160, 160))
Now we also define a list of the replacements and stylings made in the display area. This is a list comprised of other lists. In these lists, the first item is the pattern that is searched. The second one is the name. The third is the font data which is a formatted string in our case.
The fourth is the color, and the last is the offset. Later we see what this means when we use this nested list. For now, we only enlargen headers with one, two, and three hashtags, and we make text surrounded with *
bold:
# Replacements tell us were to insert tags with the font and colors given
replacements = [
[
'^#[a-zA-Z\s\d\?\!\.]+$',
'Header 1',
f'{displayFontName} {h1Size}',
h1Color,
0
], [
'^##[a-zA-Z\s\d\?\!\.]+$',
'Header 2',
f'{displayFontName} {h2Size}',
h2Color,
0
], [
'^###[a-zA-Z\s\d\?\!\.]+$',
'Header 3',
f'{displayFontName} {h3Size}',
h3Color,
0
], [
'\*.+?\*',
'Bold',
f'{displayFontName} {normalSize} bold',
displayTextColor,
2
],
]
Let's take a look at one of the expressions: ^#[a-zA-Z\s\d\?\!\.]+$
will match the largest header made with one #
hashtag. The ^
means the beginning of the string and #
means itself, [a-zA-Z\s\d\?\!\.]
indicates any character, digit, whitespace, '?
', .
or !
. We modify this group with +
so it means one or more of this character. And in the end, we have a dollar sign that means the end of the string. We do it this way because markup can not span multiple lines divided by a \n
.
Now let's make some widgets. We start with the editor, that is just a Text
. We give it the root element as a master. We set a height
and a width
that does not matter here. We set the background and foreground (text) color with our variables. We then put a hefty border and the relief to flat, resulting in nice padding for our text area. We also set the color of the caret with the insertbackground
parameter. We then place the widget with pack
:
# Making the Editor Area
editor = Text(
root,
height=5,
width=width,
bg=editorBackground,
fg=editorTextColor,
border=30,
relief=FLAT,
insertbackground=caretColor
)
editor.pack(expand=1, fill=BOTH, side=LEFT)
We bind the KeyRelease
event to our changes()
function that we'll define, highlighting the display area and updating the text. We then set the focus on the editor with focus_set()
. We also insert some starting text into the editor:
# Bind <KeyReleas> so every change is registered
editor.bind('<KeyRelease>', changes)
editor.focus_set()
# Insert a starting text
editor.insert(INSERT, """#Heading 1
##Heading 2
###Heading 3
This is a *bold* move!
- Markdown Editor -
""")
We also make a Text
Area for the Display. It is pretty similar to the editor area. But we set it to be disabled so the user cannot edit it:
# Making the Display Area
display = Text(
root,
height=5,
width=width,
bg=displayBackground,
fg=displayTextColor,
border=30,
relief=FLAT,
font=f"{displayFontName} {normalSize}",
)
display.pack(expand=1, fill=BOTH, side=LEFT)
# Disable the Display Area so the user cant write in it
# We will have to toggle it so we can insert text
display['state'] = DISABLED
Now we define three functions. Keep in mind that we have to insert these definitions early in the code because we have used them in code before.
changes()
This function was bound to the KeyRelease
event of the edit area, and it will handle changes, sync the two text areas and highlight certain parts of them.
In the function, we have a parameter called event that is not needed but have to define it because the event will pass itself to the function. Then we set the state of the display to normal so it can be edited. After that, we delete its content with its delete()
method. Here 1.0
means the first letter in the first line and END
means the end of the content.
def changes(event=None):
display['state'] = NORMAL
# Clear the Display Area
display.delete(1.0, END)
Continuing, we get the editor's text and save it to a variable. We then also copy this value to another variable because, in the following two lines, we remove #
and *
from the text, so it looks nicer. But we keep the raw version so we can search the patterns. We then insert the text
into the display
:
# Insert the content of the Edit Area into the Display Area
text = editor.get('1.0', END)
# Save Raw Text for later
textRaw = text
# Remove Unwanted Characters
text = ''.join(text.split('#'))
text = ''.join(text.split('*'))
display.insert(1.0, text)
Then we loop through our replacements list and save each item to a different variable by unpacking them:
# Loop through each replacement, unpacking it fully
for pattern, name, fontData, colorData, offset in replacements:
# Get the location indices of the given pattern
locations = search_re(pattern, textRaw, offset)
Then we call our search_re(text)
which will return a list like this. Where each tuple represents the starting and ending position of the matched pattern:
[("1.3", "1.7"), ("2.3", "2.5")]
Then we also loop through this list and add a tag at these positions with the name. These tags allow us to style certain parts of the text.
# Add tags where the search_re function found the pattern
for start, end in locations:
display.tag_add(name, start, end)
Then at the end of the loop, we also configure this tag to use our font data and color. We access the tag by its name.
The last thing we do in the function is set the state to DISABLED
once again.
# Configure the tag to use the specified font and color
# to this every time to delete the previous tags
display.tag_config(name, font=fontData, foreground=colorData)
display['state'] = DISABLED
search_re()
Now let's go over the pattern searching function. We give it the pattern, the text, and the offset. In it, we define a matches
list that will be returned in the end:
def search_re(pattern, text, offset):
matches = []
Then we split the text by \n
using the splitlines()
method on it, and we enumerate over it. For each line, we use the finditer()
function from re
which will return a list of the occurrences to loop over it:
text = text.splitlines()
for i, line in enumerate(text):
for match in re.finditer(pattern, line):
Then we append a tuple with the starting and ending position and return the list at the end after the loop:
matches.append(
(f"{i + 1}.{match.start()}", f"{i + 1}.{match.end() - offset}")
)
return matches
rgbToHex()
Now also cover the small RGB to hex conversion function. By using "string %s" % (value)
syntax, we can insert values from a tuple in a string with placeholders a little bit like f"{var}"
. But with this syntax, we can convert number to hex using %02x
where x means to hex and the 02 means convert it to two characters.
# Convert an RGB tuple to a HEX string using the % Operator
# 02 means print 2 characters
# x means hexadecimal
def rgbToHex(rgb):
return "#%02x%02x%02x" % rgb
In the end, we call the changes()
function, so the styling is applied to the starting text, and we run the main loop:
# Starting the Application
changes()
root.mainloop()
Excellent! You have successfully created a markdown editor using Python code! See how you can add more features to this program, such as:
.md
files. The functionality is covered in the text editor tutorial.Learn also: How to Make a Planet Simulator with PyGame in Python
Happy coding ♥
Liked what you read? You'll love what you can learn from our AI-powered Code Explainer. Check it out!
View Full Code Improve My Code
Got a coding query or need some guidance before you comment? Check out this Python Code Assistant for expert advice and handy tips. It's like having a coding tutor right in your fingertips!