Sadegh
Sadegh

Reputation: 412

How to show markdown formatted text in tkinter?

In python-3.x with tkinter GUI, I developed a program with a regular simple window.

I want to show a markdown format string saved in a string called markdownText on program window:

markdownText='_italic_ or **bold**'

desired output is:

italic or bold

Is there any solution?

Upvotes: 13

Views: 8519

Answers (2)

idbrii
idbrii

Reputation: 11916

If you don't want holroy's great suggestion of converting to html and displaying that, and only want markdown for basic formatting, you can write a basic markdown parser.

However, if you want full markdown support then use a real parser like marko or python-markdown and figure out how to walk the document to add each item to a tk.Text.

Otherwise, have a look:

import re
import tkinter as tk
import tkinter.font as tkfont
import tkinter.scrolledtext as tkscroll


class SimpleMarkdownText(tkscroll.ScrolledText):
    """
    Really basic Markdown display. Thanks to Bryan Oakley's RichText:
    https://stackoverflow.com/a/63105641/79125
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        default_font = tkfont.nametofont(self.cget("font"))

        em = default_font.measure("m")
        default_size = default_font.cget("size")
        bold_font = tkfont.Font(**default_font.configure())
        italic_font = tkfont.Font(**default_font.configure())

        bold_font.configure(weight="bold")
        italic_font.configure(slant="italic")

        # Small subset of markdown. Just enough to make text look nice.
        self.tag_configure("**", font=bold_font)
        self.tag_configure("*", font=italic_font)
        self.tag_configure("_", font=italic_font)
        self.tag_chars = "*_"
        self.tag_char_re = re.compile(r"[*_]")

        max_heading = 3
        for i in range(1, max_heading + 1):
            header_font = tkfont.Font(**default_font.configure())
            header_font.configure(size=int(default_size * i + 3), weight="bold")
            self.tag_configure(
                "#" * (max_heading - i), font=header_font, spacing3=default_size
            )

        lmargin2 = em + default_font.measure("\u2022 ")
        self.tag_configure("bullet", lmargin1=em, lmargin2=lmargin2)
        lmargin2 = em + default_font.measure("1. ")
        self.tag_configure("numbered", lmargin1=em, lmargin2=lmargin2)

        self.numbered_index = 1

    def insert_bullet(self, position, text):
        self.insert(position, f"\u2022 {text}", "bullet")

    def insert_numbered(self, position, text):
        self.insert(position, f"{self.numbered_index}. {text}", "numbered")
        self.numbered_index += 1

    def insert_markdown(self, mkd_text):
        """A very basic markdown parser.

        Helpful to easily set formatted text in tk. If you want actual markdown
        support then use a real parser.
        """
        for line in mkd_text.split("\n"):
            if line == "":
                # Blank lines reset numbering
                self.numbered_index = 1
                self.insert("end", line)

            elif line.startswith("#"):
                tag = re.match(r"(#+) (.*)", line)
                line = tag.group(2)
                self.insert("end", line, tag.group(1))

            elif line.startswith("* "):
                line = line[2:]
                self.insert_bullet("end", line)

            elif line.startswith("1. "):
                line = line[2:]
                self.insert_numbered("end", line)

            elif not self.tag_char_re.search(line):
                self.insert("end", line)

            else:
                tag = None
                accumulated = []
                skip_next = False
                for i, c in enumerate(line):
                    if skip_next:
                        skip_next = False
                        continue
                    if c in self.tag_chars and (not tag or c == tag[0]):
                        if tag:
                            self.insert("end", "".join(accumulated), tag)
                            accumulated = []
                            tag = None
                        else:
                            self.insert("end", "".join(accumulated))
                            accumulated = []
                            tag = c
                            next_i = i + 1
                            if len(line) > next_i and line[next_i] == tag:
                                tag = line[i : next_i + 1]
                                skip_next = True

                    else:
                        accumulated.append(c)
                self.insert("end", "".join(accumulated), tag)

            self.insert("end", "\n")


def test_markdown_display():
    root = tk.Tk()

    default_font = tkfont.nametofont("TkDefaultFont")
    default_font.configure(size=12)

    text = SimpleMarkdownText(root, width=45, height=22, font=default_font)
    text.pack(fill="both", expand=True)

    text.insert_markdown(
        """
# Rich Text Example
Hello, world
This line **has bold** text.
This line _has italicized_ text. Also *italics* using asterisks.
Text _with italics **and bold** does_ not work.

## Sub Heading
This is a more interesting line with _some_ italics, but also **some bold text**.
* Create a list
* With bullets
1. Create a list
1. Or numbers

1. Use blank lines
1. to restart numbering
"""
    )

    root.mainloop()


if __name__ == "__main__":
    test_markdown_display()

Screenshot of SimpleMarkdownText in action

You can see there are lots of limitations: no combined italics+bold, no word-boundary text wrapping. But it seems a lot better for displaying a big block of text.

Upvotes: 3

holroy
holroy

Reputation: 3127

I was just searching for a similar solution, and it does indeed not seem like there is a default module/class/library for the combination of Python, TkInter and markdown. However a continued search revealed the following options:

  • Python-Markdown – This a module capable of parsing markdown into the following output formats: Python-Markdown can output documents in HTML4, XHTML and HTML5.
  • TkInter displaying html – As the linked answer indicates, Tkhtml can display html "pretty well", and it has a python wrapper

In other words, if you are willing to use an intermediate step of converting into html that might be a viable route for you to display markdown strings within a tkinter GUI.

Upvotes: 11

Related Questions