ctk-markdown 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,70 @@
1
+ # This workflow will upload a Python Package to PyPI when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3
+
4
+ # This workflow uses actions that are not certified by GitHub.
5
+ # They are provided by a third-party and are governed by
6
+ # separate terms of service, privacy policy, and support
7
+ # documentation.
8
+
9
+ name: Upload Python Package
10
+
11
+ on:
12
+ release:
13
+ types: [published]
14
+
15
+ permissions:
16
+ contents: read
17
+
18
+ jobs:
19
+ release-build:
20
+ runs-on: ubuntu-latest
21
+
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - uses: actions/setup-python@v5
26
+ with:
27
+ python-version: "3.x"
28
+
29
+ - name: Build release distributions
30
+ run: |
31
+ # NOTE: put your own distribution build steps here.
32
+ python -m pip install build
33
+ python -m build
34
+
35
+ - name: Upload distributions
36
+ uses: actions/upload-artifact@v4
37
+ with:
38
+ name: release-dists
39
+ path: dist/
40
+
41
+ pypi-publish:
42
+ runs-on: ubuntu-latest
43
+ needs:
44
+ - release-build
45
+ permissions:
46
+ # IMPORTANT: this permission is mandatory for trusted publishing
47
+ id-token: write
48
+
49
+ # Dedicated environments with protections for publishing are strongly recommended.
50
+ # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
51
+ environment:
52
+ name: pypi
53
+ # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
54
+ # url: https://pypi.org/p/YOURPROJECT
55
+ #
56
+ # ALTERNATIVE: if your GitHub Release name is the PyPI project version string
57
+ # ALTERNATIVE: exactly, uncomment the following line instead:
58
+ # url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
59
+
60
+ steps:
61
+ - name: Retrieve release distributions
62
+ uses: actions/download-artifact@v4
63
+ with:
64
+ name: release-dists
65
+ path: dist/
66
+
67
+ - name: Publish release distributions to PyPI
68
+ uses: pypa/gh-action-pypi-publish@release/v1
69
+ with:
70
+ packages-dir: dist/
@@ -0,0 +1,4 @@
1
+ __pycache__/
2
+ dist/
3
+ *.egg-info/
4
+ .venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Luka Emanuell Freitas de Souza Gouvêa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: ctk-markdown
3
+ Version: 0.1.0
4
+ Summary: Markdown Renderer for customtkinter
5
+ Project-URL: Homepage, https://https://github.com/lukagouvea/MarkdownRenderer
6
+ Project-URL: Bug Tracker, https://https://github.com/lukagouvea/MarkdownRenderer/issues
7
+ Author-email: Luka Gouvea <luka.gouvea0@gmail.com>
8
+ License-File: LICENSE
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.7
13
+ Requires-Dist: customtkinter
14
+ Description-Content-Type: text/markdown
15
+
16
+ # ctk-markdown
17
+
18
+ ![python](https://img.shields.io/badge/python-3.9%2B-blue)
19
+ ![customtkinter](https://img.shields.io/badge/customtkinter-required-1f6feb)
20
+ ![status](https://img.shields.io/badge/status-alpha-orange)
21
+
22
+ `ctk-markdown` is a **CustomTkinter** Markdown renderer based on `CTkTextbox`. It applies rich tags for headings, lists, tables, code blocks, and inline formatting.
23
+
24
+ ## ✨ Features
25
+
26
+ - Single widget (`CTkMarkdown`) with Markdown rendering
27
+ - Headings, lists, blockquotes, tables, and code blocks
28
+ - Basic syntax highlighting for Python and JavaScript
29
+ - Theme-aware colors for light and dark appearance modes
30
+
31
+ ## 📦 Installation
32
+
33
+ ```bash
34
+ pip install ctk-markdown
35
+ ```
36
+
37
+ ## 🚀 Usage
38
+
39
+ ```python
40
+ from ctk_markdown import CTkMarkdown
41
+ import customtkinter as ctk
42
+
43
+ app = ctk.CTk()
44
+ frame = ctk.CTkFrame(app)
45
+ frame.pack(fill="both", expand=True, padx=16, pady=16)
46
+
47
+ renderer = CTkMarkdown(frame)
48
+ renderer.pack(fill="both", expand=True)
49
+ renderer.set_markdown("""# Title\nText with *italic* and **bold**.""")
50
+
51
+ app.mainloop()
52
+ ```
53
+
54
+ ## 🧠 How it works
55
+
56
+ The widget inherits from `CTkTextbox`. Markdown parsing is done line by line and uses Tkinter text tags for styling. Theme colors are applied based on the current CustomTkinter appearance mode.
57
+
58
+ ## 🧪 Run the demo
59
+
60
+ ```bash
61
+ python example.py
62
+ ```
63
+
64
+ ## 🤝 Contributing
65
+
66
+ Contributions are welcome!
67
+
68
+ 1. Fork the repo
69
+ 2. Create a feature branch (`feature/my-change`)
70
+ 3. Commit your changes
71
+ 4. Open a Pull Request with a clear description
72
+
73
+ Ideas:
74
+ - More language grammars for code highlighting
75
+ - Image support
76
+ - Clickable links
77
+
78
+ ## 📄 License
79
+
80
+ Choose a license (e.g., MIT) before publishing to GitHub.
@@ -0,0 +1,65 @@
1
+ # ctk-markdown
2
+
3
+ ![python](https://img.shields.io/badge/python-3.9%2B-blue)
4
+ ![customtkinter](https://img.shields.io/badge/customtkinter-required-1f6feb)
5
+ ![status](https://img.shields.io/badge/status-alpha-orange)
6
+
7
+ `ctk-markdown` is a **CustomTkinter** Markdown renderer based on `CTkTextbox`. It applies rich tags for headings, lists, tables, code blocks, and inline formatting.
8
+
9
+ ## ✨ Features
10
+
11
+ - Single widget (`CTkMarkdown`) with Markdown rendering
12
+ - Headings, lists, blockquotes, tables, and code blocks
13
+ - Basic syntax highlighting for Python and JavaScript
14
+ - Theme-aware colors for light and dark appearance modes
15
+
16
+ ## 📦 Installation
17
+
18
+ ```bash
19
+ pip install ctk-markdown
20
+ ```
21
+
22
+ ## 🚀 Usage
23
+
24
+ ```python
25
+ from ctk_markdown import CTkMarkdown
26
+ import customtkinter as ctk
27
+
28
+ app = ctk.CTk()
29
+ frame = ctk.CTkFrame(app)
30
+ frame.pack(fill="both", expand=True, padx=16, pady=16)
31
+
32
+ renderer = CTkMarkdown(frame)
33
+ renderer.pack(fill="both", expand=True)
34
+ renderer.set_markdown("""# Title\nText with *italic* and **bold**.""")
35
+
36
+ app.mainloop()
37
+ ```
38
+
39
+ ## 🧠 How it works
40
+
41
+ The widget inherits from `CTkTextbox`. Markdown parsing is done line by line and uses Tkinter text tags for styling. Theme colors are applied based on the current CustomTkinter appearance mode.
42
+
43
+ ## 🧪 Run the demo
44
+
45
+ ```bash
46
+ python example.py
47
+ ```
48
+
49
+ ## 🤝 Contributing
50
+
51
+ Contributions are welcome!
52
+
53
+ 1. Fork the repo
54
+ 2. Create a feature branch (`feature/my-change`)
55
+ 3. Commit your changes
56
+ 4. Open a Pull Request with a clear description
57
+
58
+ Ideas:
59
+ - More language grammars for code highlighting
60
+ - Image support
61
+ - Clickable links
62
+
63
+ ## 📄 License
64
+
65
+ Choose a license (e.g., MIT) before publishing to GitHub.
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ctk-markdown"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name="Luka Gouvea", email="luka.gouvea0@gmail.com" },
10
+ ]
11
+ description = "Markdown Renderer for customtkinter"
12
+ readme = "README.md"
13
+ requires-python = ">=3.7"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ dependencies = [
20
+ "customtkinter"
21
+ ]
22
+
23
+ [project.urls]
24
+ "Homepage" = "https://https://github.com/lukagouvea/MarkdownRenderer"
25
+ "Bug Tracker" = "https://https://github.com/lukagouvea/MarkdownRenderer/issues"
@@ -0,0 +1,3 @@
1
+ from .ctk_markdown import CTkMarkdown
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,665 @@
1
+ """
2
+ Tkinter component for Markdown rendering.
3
+ Uses ctk.CTkTextbox with custom tags for better control and rendering.
4
+ """
5
+
6
+ import tkinter as tk
7
+ import tkinter.font as tkfont
8
+ import customtkinter as ctk
9
+ import re
10
+
11
+ class CTkMarkdown(ctk.CTkTextbox):
12
+ """CTkTextbox widget with Markdown rendering."""
13
+
14
+ # Keywords for syntax highlighting
15
+ PYTHON_KEYWORDS = {
16
+ 'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await',
17
+ 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except',
18
+ 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is',
19
+ 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try',
20
+ 'while', 'with', 'yield', 'print', 'len', 'range', 'str', 'int',
21
+ 'float', 'list', 'dict', 'set', 'tuple', 'open', 'input', 'type'
22
+ }
23
+
24
+ JS_KEYWORDS = {
25
+ 'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue',
26
+ 'debugger', 'default', 'delete', 'do', 'else', 'export', 'extends',
27
+ 'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof',
28
+ 'let', 'new', 'return', 'static', 'super', 'switch', 'this', 'throw',
29
+ 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield', 'console',
30
+ 'log', 'true', 'false', 'null', 'undefined'
31
+ }
32
+
33
+ def __init__(self, master, markdown_text="", **kwargs):
34
+ defaults = {
35
+ "cursor": "arrow",
36
+ }
37
+ if 'bg' in kwargs: kwargs['fg_color'] = kwargs.pop('bg')
38
+ if 'fg' in kwargs: kwargs['text_color'] = kwargs.pop('fg')
39
+ if 'borderwidth' in kwargs: kwargs['border_width'] = kwargs.pop('borderwidth')
40
+ if 'relief' in kwargs: kwargs.pop('relief')
41
+ if 'yscrollcommand' in kwargs: kwargs.pop('yscrollcommand')
42
+ defaults.update(kwargs)
43
+ super().__init__(master, **defaults)
44
+ self._setup_tags()
45
+ try:
46
+ ctk.AppearanceModeTracker.add(self._apply_theme, self)
47
+ except Exception:
48
+ pass
49
+ self._render_markdown(markdown_text)
50
+
51
+ def _setup_tags(self):
52
+ """Configure formatting tags."""
53
+ # Fonts
54
+ base_font = tkfont.Font(font=self._textbox.cget('font'))
55
+ base_size = int(base_font.cget('size'))
56
+ base_family = base_font.cget('family')
57
+
58
+ self._theme_colors = {
59
+ 'light': {
60
+ 'heading_1': '#1a1a2e',
61
+ 'heading_2': '#16213e',
62
+ 'heading_3': '#1f4068',
63
+ 'heading_4': '#1b1b2f',
64
+ 'heading_5': '#464866',
65
+ 'heading_6': '#6b778d',
66
+ 'muted': '#6c757d',
67
+ 'link': '#0d6efd',
68
+ 'code_inline_fg': '#d63384',
69
+ 'code_inline_bg': '#f6f8fa',
70
+ 'code_block_fg': '#1f2328',
71
+ 'code_block_bg': "#EEEEEE",
72
+ 'code_keyword': '#0550ae',
73
+ 'code_string': '#0a3069',
74
+ 'code_comment': '#6e7781',
75
+ 'code_number': '#953800',
76
+ 'code_function': '#8250df',
77
+ 'code_class': '#1f6feb',
78
+ 'code_decorator': '#a371f7',
79
+ 'code_operator': '#24292f',
80
+ 'blockquote_fg': '#6c757d',
81
+ 'blockquote_bg': '#f8f9fa',
82
+ 'list_bullet': '#6c757d',
83
+ 'list_number': '#0d6efd',
84
+ 'hr': '#dee2e6',
85
+ 'table_border': '#6c757d',
86
+ 'table_header_bg': '#e9ecef',
87
+ 'table_header_fg': '#212529',
88
+ 'table_cell_bg': '#ffffff',
89
+ 'table_cell_fg': '#212529',
90
+ 'table_row_alt_bg': '#f8f9fa',
91
+ 'checkbox_done': '#198754',
92
+ 'checkbox_pending': '#dc3545'
93
+ },
94
+ 'dark': {
95
+ 'heading_1': '#e6edf3',
96
+ 'heading_2': '#d1d9e0',
97
+ 'heading_3': '#b6c2cf',
98
+ 'heading_4': '#9fb0c2',
99
+ 'heading_5': '#8b9bb0',
100
+ 'heading_6': '#778899',
101
+ 'muted': '#9aa0a6',
102
+ 'link': '#4da3ff',
103
+ 'code_inline_fg': '#ff7aa8',
104
+ 'code_inline_bg': '#2b2b2b',
105
+ 'code_block_fg': '#f0f6fc',
106
+ 'code_block_bg': "#212121",
107
+ 'code_keyword': '#569cd6',
108
+ 'code_string': '#ce9178',
109
+ 'code_comment': '#6a9955',
110
+ 'code_number': '#b5cea8',
111
+ 'code_function': '#dcdcaa',
112
+ 'code_class': '#4ec9b0',
113
+ 'code_decorator': '#c586c0',
114
+ 'code_operator': '#d4d4d4',
115
+ 'blockquote_fg': '#9aa0a6',
116
+ 'blockquote_bg': '#20242a',
117
+ 'list_bullet': '#9aa0a6',
118
+ 'list_number': '#4da3ff',
119
+ 'hr': '#30363d',
120
+ 'table_border': '#4b5563',
121
+ 'table_header_bg': '#30363d',
122
+ 'table_header_fg': '#e6edf3',
123
+ 'table_cell_bg': '#0d1117',
124
+ 'table_cell_fg': '#c9d1d9',
125
+ 'table_row_alt_bg': '#161b22',
126
+ 'checkbox_done': '#3fb950',
127
+ 'checkbox_pending': '#ff7b72'
128
+ }
129
+ }
130
+
131
+ # Headings
132
+ self._textbox.tag_config('h1', font=('Segoe UI', base_size + 12, 'bold'),
133
+ spacing1=20, spacing3=10)
134
+ self._textbox.tag_config('h2', font=('Segoe UI', base_size + 8, 'bold'),
135
+ spacing1=18, spacing3=8)
136
+ self._textbox.tag_config('h3', font=('Segoe UI', base_size + 5, 'bold'),
137
+ spacing1=15, spacing3=6)
138
+ self._textbox.tag_config('h4', font=('Segoe UI', base_size + 3, 'bold'),
139
+ spacing1=12, spacing3=5)
140
+ self._textbox.tag_config('h5', font=('Segoe UI', base_size + 2, 'bold'),
141
+ spacing1=10, spacing3=4)
142
+ self._textbox.tag_config('h6', font=('Segoe UI', base_size + 1, 'bold'),
143
+ spacing1=8, spacing3=3)
144
+
145
+ # Text formatting
146
+ self._textbox.tag_config('bold', font=(base_family, base_size, 'bold'))
147
+ self._textbox.tag_config('italic', font=(base_family, base_size, 'italic'))
148
+ self._textbox.tag_config('bold_italic', font=(base_family, base_size, 'bold italic'))
149
+ self._textbox.tag_config('strikethrough', overstrike=True)
150
+ self._textbox.tag_config('underline', underline=True)
151
+
152
+ # Inline code
153
+ self._textbox.tag_config('code_inline',
154
+ font=('Consolas', base_size),
155
+ spacing1=2)
156
+
157
+ # Code block
158
+ self._textbox.tag_config('code_block',
159
+ font=('Consolas', base_size),
160
+ spacing1=10,
161
+ spacing3=10,
162
+ lmargin1=20,
163
+ lmargin2=20,
164
+ rmargin=20)
165
+
166
+ # Syntax highlighting for code
167
+ self._textbox.tag_config('code_keyword', font=('Consolas', base_size - 1))
168
+ self._textbox.tag_config('code_string', font=('Consolas', base_size - 1))
169
+ self._textbox.tag_config('code_comment', font=('Consolas', base_size - 1))
170
+ self._textbox.tag_config('code_number', font=('Consolas', base_size - 1))
171
+ self._textbox.tag_config('code_function', font=('Consolas', base_size - 1))
172
+ self._textbox.tag_config('code_class', font=('Consolas', base_size - 1))
173
+ self._textbox.tag_config('code_decorator', font=('Consolas', base_size - 1))
174
+ self._textbox.tag_config('code_operator', font=('Consolas', base_size - 1))
175
+
176
+ # Blockquote
177
+ self._textbox.tag_config('blockquote',
178
+ font=('Segoe UI', base_size, 'italic'),
179
+ lmargin1=30,
180
+ lmargin2=30,
181
+ spacing1=8,
182
+ spacing3=8,
183
+ borderwidth=3)
184
+
185
+ # Links
186
+ self._textbox.tag_config('link', underline=True)
187
+ self._textbox.tag_bind('link', '<Enter>', lambda e: self.configure(cursor='hand2'))
188
+ self._textbox.tag_bind('link', '<Leave>', lambda e: self.configure(cursor='arrow'))
189
+
190
+ # Lists
191
+ self._textbox.tag_config('list_item', lmargin1=25, lmargin2=40)
192
+ self._textbox.tag_config('list_bullet')
193
+ self._textbox.tag_config('list_number', font=('Segoe UI', base_size, 'bold'))
194
+
195
+ # Horizontal rule
196
+ self._textbox.tag_config('hr', font=('Segoe UI', 4),
197
+ spacing1=15, spacing3=15, justify='center')
198
+
199
+ # Table
200
+ self._textbox.tag_config('table_border', font=('Consolas', base_size))
201
+ self._textbox.tag_config('table_header', font=('Consolas', base_size, 'bold'))
202
+ self._textbox.tag_config('table_cell', font=('Consolas', base_size))
203
+ self._textbox.tag_config('table_row_alt', font=('Consolas', base_size))
204
+
205
+ # Checkbox
206
+ self._textbox.tag_config('checkbox_done')
207
+ self._textbox.tag_config('checkbox_pending')
208
+
209
+ self._apply_theme()
210
+
211
+ def _get_mode(self, mode=None):
212
+ if mode is None:
213
+ mode = ctk.get_appearance_mode()
214
+ return 'dark' if str(mode).lower().startswith('dark') else 'light'
215
+
216
+ def _apply_theme(self, mode=None):
217
+ mode = self._get_mode(mode)
218
+ colors = self._theme_colors[mode]
219
+ tb = self._textbox
220
+
221
+ tb.tag_config('h1', foreground=colors['heading_1'])
222
+ tb.tag_config('h2', foreground=colors['heading_2'])
223
+ tb.tag_config('h3', foreground=colors['heading_3'])
224
+ tb.tag_config('h4', foreground=colors['heading_4'])
225
+ tb.tag_config('h5', foreground=colors['heading_5'])
226
+ tb.tag_config('h6', foreground=colors['heading_6'])
227
+
228
+ tb.tag_config('strikethrough', foreground=colors['muted'])
229
+ tb.tag_config('code_inline', foreground=colors['code_inline_fg'], background=colors['code_inline_bg'])
230
+ tb.tag_config('code_block', foreground=colors['code_block_fg'], background=colors['code_block_bg'])
231
+
232
+ tb.tag_config('code_keyword', foreground=colors['code_keyword'])
233
+ tb.tag_config('code_string', foreground=colors['code_string'])
234
+ tb.tag_config('code_comment', foreground=colors['code_comment'])
235
+ tb.tag_config('code_number', foreground=colors['code_number'])
236
+ tb.tag_config('code_function', foreground=colors['code_function'])
237
+ tb.tag_config('code_class', foreground=colors['code_class'])
238
+ tb.tag_config('code_decorator', foreground=colors['code_decorator'])
239
+ tb.tag_config('code_operator', foreground=colors['code_operator'])
240
+
241
+ tb.tag_config('blockquote', foreground=colors['blockquote_fg'], background=colors['blockquote_bg'])
242
+ tb.tag_config('link', foreground=colors['link'])
243
+
244
+ tb.tag_config('list_bullet', foreground=colors['list_bullet'])
245
+ tb.tag_config('list_number', foreground=colors['list_number'])
246
+
247
+ tb.tag_config('hr', foreground=colors['hr'])
248
+
249
+ tb.tag_config('table_border', foreground=colors['table_border'])
250
+ tb.tag_config('table_header', background=colors['table_header_bg'], foreground=colors['table_header_fg'])
251
+ tb.tag_config('table_cell', background=colors['table_cell_bg'], foreground=colors['table_cell_fg'])
252
+ tb.tag_config('table_row_alt', background=colors['table_row_alt_bg'], foreground=colors['table_cell_fg'])
253
+
254
+ tb.tag_config('checkbox_done', foreground=colors['checkbox_done'])
255
+ tb.tag_config('checkbox_pending', foreground=colors['checkbox_pending'])
256
+
257
+
258
+ def set_markdown(self, markdown_text: str):
259
+ """Set the Markdown text to be rendered."""
260
+ self._render_markdown(markdown_text)
261
+
262
+ def _render_markdown(self, text: str):
263
+ """Process and render Markdown."""
264
+ self.configure(state='normal')
265
+ self.delete("0.0", "end")
266
+
267
+ lines = text.split('\n')
268
+ i = 0
269
+ in_code_block = False
270
+ code_block_content = []
271
+ code_language = ""
272
+
273
+ while i < len(lines):
274
+ line = lines[i]
275
+
276
+ # Code block
277
+ if line.strip().startswith('```'):
278
+ if not in_code_block:
279
+ in_code_block = True
280
+ code_language = line.strip()[3:].strip().lower()
281
+ code_block_content = []
282
+ else:
283
+ in_code_block = False
284
+ self._insert_code_block('\n'.join(code_block_content), code_language)
285
+ code_block_content = []
286
+ code_language = ""
287
+ i += 1
288
+ continue
289
+
290
+ if in_code_block:
291
+ code_block_content.append(line)
292
+ i += 1
293
+ continue
294
+
295
+ # Horizontal rule
296
+ if re.match(r'^(-{3,}|\*{3,}|_{3,})\s*$', line.strip()):
297
+ self.insert(tk.END, '─' * 60 + '\n', 'hr')
298
+ i += 1
299
+ continue
300
+
301
+ # Headings
302
+ header_match = re.match(r'^\s*(#{1,6})\s+(.+)$', line)
303
+ if header_match:
304
+ level = len(header_match.group(1))
305
+ content = header_match.group(2)
306
+ self._insert_formatted_text(content, f'h{level}')
307
+ self.insert(tk.END, '\n')
308
+ i += 1
309
+ continue
310
+
311
+ # Blockquote
312
+ if line.strip().startswith('>'):
313
+ quote_lines = []
314
+ while i < len(lines) and lines[i].strip().startswith('>'):
315
+ quote_lines.append(lines[i].strip()[1:].strip())
316
+ i += 1
317
+ quote_text = ' '.join(quote_lines)
318
+ self.insert(tk.END, '┃ ', 'blockquote')
319
+ self._insert_formatted_text(quote_text + ' ', 'blockquote')
320
+ self.insert(tk.END, '\n\n')
321
+ continue
322
+
323
+ # Unordered list
324
+ list_match = re.match(r'^(\s*)([-*+])\s+(.+)$', line)
325
+ if list_match:
326
+ indent = len(list_match.group(1)) // 2
327
+ content = list_match.group(3)
328
+
329
+ # Checkbox
330
+ checkbox_match = re.match(r'\[([ xX])\]\s*(.+)', content)
331
+ if checkbox_match:
332
+ checked = checkbox_match.group(1).lower() == 'x'
333
+ text_content = checkbox_match.group(2)
334
+ checkbox = '☑' if checked else '☐'
335
+ tag = 'checkbox_done' if checked else 'checkbox_pending'
336
+ self.insert(tk.END, ' ' * indent + checkbox + ' ', tag)
337
+ self._insert_formatted_text(text_content, 'list_item')
338
+ else:
339
+ self.insert(tk.END, ' ' * indent + '• ', 'list_bullet')
340
+ self._insert_formatted_text(content, 'list_item')
341
+
342
+ self.insert(tk.END, '\n')
343
+ i += 1
344
+ continue
345
+
346
+ # Ordered list
347
+ ordered_match = re.match(r'^(\s*)(\d+)\.\s+(.+)$', line)
348
+ if ordered_match:
349
+ indent = len(ordered_match.group(1)) // 2
350
+ num = ordered_match.group(2)
351
+ content = ordered_match.group(3)
352
+ self.insert(tk.END, ' ' * indent + f'{num}. ', 'list_number')
353
+ self._insert_formatted_text(content, 'list_item')
354
+ self.insert(tk.END, '\n')
355
+ i += 1
356
+ continue
357
+
358
+ # Table
359
+ if '|' in line and i + 1 < len(lines) and re.match(r'^[\s|:-]+$', lines[i + 1]):
360
+ table_lines = []
361
+ while i < len(lines) and '|' in lines[i]:
362
+ table_lines.append(lines[i])
363
+ i += 1
364
+ self._insert_table(table_lines)
365
+ continue
366
+
367
+ # Normal paragraph
368
+ if line.strip():
369
+ self._insert_formatted_text(line)
370
+ self.insert(tk.END, '\n')
371
+ else:
372
+ self.insert(tk.END, '\n')
373
+
374
+ i += 1
375
+
376
+ self.configure(state='disabled')
377
+
378
+ def _insert_formatted_text(self, text: str, base_tag: str = None):
379
+ """Insert text with inline formatting."""
380
+ pattern = re.compile(
381
+ r'(?P<bold_italic>\*\*\*(?P<bold_italic_text>.+?)\*\*\*|___(?P<bold_italic_text2>.+?)___)'
382
+ r'|(?P<bold>\*\*(?P<bold_text>.+?)\*\*|__(?P<bold_text2>.+?)__)'
383
+ r'|(?P<italic>\*(?P<italic_text>.+?)\*|_(?P<italic_text2>.+?)_)'
384
+ r'|(?P<strike>~~(?P<strike_text>.+?)~~)'
385
+ r'|(?P<code>`(?P<code_text>[^`]+)`)'
386
+ r'|(?P<link>\[(?P<link_text>[^\]]+)\]\((?P<link_url>[^)]+)\))'
387
+ )
388
+
389
+ last_end = 0
390
+ for match in pattern.finditer(text):
391
+ start, end = match.span()
392
+ # Text before formatting
393
+ if start > last_end:
394
+ plain_text = text[last_end:start]
395
+ if base_tag:
396
+ self.insert(tk.END, plain_text, base_tag)
397
+ else:
398
+ self.insert(tk.END, plain_text)
399
+
400
+ if match.group('bold_italic'):
401
+ content = match.group('bold_italic_text') or match.group('bold_italic_text2')
402
+ tags = ('bold_italic', base_tag) if base_tag else ('bold_italic',)
403
+ self.insert(tk.END, content, tags)
404
+ elif match.group('bold'):
405
+ content = match.group('bold_text') or match.group('bold_text2')
406
+ tags = ('bold', base_tag) if base_tag else ('bold',)
407
+ self.insert(tk.END, content, tags)
408
+ elif match.group('italic'):
409
+ content = match.group('italic_text') or match.group('italic_text2')
410
+ tags = ('italic', base_tag) if base_tag else ('italic',)
411
+ self.insert(tk.END, content, tags)
412
+ elif match.group('strike'):
413
+ content = match.group('strike_text')
414
+ tags = ('strikethrough', base_tag) if base_tag else ('strikethrough',)
415
+ self.insert(tk.END, content, tags)
416
+ elif match.group('code'):
417
+ content = match.group('code_text')
418
+ tags = ('code_inline', base_tag) if base_tag else ('code_inline',)
419
+ self.insert(tk.END, content, tags)
420
+ elif match.group('link'):
421
+ link_text = match.group('link_text')
422
+ # link_url = match.group('link_url') # Can be used to open links
423
+ tags = ('link', base_tag) if base_tag else ('link',)
424
+ self.insert(tk.END, link_text, tags)
425
+
426
+ last_end = end
427
+
428
+ # Remaining text
429
+ if last_end < len(text):
430
+ remaining = text[last_end:]
431
+ if base_tag:
432
+ self.insert(tk.END, remaining, base_tag)
433
+ else:
434
+ self.insert(tk.END, remaining)
435
+
436
+ def _insert_code_block(self, code: str, language: str):
437
+ """Insert a code block with syntax highlighting."""
438
+ self.insert(tk.END, '\n')
439
+
440
+ # Code block header
441
+ if language:
442
+ lang_display = language.upper()
443
+ self.insert(tk.END, f' {lang_display} \n', 'code_block')
444
+
445
+ # Apply syntax highlighting
446
+ if language in ('python', 'py'):
447
+ self._highlight_python(code)
448
+ elif language in ('javascript', 'js', 'typescript', 'ts'):
449
+ self._highlight_javascript(code)
450
+ else:
451
+ self.insert(tk.END, code + '\n', 'code_block')
452
+
453
+ self.insert(tk.END, '\n')
454
+
455
+ def _highlight_python(self, code: str):
456
+ """Syntax highlighting for Python."""
457
+ # Patterns for Python
458
+ patterns = [
459
+ (r'#.*$', 'code_comment'), # Comments
460
+ (r'("""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\')', 'code_string'), # Docstrings
461
+ (r'(["\'])(?:(?!\1|\\).|\\.)*\1', 'code_string'), # Strings
462
+ (r'\b(\d+\.?\d*)\b', 'code_number'), # Numbers
463
+ (r'@\w+', 'code_decorator'), # Decorators
464
+ (r'\bdef\s+(\w+)', 'code_function'), # Functions
465
+ (r'\bclass\s+(\w+)', 'code_class'), # Classes
466
+ ]
467
+
468
+ lines = code.split('\n')
469
+ for line in lines:
470
+ self._highlight_line(line, patterns, self.PYTHON_KEYWORDS)
471
+ self.insert(tk.END, '\n', 'code_block')
472
+
473
+ def _highlight_javascript(self, code: str):
474
+ """Syntax highlighting for JavaScript."""
475
+ patterns = [
476
+ (r'//.*$', 'code_comment'), # Line comments
477
+ (r'/\*[\s\S]*?\*/', 'code_comment'), # Block comments
478
+ (r'(["\'])(?:(?!\1|\\).|\\.)*\1', 'code_string'), # Strings
479
+ (r'`[^`]*`', 'code_string'), # Template literals
480
+ (r'\b(\d+\.?\d*)\b', 'code_number'), # Numbers
481
+ (r'\bfunction\s+(\w+)', 'code_function'), # Functions
482
+ (r'const\s+(\w+)\s*=\s*\([^)]*\)\s*=>', 'code_function'), # Arrow functions
483
+ ]
484
+
485
+ lines = code.split('\n')
486
+ for line in lines:
487
+ self._highlight_line(line, patterns, self.JS_KEYWORDS)
488
+ self.insert(tk.END, '\n', 'code_block')
489
+
490
+ def _highlight_line(self, line: str, patterns: list, keywords: set):
491
+ """Apply highlighting to a line."""
492
+ if not line:
493
+ return
494
+
495
+ # Find all matches
496
+ highlights = [] # (start, end, tag)
497
+
498
+ for pattern, tag in patterns:
499
+ for match in re.finditer(pattern, line, re.MULTILINE):
500
+ highlights.append((match.start(), match.end(), tag))
501
+
502
+ # Add keywords
503
+ for keyword in keywords:
504
+ pattern = rf'\b{re.escape(keyword)}\b'
505
+ for match in re.finditer(pattern, line):
506
+ highlights.append((match.start(), match.end(), 'code_keyword'))
507
+
508
+ # Sort and remove overlaps
509
+ highlights.sort(key=lambda x: (x[0], -x[1]))
510
+ filtered = []
511
+ last_end = 0
512
+ for start, end, tag in highlights:
513
+ if start >= last_end:
514
+ filtered.append((start, end, tag))
515
+ last_end = end
516
+
517
+ # Insert text with highlighting
518
+ last_pos = 0
519
+ for start, end, tag in filtered:
520
+ if start > last_pos:
521
+ self.insert(tk.END, line[last_pos:start], 'code_block')
522
+ self.insert(tk.END, line[start:end], ('code_block', tag))
523
+ last_pos = end
524
+
525
+ if last_pos < len(line):
526
+ self.insert(tk.END, line[last_pos:], 'code_block')
527
+
528
+ def _insert_table(self, table_lines: list):
529
+ """Insert a table using a real widget (Frame + Grid) for precise alignment."""
530
+ if len(table_lines) < 2:
531
+ return
532
+
533
+ # Parse headers
534
+ header_line = table_lines[0].strip()
535
+ if header_line.startswith('|'): header_line = header_line[1:]
536
+ if header_line.endswith('|'): header_line = header_line[:-1]
537
+ headers = [cell.strip() for cell in header_line.split('|')]
538
+
539
+ # Parse rows
540
+ rows = []
541
+ for line in table_lines[2:]:
542
+ line = line.strip()
543
+ if line.startswith('|'): line = line[1:]
544
+ if line.endswith('|'): line = line[:-1]
545
+ cells = [cell.strip() for cell in line.split('|')]
546
+ if cells and any(c for c in cells):
547
+ rows.append(cells)
548
+
549
+ # Create a container for the table
550
+ # The bg here defines the "border" color between cells
551
+ table_frame = tk.Frame(self, bg='#bdc3c7', padx=0, pady=0)
552
+
553
+ # Add headers
554
+ for col, header in enumerate(headers):
555
+ lbl = tk.Label(table_frame, text=header, font=('Segoe UI', 10, 'bold'),
556
+ bg='#e9ecef', fg='#212529', padx=10, pady=5,
557
+ relief='flat', anchor='w')
558
+ lbl.grid(row=0, column=col, sticky='nsew', padx=1, pady=1)
559
+
560
+ # Add data rows
561
+ for row_idx, row in enumerate(rows):
562
+ for col_idx in range(len(headers)):
563
+ cell_text = row[col_idx] if col_idx < len(row) else ""
564
+ bg_color = '#f8f9fa' if row_idx % 2 == 1 else '#ffffff'
565
+ lbl = tk.Label(table_frame, text=cell_text, font=('Segoe UI', 10),
566
+ bg=bg_color, fg='#333333', padx=10, pady=5,
567
+ relief='flat', anchor='w')
568
+ lbl.grid(row=row_idx + 1, column=col_idx, sticky='nsew', padx=1, pady=1)
569
+
570
+ # Force columns to have weight for spacing distribution
571
+ for col in range(len(headers)):
572
+ table_frame.columnconfigure(col, weight=1)
573
+
574
+ # Insert the table widget inside the Text
575
+ self.insert(tk.END, '\n')
576
+ self._textbox.window_create(tk.END, window=table_frame)
577
+ self.insert(tk.END, '\n')
578
+
579
+ def _insert_sample(self):
580
+ """Insert sample text."""
581
+ sample = '''# 🎉 Renderizador Markdown para Tkinter
582
+
583
+ Este é um **componente nativo** para visualizar *Markdown* em tempo real!
584
+ ---
585
+ ## ✨ Formatação de Texto
586
+
587
+ - **Texto em negrito** usando `**texto**`
588
+ - *Texto em itálico* usando `*texto*`
589
+ - ***Negrito e itálico*** usando `***texto***`
590
+ - ~~Texto riscado~~ usando `~~texto~~`
591
+ - `Código inline` usando crases
592
+
593
+ ## 📝 Listas
594
+
595
+ ### Lista não ordenada:
596
+ - Item principal
597
+ - Sub-item
598
+ - Outro sub-item
599
+ - Sub-sub-item
600
+ - Segundo item
601
+ - Terceiro item
602
+
603
+ ### Lista ordenada:
604
+ 1. Primeiro passo
605
+ 2. Segundo passo
606
+ 3. Terceiro passo
607
+
608
+ ### Checkboxes:
609
+ - [x] Tarefa concluída
610
+ - [ ] Tarefa pendente
611
+ - [x] Outra tarefa feita
612
+
613
+ ## 💬 Citações
614
+
615
+ > "A simplicidade é a sofisticação máxima."
616
+ > — Leonardo da Vinci
617
+
618
+ ## 💻 Blocos de Código
619
+
620
+ ### Python:
621
+ ```python
622
+ def fibonacci(n):
623
+ """Calcula o n-ésimo número de Fibonacci"""
624
+ if n <= 1:
625
+ return n
626
+ return fibonacci(n-1) + fibonacci(n-2)
627
+
628
+ # Exemplo de uso
629
+ for i in range(10):
630
+ print(f"F({i}) = {fibonacci(i)}")
631
+ ```
632
+
633
+ ### JavaScript:
634
+ ```javascript
635
+ // Função assíncrona moderna
636
+ const fetchData = async (url) => {
637
+ const response = await fetch(url);
638
+ const data = await response.json();
639
+ console.log("Dados recebidos:", data);
640
+ return data;
641
+ };
642
+
643
+ fetchData("https://api.exemplo.com/dados");
644
+ ```
645
+
646
+ ## 📊 Tabelas
647
+
648
+ | Linguagem | Tipo | Popularidade |
649
+ |-----------|------|--------------|
650
+ | Python | Dinâmica | ⭐⭐⭐⭐⭐ |
651
+ | JavaScript | Dinâmica | ⭐⭐⭐⭐⭐ |
652
+ | Rust | Estática | ⭐⭐⭐⭐ |
653
+ | Go | Estática | ⭐⭐⭐⭐ |
654
+
655
+ ---
656
+
657
+ ## 🔗 Links
658
+
659
+ Visite o [Python.org](https://python.org) para mais informações!
660
+
661
+ ---
662
+
663
+ **Divirta-se escrevendo em Markdown!** 🚀
664
+ '''
665
+ self._render_markdown(sample)
@@ -0,0 +1,58 @@
1
+ from ctk_markdown import CTkMarkdown
2
+ import customtkinter as ctk
3
+ import tkinter as tk
4
+
5
+
6
+ # ============================================================
7
+ # Demo Application
8
+ # ============================================================
9
+
10
+ def main():
11
+ """Main function"""
12
+ root = ctk.CTk()
13
+ root.title("📝 Markdown Editor & Viewer")
14
+ root.geometry("1300x800")
15
+ root.configure(fg_color='#2c3e50')
16
+
17
+ # Toolbar
18
+ toolbar = ctk.CTkFrame(root, fg_color='#34495e', corner_radius=0)
19
+ toolbar.pack(fill=tk.X)
20
+
21
+ title = ctk.CTkLabel(toolbar, text="🔮 Markdown Renderer",
22
+ font=('Segoe UI', 16, 'bold'),
23
+ text_color='white')
24
+ title.pack(side=tk.LEFT, padx=20, pady=10)
25
+
26
+ # Buttons
27
+ btn_frame = ctk.CTkFrame(toolbar, fg_color='transparent')
28
+ btn_frame.pack(side=tk.RIGHT, padx=20)
29
+
30
+ def clear():
31
+ renderer.set_markdown("")
32
+
33
+ def export_md():
34
+ content = renderer.get("0.0", tk.END)
35
+ root.clipboard_clear()
36
+ root.clipboard_append(content)
37
+
38
+ ctk.CTkButton(btn_frame, text="🗑️ Limpar", command=clear,
39
+ font=('Segoe UI', 10), fg_color='#e74c3c',
40
+ text_color='white', corner_radius=6).pack(side=tk.LEFT, padx=5)
41
+
42
+ ctk.CTkButton(btn_frame, text="📋 Copiar MD", command=export_md,
43
+ font=('Segoe UI', 10), fg_color='#27ae60',
44
+ text_color='white', corner_radius=6).pack(side=tk.LEFT, padx=5)
45
+
46
+ # Main component
47
+ content_frame = ctk.CTkFrame(root, fg_color='#f5f5f5', corner_radius=8)
48
+ content_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
49
+
50
+ renderer = CTkMarkdown(content_frame)
51
+ renderer._insert_sample()
52
+ renderer.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
53
+
54
+ root.mainloop()
55
+
56
+
57
+ if __name__ == "__main__":
58
+ main()