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.
- ctk_markdown-0.1.0/.github/workflows/python-publish.yml +70 -0
- ctk_markdown-0.1.0/.gitignore +4 -0
- ctk_markdown-0.1.0/LICENSE +21 -0
- ctk_markdown-0.1.0/PKG-INFO +80 -0
- ctk_markdown-0.1.0/README.md +65 -0
- ctk_markdown-0.1.0/pyproject.toml +25 -0
- ctk_markdown-0.1.0/src/ctk_markdown/__init__.py +3 -0
- ctk_markdown-0.1.0/src/ctk_markdown/ctk_markdown.py +665 -0
- ctk_markdown-0.1.0/src/example.py +58 -0
|
@@ -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,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
|
+

|
|
19
|
+

|
|
20
|
+

|
|
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
|
+

|
|
4
|
+

|
|
5
|
+

|
|
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,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()
|