Skip to content

Commit 2d18982

Browse files
committed
feat: add Jinja tester PySide6 simple app
1 parent 3de0082 commit 2d18982

File tree

2 files changed

+230
-0
lines changed

2 files changed

+230
-0
lines changed

scripts/jinja/jinja-tester.py

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import sys
2+
import json
3+
import traceback
4+
from PySide6.QtWidgets import (
5+
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
6+
QLabel, QPlainTextEdit, QTextEdit, QPushButton
7+
)
8+
from PySide6.QtGui import QColor, QTextCursor, QTextFormat
9+
from PySide6.QtCore import Qt, QRect, QSize
10+
from jinja2 import Environment, TemplateSyntaxError, TemplateError
11+
12+
13+
# ------------------------
14+
# Line Number Widget
15+
# ------------------------
16+
class LineNumberArea(QWidget):
17+
def __init__(self, editor):
18+
super().__init__(editor)
19+
self.code_editor = editor
20+
21+
def sizeHint(self):
22+
return QSize(self.code_editor.line_number_area_width(), 0)
23+
24+
def paintEvent(self, event):
25+
self.code_editor.line_number_area_paint_event(event)
26+
27+
28+
class CodeEditor(QPlainTextEdit):
29+
def __init__(self):
30+
super().__init__()
31+
self.line_number_area = LineNumberArea(self)
32+
33+
self.blockCountChanged.connect(self.update_line_number_area_width)
34+
self.updateRequest.connect(self.update_line_number_area)
35+
self.cursorPositionChanged.connect(self.highlight_current_line)
36+
37+
self.update_line_number_area_width(0)
38+
self.highlight_current_line()
39+
40+
def line_number_area_width(self):
41+
digits = len(str(self.blockCount()))
42+
space = 3 + self.fontMetrics().horizontalAdvance("9") * digits
43+
return space
44+
45+
def update_line_number_area_width(self, _):
46+
self.setViewportMargins(self.line_number_area_width(), 0, 0, 0)
47+
48+
def update_line_number_area(self, rect, dy):
49+
if dy:
50+
self.line_number_area.scroll(0, dy)
51+
else:
52+
self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height())
53+
54+
if rect.contains(self.viewport().rect()):
55+
self.update_line_number_area_width(0)
56+
57+
def resizeEvent(self, event):
58+
super().resizeEvent(event)
59+
cr = self.contentsRect()
60+
self.line_number_area.setGeometry(QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height()))
61+
62+
def line_number_area_paint_event(self, event):
63+
from PySide6.QtGui import QPainter
64+
painter = QPainter(self.line_number_area)
65+
painter.fillRect(event.rect(), Qt.lightGray)
66+
67+
block = self.firstVisibleBlock()
68+
block_number = block.blockNumber()
69+
top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
70+
bottom = top + int(self.blockBoundingRect(block).height())
71+
72+
while block.isValid() and top <= event.rect().bottom():
73+
if block.isVisible() and bottom >= event.rect().top():
74+
number = str(block_number + 1)
75+
painter.setPen(Qt.black)
76+
painter.drawText(0, top, self.line_number_area.width() - 2,
77+
self.fontMetrics().height(),
78+
Qt.AlignRight, number)
79+
block = block.next()
80+
top = bottom
81+
bottom = top + int(self.blockBoundingRect(block).height())
82+
block_number += 1
83+
84+
def highlight_current_line(self):
85+
extra_selections = []
86+
if not self.isReadOnly():
87+
selection = QTextEdit.ExtraSelection()
88+
line_color = QColor(Qt.yellow).lighter(160)
89+
selection.format.setBackground(line_color)
90+
selection.format.setProperty(QTextFormat.FullWidthSelection, True)
91+
selection.cursor = self.textCursor()
92+
selection.cursor.clearSelection()
93+
extra_selections.append(selection)
94+
self.setExtraSelections(extra_selections)
95+
96+
def highlight_position(self, lineno: int, col: int, color: QColor):
97+
block = self.document().findBlockByLineNumber(lineno - 1)
98+
if block.isValid():
99+
cursor = QTextCursor(block)
100+
text = block.text()
101+
start = block.position() + max(0, col - 1)
102+
cursor.setPosition(start)
103+
if col <= len(text):
104+
cursor.movePosition(QTextCursor.NextCharacter, QTextCursor.KeepAnchor)
105+
106+
extra = QTextEdit.ExtraSelection()
107+
extra.format.setBackground(color.lighter(160))
108+
extra.cursor = cursor
109+
110+
self.setExtraSelections(self.extraSelections() + [extra])
111+
112+
def highlight_line(self, lineno: int, color: QColor):
113+
block = self.document().findBlockByLineNumber(lineno - 1)
114+
if block.isValid():
115+
cursor = QTextCursor(block)
116+
cursor.select(QTextCursor.LineUnderCursor)
117+
118+
extra = QTextEdit.ExtraSelection()
119+
extra.format.setBackground(color.lighter(160))
120+
extra.cursor = cursor
121+
122+
self.setExtraSelections(self.extraSelections() + [extra])
123+
124+
def clear_highlighting(self):
125+
self.highlight_current_line()
126+
127+
128+
# ------------------------
129+
# Main App
130+
# ------------------------
131+
class JinjaTester(QMainWindow):
132+
def __init__(self):
133+
super().__init__()
134+
self.setWindowTitle("Jinja Template Tester")
135+
self.resize(1200, 800)
136+
137+
central = QWidget()
138+
main_layout = QVBoxLayout(central)
139+
140+
# -------- Top input area --------
141+
input_layout = QHBoxLayout()
142+
143+
# Template editor with label
144+
template_layout = QVBoxLayout()
145+
template_label = QLabel("Jinja2 Template")
146+
template_layout.addWidget(template_label)
147+
self.template_edit = CodeEditor()
148+
template_layout.addWidget(self.template_edit)
149+
input_layout.addLayout(template_layout)
150+
151+
# JSON editor with label
152+
json_layout = QVBoxLayout()
153+
json_label = QLabel("Context (JSON)")
154+
json_layout.addWidget(json_label)
155+
self.json_edit = CodeEditor()
156+
json_layout.addWidget(self.json_edit)
157+
input_layout.addLayout(json_layout)
158+
159+
main_layout.addLayout(input_layout)
160+
161+
# -------- Rendered output area --------
162+
output_label = QLabel("Rendered Output")
163+
main_layout.addWidget(output_label)
164+
self.output_edit = QPlainTextEdit()
165+
self.output_edit.setReadOnly(True)
166+
main_layout.addWidget(self.output_edit)
167+
168+
# -------- Render button and status --------
169+
btn_layout = QHBoxLayout()
170+
self.render_btn = QPushButton("Render")
171+
self.render_btn.clicked.connect(self.render_template)
172+
btn_layout.addWidget(self.render_btn)
173+
self.status_label = QLabel("Ready")
174+
btn_layout.addWidget(self.status_label)
175+
main_layout.addLayout(btn_layout)
176+
177+
self.setCentralWidget(central)
178+
179+
def render_template(self):
180+
self.template_edit.clear_highlighting()
181+
self.output_edit.clear()
182+
183+
template_str = self.template_edit.toPlainText()
184+
json_str = self.json_edit.toPlainText()
185+
186+
# Parse JSON context
187+
try:
188+
context = json.loads(json_str) if json_str.strip() else {}
189+
except Exception as e:
190+
self.status_label.setText(f"❌ JSON Error: {e}")
191+
return
192+
193+
env = Environment()
194+
try:
195+
template = env.from_string(template_str)
196+
output = template.render(context)
197+
self.output_edit.setPlainText(output)
198+
self.status_label.setText("✅ Render successful")
199+
except TemplateSyntaxError as e:
200+
self.status_label.setText(f"❌ Syntax Error (line {e.lineno}): {e.message}")
201+
if e.lineno:
202+
self.template_edit.highlight_line(e.lineno, QColor("red"))
203+
except Exception as e:
204+
# Catch all runtime errors
205+
# Try to extract template line number
206+
lineno = None
207+
tb = e.__traceback__
208+
while tb:
209+
frame = tb.tb_frame
210+
if frame.f_code.co_filename == "<template>":
211+
lineno = tb.tb_lineno
212+
break
213+
tb = tb.tb_next
214+
215+
error_msg = f"Runtime Error: {type(e).__name__}: {e}"
216+
if lineno:
217+
error_msg = f"Runtime Error at line {lineno} in template: {type(e).__name__}: {e}"
218+
self.template_edit.highlight_line(lineno, QColor("orange"))
219+
220+
self.output_edit.setPlainText(error_msg)
221+
self.status_label.setText(f"❌ {error_msg}")
222+
223+
224+
if __name__ == "__main__":
225+
app = QApplication(sys.argv)
226+
window = JinjaTester()
227+
window.show()
228+
sys.exit(app.exec())

scripts/jinja/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
PySide6
2+
jinja2

0 commit comments

Comments
 (0)