1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
from __future__ import annotations
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
QLabel, QLineEdit, QComboBox, QPushButton,
QCompleter, QWidget, QScrollArea,
)
from PyQt6.QtCore import Qt
from pathlib import Path
from core.models import Article, ARTICLE_TYPES
from core.frontmatter import parse_frontmatter, write_frontmatter
from core.taxonomy import load_taxonomy
class MultiTokenCompleter(QCompleter):
"""QCompleter that completes only the last comma-separated token."""
def __init__(self, tags: list[str], parent=None):
super().__init__(tags, parent)
self.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
def attach(self, line_edit):
self.setWidget(line_edit)
line_edit.textEdited.connect(self._on_text_edited)
self.activated[str].connect(self._on_activated)
def _on_text_edited(self, text: str):
token = text.split(",")[-1].lstrip()
if token:
self.setCompletionPrefix(token)
self.complete()
else:
self.popup().hide()
def _on_activated(self, completion: str):
widget = self.widget()
if widget is None:
return
current = widget.text()
if "," in current:
prefix = current.rsplit(",", 1)[0]
new_text = prefix + ", " + completion + ", "
else:
new_text = completion + ", "
widget.setText(new_text)
widget.setCursorPosition(len(new_text))
self.popup().hide()
class FrontmatterEditor(QDialog):
def __init__(self, article: Article, blog_root: Path, parent=None):
super().__init__(parent)
self.setWindowTitle(f"Frontmatter — {article.slug}")
self.setMinimumWidth(520)
self._article = article
self._blog_root = blog_root
self._fields: dict[str, QWidget] = {}
self._build_ui()
def _build_ui(self):
layout = QVBoxLayout(self)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
inner = QWidget()
form = QFormLayout(inner)
scroll.setWidget(inner)
layout.addWidget(scroll)
lang = self._article.lang
tags_it = self._blog_root / "docs" / "tags-it.txt"
tags_en = self._blog_root / "docs" / "tags-en.txt"
taxonomy = load_taxonomy(tags_it, tags_en)
known_tags = list(taxonomy.it_to_en.keys()) if lang == "it" else list(taxonomy.it_to_en.values())
for key, val in self._article.frontmatter.items():
if key == "type":
widget = QComboBox()
widget.addItems(ARTICLE_TYPES)
val_lower = str(val).lower()
if val_lower in ARTICLE_TYPES:
widget.setCurrentText(val_lower)
self._fields[key] = widget
elif key == "tags":
widget = QLineEdit(", ".join(str(v) for v in val) if isinstance(val, list) else str(val))
MultiTokenCompleter(known_tags, widget).attach(widget)
self._fields[key] = widget
elif isinstance(val, list):
widget = QLineEdit(", ".join(str(v) for v in val))
self._fields[key] = widget
else:
widget = QLineEdit(str(val))
self._fields[key] = widget
form.addRow(QLabel(key), widget)
btns = QHBoxLayout()
save_btn = QPushButton("Salva")
save_btn.clicked.connect(self._save)
cancel_btn = QPushButton("Annulla")
cancel_btn.clicked.connect(self.reject)
btns.addStretch()
btns.addWidget(cancel_btn)
btns.addWidget(save_btn)
layout.addLayout(btns)
def _save(self):
updated = dict(self._article.frontmatter)
for key, widget in self._fields.items():
if isinstance(widget, QComboBox):
updated[key] = widget.currentText()
elif isinstance(widget, QLineEdit):
val = widget.text().strip()
if isinstance(self._article.frontmatter.get(key), list):
updated[key] = [t.strip() for t in val.split(",") if t.strip()]
else:
updated[key] = val
try:
_, body = parse_frontmatter(self._article.path)
write_frontmatter(self._article.path, updated, body)
self._article.frontmatter.update(updated)
self.accept()
except Exception as e:
from PyQt6.QtWidgets import QMessageBox
QMessageBox.critical(self, "Errore", str(e))
|