diff --git a/CHANGELOG.md b/CHANGELOG.md
index d94ebeb..01266cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,18 @@
+# 0.8.3
+
+ * Update urllib3 dependency to resolve CVE-2026-21441
+ * Fix carrying over data to next day from over-capturing data belonging to next header section
+ * Other dependency updates
+
+# 0.8.2
+
+ * Add ability to delete an invoice via 'Manage Invoices' dialog
+
+# 0.8.1
+
+ * Fix bold/italic/strikethrough styling in certain conditions when toolbar action is used.
+ * Move a code block or collapsed section (or generally, anything after a checkbox line until the next checkbox line) when moving an unchecked checkbox line to another day.
+
# 0.8.0
* Add .desktop file for Debian
diff --git a/Dockerfile.rpmbuild b/Dockerfile.rpmbuild
new file mode 100644
index 0000000..9013b6e
--- /dev/null
+++ b/Dockerfile.rpmbuild
@@ -0,0 +1,110 @@
+# syntax=docker/dockerfile:1
+FROM fedora:42
+
+# rpmbuild in a container does not auto-install BuildRequires. Since we're
+# building directly in Docker (not mock), we pre-install the common deps that
+# Fedora's pyproject macros will require for Bouquin.
+#
+# NOTE: bouquin also needs python3dist(sqlcipher4) at build time (because
+# %pyproject_buildrequires includes runtime deps). That one is NOT in Fedora;
+# we install it from /deps.
+RUN set -eux; \
+ dnf -y update; \
+ dnf -y install \
+ rpm-build rpmdevtools \
+ redhat-rpm-config \
+ gcc \
+ make \
+ findutils \
+ tar \
+ gzip \
+ rsync \
+ python3 \
+ python3-devel \
+ python3-pip \
+ python3-setuptools \
+ python3-wheel \
+ pyproject-rpm-macros \
+ python3-rpm-macros \
+ python3-poetry-core \
+ desktop-file-utils \
+ python3-requests \
+ python3-markdown \
+ python3-pyside6 \
+ xcb-util-cursor ; \
+ dnf -y clean all
+
+RUN set -eux; cat > /usr/local/bin/build-rpm <<'EOF'
+#!/usr/bin/env bash
+set -euo pipefail
+
+SRC="${SRC:-/src}"
+WORKROOT="${WORKROOT:-/work}"
+OUT="${OUT:-/out}"
+DEPS_DIR="${DEPS_DIR:-/deps}"
+VERSION_ID="$(grep VERSION_ID /etc/os-release | cut -d= -f2)"
+echo "Version ID is ${VERSION_ID}"
+
+# Install bouquin-sqlcipher4 from local rpm
+# Filter out .src.rpm and debug* subpackages if present.
+if [ -d "${DEPS_DIR}" ] && compgen -G "${DEPS_DIR}/*.rpm" > /dev/null; then
+ mapfile -t rpms < <(ls -1 "${DEPS_DIR}"/*.rpm | grep -vE '(\.src\.rpm$|-(debuginfo|debugsource)-)' | grep "${VERSION_ID}")
+ if [ "${#rpms[@]}" -gt 0 ]; then
+ echo "Installing dependency RPMs from ${DEPS_DIR}:"
+ printf ' - %s\n' "${rpms[@]}"
+ dnf -y install "${rpms[@]}"
+ dnf -y clean all
+ else
+ echo "NOTE: Only src/debug RPMs found in ${DEPS_DIR}; nothing installed." >&2
+ fi
+else
+ echo "NOTE: No RPMs found in ${DEPS_DIR}. If the build fails with missing python3dist(sqlcipher4)," >&2
+ echo " mount your bouquin-sqlcipher4 RPM directory as -v
:/deps" >&2
+fi
+
+mkdir -p "${WORKROOT}" "${OUT}"
+WORK="${WORKROOT}/src"
+rm -rf "${WORK}"
+mkdir -p "${WORK}"
+
+rsync -a --delete \
+ --exclude '.git' \
+ --exclude '.venv' \
+ --exclude 'dist' \
+ --exclude 'build' \
+ --exclude '__pycache__' \
+ --exclude '.pytest_cache' \
+ --exclude '.mypy_cache' \
+ "${SRC}/" "${WORK}/"
+
+cd "${WORK}"
+
+# Determine version from pyproject.toml unless provided
+if [ -n "${VERSION:-}" ]; then
+ ver="${VERSION}"
+else
+ ver="$(grep -m1 '^version = ' pyproject.toml | sed -E 's/version = "([^"]+)".*/\1/')"
+fi
+
+TOPDIR="${WORKROOT}/rpmbuild"
+mkdir -p "${TOPDIR}"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
+
+tarball="${TOPDIR}/SOURCES/bouquin-${ver}.tar.gz"
+tar -czf "${tarball}" --transform "s#^#bouquin/#" .
+
+cp -v "rpm/bouquin.spec" "${TOPDIR}/SPECS/bouquin.spec"
+
+rpmbuild -ba "${TOPDIR}/SPECS/bouquin.spec" \
+ --define "_topdir ${TOPDIR}" \
+ --define "upstream_version ${ver}"
+
+shopt -s nullglob
+cp -v "${TOPDIR}"/RPMS/*/*.rpm "${OUT}/" || true
+cp -v "${TOPDIR}"/SRPMS/*.src.rpm "${OUT}/" || true
+echo "Artifacts copied to ${OUT}"
+EOF
+
+RUN chmod +x /usr/local/bin/build-rpm
+
+WORKDIR /work
+ENTRYPOINT ["/usr/local/bin/build-rpm"]
diff --git a/README.md b/README.md
index 4a52ef4..30f7ce1 100644
--- a/README.md
+++ b/README.md
@@ -101,6 +101,25 @@ sudo apt update
sudo apt install bouquin
```
+### Fedora 42
+
+```bash
+sudo rpm --import https://mig5.net/static/mig5.asc
+
+sudo tee /etc/yum.repos.d/mig5.repo > /dev/null << 'EOF'
+[mig5]
+name=mig5 Repository
+baseurl=https://rpm.mig5.net/$releasever/rpm/$basearch
+enabled=1
+gpgcheck=1
+repo_gpgcheck=1
+gpgkey=https://mig5.net/static/mig5.asc
+EOF
+
+sudo dnf upgrade --refresh
+sudo dnf install bouquin
+```
+
### From PyPi/pip
* `pip install bouquin`
diff --git a/bouquin/db.py b/bouquin/db.py
index d1c6a69..157aae8 100644
--- a/bouquin/db.py
+++ b/bouquin/db.py
@@ -2392,6 +2392,18 @@ class DBManager:
(document_id, invoice_id),
)
+ def delete_invoice(self, invoice_id: int) -> None:
+ """Delete an invoice.
+
+ Related invoice line items and invoice ↔ time log links are removed via
+ ON DELETE CASCADE.
+ """
+ with self.conn:
+ self.conn.execute(
+ "DELETE FROM invoices WHERE id = ?",
+ (invoice_id,),
+ )
+
def time_logs_for_range(
self,
project_id: int,
diff --git a/bouquin/invoices.py b/bouquin/invoices.py
index a0b50cb..fde6a92 100644
--- a/bouquin/invoices.py
+++ b/bouquin/invoices.py
@@ -1065,6 +1065,10 @@ class InvoicesDialog(QDialog):
btn_row = QHBoxLayout()
btn_row.addStretch(1)
+ delete_btn = QPushButton(strings._("delete"))
+ delete_btn.clicked.connect(self._on_delete_clicked)
+ btn_row.addWidget(delete_btn)
+
close_btn = QPushButton(strings._("close"))
close_btn.clicked.connect(self.accept)
btn_row.addWidget(close_btn)
@@ -1073,6 +1077,68 @@ class InvoicesDialog(QDialog):
self._reload_invoices()
+ # ----------------------------------------------------------------- deletion
+
+ def _on_delete_clicked(self) -> None:
+ """Delete the currently selected invoice."""
+ row = self.table.currentRow()
+ if row < 0:
+ sel = self.table.selectionModel().selectedRows()
+ if sel:
+ row = sel[0].row()
+ if row < 0:
+ QMessageBox.information(
+ self,
+ strings._("delete"),
+ strings._("invoice_required"),
+ )
+ return
+
+ base_item = self.table.item(row, self.COL_NUMBER)
+ if base_item is None:
+ return
+
+ inv_id = base_item.data(Qt.ItemDataRole.UserRole)
+ if not inv_id:
+ return
+
+ invoice_number = (base_item.text() or "").strip() or "?"
+ proj_item = self.table.item(row, self.COL_PROJECT)
+ project_name = (proj_item.text() if proj_item is not None else "").strip()
+
+ label = strings._("delete")
+ prompt = (
+ f"{label} '{invoice_number}'"
+ + (f" ({project_name})" if project_name else "")
+ + "?"
+ )
+
+ resp = QMessageBox.question(
+ self,
+ label,
+ prompt,
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
+ QMessageBox.StandardButton.No,
+ )
+ if resp != QMessageBox.StandardButton.Yes:
+ return
+
+ # Remove any automatically created due-date reminder.
+ if self.cfg.reminders:
+ self._remove_invoice_due_reminder(row, int(inv_id))
+
+ try:
+ self._db.delete_invoice(int(inv_id))
+ except Exception as e:
+ QMessageBox.warning(
+ self,
+ strings._("error"),
+ f"Failed to delete invoice: {e}",
+ )
+ return
+
+ self._reload_invoices()
+
# ------------------------------------------------------------------ helpers
def _reload_projects(self) -> None:
diff --git a/bouquin/locales/en.json b/bouquin/locales/en.json
index 26a4d5c..f1f86dd 100644
--- a/bouquin/locales/en.json
+++ b/bouquin/locales/en.json
@@ -436,5 +436,6 @@
"invoice_invalid_date_format": "Invalid date format",
"invoice_invalid_tax_rate": "The tax rate is invalid",
"invoice_no_items": "There are no items in the invoice",
- "invoice_number_required": "An invoice number is required"
+ "invoice_number_required": "An invoice number is required",
+ "invoice_required": "Please select a specific invoice before trying to delete an invoice."
}
diff --git a/bouquin/locales/fr.json b/bouquin/locales/fr.json
index d82890d..87a6a16 100644
--- a/bouquin/locales/fr.json
+++ b/bouquin/locales/fr.json
@@ -432,5 +432,6 @@
"invoice_invalid_date_format": "Format de date invalide",
"invoice_invalid_tax_rate": "Le taux de TVA est invalide",
"invoice_no_items": "La facture ne contient aucun article",
- "invoice_number_required": "Un numéro de facture est requis"
+ "invoice_number_required": "Un numéro de facture est requis",
+ "invoice_required": "Veuillez sélectionner une facture spécifique avant d'essayer de supprimer la facture."
}
diff --git a/bouquin/main_window.py b/bouquin/main_window.py
index 9b812b4..2759272 100644
--- a/bouquin/main_window.py
+++ b/bouquin/main_window.py
@@ -28,7 +28,6 @@ from PySide6.QtGui import (
QGuiApplication,
QKeySequence,
QTextCursor,
- QTextListFormat,
)
from PySide6.QtWidgets import (
QApplication,
@@ -871,6 +870,13 @@ class MainWindow(QMainWindow):
into the rollover target date (today, or next Monday if today
is a weekend).
+ In addition to moving the unchecked checkbox *line* itself, this also
+ moves any subsequent lines that belong to that unchecked item, stopping
+ at the next *checked* checkbox line **or** the next markdown heading.
+
+ This allows code fences, collapsed blocks, and notes under a todo to
+ travel with it without accidentally pulling in the next section.
+
Returns True if any items were moved, False otherwise.
"""
if not getattr(self.cfg, "move_todos", False):
@@ -885,7 +891,9 @@ class MainWindow(QMainWindow):
# Regexes for markdown headings and checkboxes
heading_re = re.compile(r"^\s{0,3}(#+)\s+(.*)$")
- unchecked_re = re.compile(r"^\s*-\s*\[[\s☐]\]\s+")
+ unchecked_re = re.compile(r"^(\s*)-\s*\[[\s☐]\]\s+(.*)$")
+ checked_re = re.compile(r"^(\s*)-\s*\[[xX☑]\]\s+(.*)$")
+ fence_re = re.compile(r"^\s*(`{3,}|~{3,})")
def _normalize_heading(text: str) -> str:
"""
@@ -896,13 +904,47 @@ class MainWindow(QMainWindow):
text = re.sub(r"\s+#+\s*$", "", text)
return text.strip()
- def _insert_todos_under_heading(
+ def _update_fence_state(
+ line: str, in_fence: bool, fence_marker: str | None
+ ) -> tuple[bool, str | None]:
+ """
+ Track fenced code blocks (``` / ~~~). We ignore checkbox markers inside
+ fences so we don't accidentally split/move based on "- [x]" that appears
+ in code.
+ """
+ m = fence_re.match(line)
+ if not m:
+ return in_fence, fence_marker
+
+ marker = m.group(1)
+ if not in_fence:
+ return True, marker
+
+ # Close only when we see a fence of the same char and >= length
+ if (
+ fence_marker
+ and marker[0] == fence_marker[0]
+ and len(marker) >= len(fence_marker)
+ ):
+ return False, None
+
+ return in_fence, fence_marker
+
+ def _is_list_item(line: str) -> bool:
+ s = line.lstrip()
+ return bool(
+ re.match(r"^([-*+]\s+|\d+\.\s+)", s)
+ or unchecked_re.match(line)
+ or checked_re.match(line)
+ )
+
+ def _insert_blocks_under_heading(
target_lines: list[str],
heading_level: int,
heading_text: str,
- todos: list[str],
+ blocks: list[list[str]],
) -> list[str]:
- """Ensure a heading exists and append todos to the end of its section."""
+ """Ensure a heading exists and append blocks to the end of its section."""
normalized = _normalize_heading(heading_text)
# 1) Find existing heading with same text (any level)
@@ -942,15 +984,137 @@ class MainWindow(QMainWindow):
):
insert_at -= 1
- for todo in todos:
- target_lines.insert(insert_at, todo)
- insert_at += 1
+ # Insert blocks (preserve internal blank lines)
+ for block in blocks:
+ if not block:
+ continue
+
+ # Avoid gluing a paragraph to the new block unless both look like list items
+ if (
+ insert_at > start_idx + 1
+ and target_lines[insert_at - 1].strip() != ""
+ and block[0].strip() != ""
+ and not (
+ _is_list_item(target_lines[insert_at - 1])
+ and _is_list_item(block[0])
+ )
+ ):
+ target_lines.insert(insert_at, "")
+ insert_at += 1
+
+ for line in block:
+ target_lines.insert(insert_at, line)
+ insert_at += 1
return target_lines
- # Collect moved todos as (heading_info, item_text)
+ def _prune_empty_headings(src_lines: list[str]) -> list[str]:
+ """Remove markdown headings whose section became empty.
+
+ The rollover logic removes unchecked todo *blocks* but intentionally keeps
+ headings on the source day so we can re-create the same section on the
+ target day. If a heading ends up with no remaining content (including
+ empty subheadings), we should remove it from the source day too.
+
+ Headings inside fenced code blocks are ignored.
+ """
+
+ # Identify headings (outside fences) and their levels
+ heading_levels: dict[int, int] = {}
+ heading_indices: list[int] = []
+
+ in_f = False
+ f_mark: str | None = None
+ for idx, ln in enumerate(src_lines):
+ if not in_f:
+ m = heading_re.match(ln)
+ if m:
+ heading_indices.append(idx)
+ heading_levels[idx] = len(m.group(1))
+ in_f, f_mark = _update_fence_state(ln, in_f, f_mark)
+
+ if not heading_indices:
+ return src_lines
+
+ # Compute each heading's section boundary: next heading with level <= current
+ boundary: dict[int, int] = {}
+ stack: list[int] = []
+ for idx in heading_indices:
+ lvl = heading_levels[idx]
+ while stack and lvl <= heading_levels[stack[-1]]:
+ boundary[stack.pop()] = idx
+ stack.append(idx)
+ for idx in stack:
+ boundary[idx] = len(src_lines)
+
+ # Build parent/children relationships based on heading levels
+ children: dict[int, list[int]] = {}
+ parent_stack: list[int] = []
+ for idx in heading_indices:
+ lvl = heading_levels[idx]
+ while parent_stack and lvl <= heading_levels[parent_stack[-1]]:
+ parent_stack.pop()
+ if parent_stack:
+ children.setdefault(parent_stack[-1], []).append(idx)
+ parent_stack.append(idx)
+
+ # Determine whether each heading has any non-heading, non-blank content in its span
+ has_body: dict[int, bool] = {}
+ for h_idx in heading_indices:
+ end = boundary[h_idx]
+ body = False
+ in_f = False
+ f_mark = None
+ for j in range(h_idx + 1, end):
+ ln = src_lines[j]
+ if not in_f:
+ if ln.strip() and not heading_re.match(ln):
+ body = True
+ break
+ in_f, f_mark = _update_fence_state(ln, in_f, f_mark)
+ has_body[h_idx] = body
+
+ # Bottom-up: keep headings that have body content or any kept child headings
+ keep: dict[int, bool] = {}
+ for h_idx in reversed(heading_indices):
+ keep_child = any(keep.get(ch, False) for ch in children.get(h_idx, []))
+ keep[h_idx] = has_body[h_idx] or keep_child
+
+ remove_set = {idx for idx, k in keep.items() if not k}
+ if not remove_set:
+ return src_lines
+
+ # Remove empty headings and any immediate blank lines following them
+ out: list[str] = []
+ i = 0
+ while i < len(src_lines):
+ if i in remove_set:
+ i += 1
+ while i < len(src_lines) and src_lines[i].strip() == "":
+ i += 1
+ continue
+ out.append(src_lines[i])
+ i += 1
+
+ # Normalize excessive blank lines created by removals
+ cleaned: list[str] = []
+ prev_blank = False
+ for ln in out:
+ blank = ln.strip() == ""
+ if blank and prev_blank:
+ continue
+ cleaned.append(ln)
+ prev_blank = blank
+
+ while cleaned and cleaned[0].strip() == "":
+ cleaned.pop(0)
+ while cleaned and cleaned[-1].strip() == "":
+ cleaned.pop()
+ return cleaned
+
+ # Collect moved blocks as (heading_info, block_lines)
# heading_info is either None or (level, heading_text)
- moved_items: list[tuple[tuple[int, str] | None, str]] = []
+ moved_blocks: list[tuple[tuple[int, str] | None, list[str]]] = []
any_moved = False
# Look back N days (yesterday = 1, up to `days_back`)
@@ -966,28 +1130,87 @@ class MainWindow(QMainWindow):
moved_from_this_day = False
current_heading: tuple[int, str] | None = None
- for line in lines:
- # Track the last seen heading (# / ## / ###)
- m_head = heading_re.match(line)
- if m_head:
- level = len(m_head.group(1))
- heading_text = _normalize_heading(m_head.group(2))
- if level <= 3:
- current_heading = (level, heading_text)
- # Keep headings in the original day
- remaining_lines.append(line)
- continue
+ in_fence = False
+ fence_marker: str | None = None
- # Unchecked markdown checkboxes: "- [ ] " or "- [☐] "
- if unchecked_re.match(line):
- item_text = unchecked_re.sub("", line)
- moved_items.append((current_heading, item_text))
- moved_from_this_day = True
- any_moved = True
- else:
- remaining_lines.append(line)
+ i = 0
+ while i < len(lines):
+ line = lines[i]
+
+ # If we're not in a fenced code block, we can interpret headings/checkboxes
+ if not in_fence:
+ # Track the last seen heading (# / ## / ###)
+ m_head = heading_re.match(line)
+ if m_head:
+ level = len(m_head.group(1))
+ heading_text = _normalize_heading(m_head.group(2))
+ if level <= 3:
+ current_heading = (level, heading_text)
+ # Keep headings in the original day (only headings ABOVE a moved block are "carried")
+ remaining_lines.append(line)
+ in_fence, fence_marker = _update_fence_state(
+ line, in_fence, fence_marker
+ )
+ i += 1
+ continue
+
+ # Start of an unchecked checkbox block
+ m_unchecked = unchecked_re.match(line)
+ if m_unchecked:
+ indent = m_unchecked.group(1) or ""
+ item_text = m_unchecked.group(2)
+ block: list[str] = [f"{indent}- [ ] {item_text}"]
+
+ i += 1
+ # Consume subsequent lines until the next *checked* checkbox
+ # (ignoring any "- [x]" that appear inside fenced code blocks)
+ block_in_fence = in_fence
+ block_fence_marker = fence_marker
+
+ while i < len(lines):
+ nxt = lines[i]
+
+ # If we're not inside a fence, a checked checkbox ends the block,
+ # otherwise a new heading does as well.
+ if not block_in_fence and (
+ checked_re.match(nxt) or heading_re.match(nxt)
+ ):
+ break
+
+ # Normalize any unchecked checkbox lines inside the block
+ m_inner_unchecked = (
+ unchecked_re.match(nxt) if not block_in_fence else None
+ )
+ if m_inner_unchecked:
+ inner_indent = m_inner_unchecked.group(1) or ""
+ inner_text = m_inner_unchecked.group(2)
+ block.append(f"{inner_indent}- [ ] {inner_text}")
+ else:
+ block.append(nxt)
+
+ # Update fence state after consuming the line
+ block_in_fence, block_fence_marker = _update_fence_state(
+ nxt, block_in_fence, block_fence_marker
+ )
+ i += 1
+
+ # Carry the last heading *above* the unchecked checkbox
+ moved_blocks.append((current_heading, block))
+ moved_from_this_day = True
+ any_moved = True
+
+ # We consumed the block; keep scanning from the checked checkbox (or EOF)
+ continue
+
+ # Default: keep the line on the original day
+ remaining_lines.append(line)
+ in_fence, fence_marker = _update_fence_state(
+ line, in_fence, fence_marker
+ )
+ i += 1
if moved_from_this_day:
+ remaining_lines = _prune_empty_headings(remaining_lines)
modified_text = "\n".join(remaining_lines)
# Save the cleaned-up source day
self.db.save_new_version(
@@ -999,33 +1222,52 @@ class MainWindow(QMainWindow):
if not any_moved:
return False
- # --- Merge all moved items into the *target* date ---
+ # --- Merge all moved blocks into the *target* date ---
target_text = self.db.get_entry(target_iso) or ""
- target_lines = target_text.split("\n") if target_text else []
+ # Treat a whitespace-only target note as truly empty; otherwise we can
+ # end up appending the new heading *after* leading blank lines (e.g. if
+ # a newly-created empty day was previously saved as just "\n").
+ if not target_text.strip():
+ target_lines = []
+ else:
+ target_lines = target_text.split("\n")
- by_heading: dict[tuple[int, str], list[str]] = {}
- plain_items: list[str] = []
+ by_heading: dict[tuple[int, str], list[list[str]]] = {}
+ plain_blocks: list[list[str]] = []
- for heading_info, item_text in moved_items:
- todo_line = f"- [ ] {item_text}"
+ for heading_info, block in moved_blocks:
if heading_info is None:
- # No heading above this checkbox in the source: behave as before
- plain_items.append(todo_line)
+ plain_blocks.append(block)
else:
- by_heading.setdefault(heading_info, []).append(todo_line)
+ by_heading.setdefault(heading_info, []).append(block)
- # First insert all items that have headings
- for (level, heading_text), todos in by_heading.items():
- target_lines = _insert_todos_under_heading(
- target_lines, level, heading_text, todos
+ # First insert all blocks that have headings
+ for (level, heading_text), blocks in by_heading.items():
+ target_lines = _insert_blocks_under_heading(
+ target_lines, level, heading_text, blocks
)
- # Then append all items without headings at the end, like before
- if plain_items:
+ # Then append all blocks without headings at the end, like before
+ if plain_blocks:
if target_lines and target_lines[-1].strip():
target_lines.append("") # one blank line before the "unsectioned" todos
- target_lines.extend(plain_items)
+ first = True
+ for block in plain_blocks:
+ if not block:
+ continue
+ if (
+ not first
+ and target_lines
+ and target_lines[-1].strip() != ""
+ and block[0].strip() != ""
+ and not (
+ _is_list_item(target_lines[-1]) and _is_list_item(block[0])
+ )
+ ):
+ target_lines.append("")
+ target_lines.extend(block)
+ first = False
new_target_text = "\n".join(target_lines)
if not new_target_text.endswith("\n"):
@@ -1241,46 +1483,58 @@ class MainWindow(QMainWindow):
self._toolbar_bound = True
def _sync_toolbar(self):
- fmt = self.editor.currentCharFormat()
+ """
+ Keep the toolbar "sticky" by reflecting the markdown state at the current caret/selection.
+ """
c = self.editor.textCursor()
+ line = c.block().text()
+
+ # Inline styles (markdown-aware)
+ bold_on = bool(getattr(self.editor, "is_markdown_bold_active", lambda: False)())
+ italic_on = bool(
+ getattr(self.editor, "is_markdown_italic_active", lambda: False)()
+ )
+ strike_on = bool(
+ getattr(self.editor, "is_markdown_strike_active", lambda: False)()
+ )
# Block signals so setChecked() doesn't re-trigger actions
QSignalBlocker(self.toolBar.actBold)
QSignalBlocker(self.toolBar.actItalic)
QSignalBlocker(self.toolBar.actStrike)
- self.toolBar.actBold.setChecked(fmt.fontWeight() == QFont.Weight.Bold)
- self.toolBar.actItalic.setChecked(fmt.fontItalic())
- self.toolBar.actStrike.setChecked(fmt.fontStrikeOut())
+ self.toolBar.actBold.setChecked(bold_on)
+ self.toolBar.actItalic.setChecked(italic_on)
+ self.toolBar.actStrike.setChecked(strike_on)
- # Headings: decide which to check by current point size
- def _approx(a, b, eps=0.5): # small float tolerance
- return abs(float(a) - float(b)) <= eps
-
- cur_size = fmt.fontPointSize() or self.editor.font().pointSizeF()
-
- bH1 = _approx(cur_size, 24)
- bH2 = _approx(cur_size, 18)
- bH3 = _approx(cur_size, 14)
+ # Headings: infer from leading markdown markers
+ heading_level = 0
+ m = re.match(r"^\s*(#{1,3})\s+", line)
+ if m:
+ heading_level = len(m.group(1))
QSignalBlocker(self.toolBar.actH1)
QSignalBlocker(self.toolBar.actH2)
QSignalBlocker(self.toolBar.actH3)
QSignalBlocker(self.toolBar.actNormal)
- self.toolBar.actH1.setChecked(bH1)
- self.toolBar.actH2.setChecked(bH2)
- self.toolBar.actH3.setChecked(bH3)
- self.toolBar.actNormal.setChecked(not (bH1 or bH2 or bH3))
+ self.toolBar.actH1.setChecked(heading_level == 1)
+ self.toolBar.actH2.setChecked(heading_level == 2)
+ self.toolBar.actH3.setChecked(heading_level == 3)
+ self.toolBar.actNormal.setChecked(heading_level == 0)
+
+ # Lists: infer from leading markers on the current line
+ bullets_on = bool(re.match(r"^\s*(?:•|-|\*)\s+", line))
+ numbers_on = bool(re.match(r"^\s*\d+\.\s+", line))
+ checkboxes_on = bool(re.match(r"^\s*[☐☑]\s+", line))
- # Lists
- lst = c.currentList()
- bullets_on = lst and lst.format().style() == QTextListFormat.Style.ListDisc
- numbers_on = lst and lst.format().style() == QTextListFormat.Style.ListDecimal
QSignalBlocker(self.toolBar.actBullets)
QSignalBlocker(self.toolBar.actNumbers)
- self.toolBar.actBullets.setChecked(bool(bullets_on))
- self.toolBar.actNumbers.setChecked(bool(numbers_on))
+ QSignalBlocker(self.toolBar.actCheckboxes)
+
+ self.toolBar.actBullets.setChecked(bullets_on)
+ self.toolBar.actNumbers.setChecked(numbers_on)
+ self.toolBar.actCheckboxes.setChecked(checkboxes_on)
def _change_font_size(self, delta: int) -> None:
"""Change font size for all editor tabs and save the setting."""
diff --git a/bouquin/markdown_editor.py b/bouquin/markdown_editor.py
index 849f515..b1a6d66 100644
--- a/bouquin/markdown_editor.py
+++ b/bouquin/markdown_editor.py
@@ -46,7 +46,7 @@ class MarkdownEditor(QTextEdit):
_COLLAPSE_LABEL_COLLAPSE = "collapse"
_COLLAPSE_LABEL_EXPAND = "expand"
_COLLAPSE_END_MARKER = ""
- # Accept either "collapse" or "expand" in the header text (older files used only "collapse")
+ # Accept either "collapse" or "expand" in the header text
_COLLAPSE_HEADER_RE = re.compile(r"^([ \t]*)([▸▾])\s+(?:collapse|expand)\s*$")
_COLLAPSE_END_RE = re.compile(r"^([ \t]*)\s*$")
@@ -114,6 +114,9 @@ class MarkdownEditor(QTextEdit):
# Track if we're currently updating text programmatically
self._updating = False
+ # Track pending inline marker insertion (e.g. Italic with no selection)
+ self._pending_inline_marker: str | None = None
+
# Help avoid double-click selecting of checkbox
self._suppress_next_checkbox_double_click = False
@@ -928,6 +931,69 @@ class MarkdownEditor(QTextEdit):
return None
+ def _maybe_skip_over_marker_run(self, key: Qt.Key) -> bool:
+ """Skip over common markdown marker runs when navigating with Left/Right.
+
+ This prevents the caret from landing *inside* runs like '**', '***', '__', '___' or '~~',
+ which can cause temporary toolbar-state flicker and makes navigation feel like it takes
+ "two presses" to get past closing markers.
+
+ Hold any modifier key (Shift/Ctrl/Alt/Meta) to disable this behavior.
+ """
+ c = self.textCursor()
+ if c.hasSelection():
+ return False
+
+ p = c.position()
+ doc_max = self._doc_max_pos()
+
+ # Right: run starts at the caret
+ if key == Qt.Key.Key_Right:
+ if p >= doc_max:
+ return False
+ ch = self._text_range(p, p + 1)
+ if ch not in ("*", "_", "~"):
+ return False
+
+ run = 0
+ while p + run < doc_max and self._text_range(p + run, p + run + 1) == ch:
+ run += 1
+
+ # Only skip multi-char runs (bold/strong/emphasis runs or strike)
+ if ch in ("*", "_") and run >= 2:
+ c.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor, run)
+ self.setTextCursor(c)
+ return True
+ if ch == "~" and run == 2:
+ c.movePosition(QTextCursor.Right, QTextCursor.MoveAnchor, 2)
+ self.setTextCursor(c)
+ return True
+
+ return False
+
+ # Left: run ends at the caret
+ if key == Qt.Key.Key_Left:
+ if p <= 0:
+ return False
+ ch = self._text_range(p - 1, p)
+ if ch not in ("*", "_", "~"):
+ return False
+
+ run = 0
+ while p - 1 - run >= 0 and self._text_range(p - 1 - run, p - run) == ch:
+ run += 1
+
+ if ch in ("*", "_") and run >= 2:
+ c.movePosition(QTextCursor.Left, QTextCursor.MoveAnchor, run)
+ self.setTextCursor(c)
+ return True
+ if ch == "~" and run == 2:
+ c.movePosition(QTextCursor.Left, QTextCursor.MoveAnchor, 2)
+ self.setTextCursor(c)
+ return True
+
+ return False
+
def keyPressEvent(self, event):
"""Handle special key events for markdown editing."""
c = self.textCursor()
@@ -936,7 +1002,6 @@ class MarkdownEditor(QTextEdit):
in_code = self._is_inside_code_block(block)
is_fence_line = block.text().strip().startswith("```")
- # --- NEW: 3rd backtick shortcut → open code block dialog ---
# Only when we're *not* already in a code block or on a fence line.
if event.text() == "`" and not (in_code or is_fence_line):
line = block.text()
@@ -1002,6 +1067,14 @@ class MarkdownEditor(QTextEdit):
super().keyPressEvent(event)
return
+ if (
+ event.key() in (Qt.Key.Key_Left, Qt.Key.Key_Right)
+ and event.modifiers() == Qt.KeyboardModifier.NoModifier
+ and not self.textCursor().hasSelection()
+ ):
+ if self._maybe_skip_over_marker_run(event.key()):
+ return
+
# --- Step out of a code block with Down at EOF ---
if event.key() == Qt.Key.Key_Down:
c = self.textCursor()
@@ -1509,19 +1582,389 @@ class MarkdownEditor(QTextEdit):
# ------------------------ Toolbar action handlers ------------------------
+ # ------------------------ Inline markdown helpers ------------------------
+
+ def _doc_max_pos(self) -> int:
+ # QTextDocument includes a trailing null character; cursor positions stop before it.
+ doc = self.document()
+ return max(0, doc.characterCount() - 1)
+
+ def _text_range(self, start: int, end: int) -> str:
+ """Return document text between [start, end) using QTextCursor indexing."""
+ doc_max = self._doc_max_pos()
+ start = max(0, min(start, doc_max))
+ end = max(0, min(end, doc_max))
+ if end < start:
+ start, end = end, start
+ tc = QTextCursor(self.document())
+ tc.setPosition(start)
+ tc.setPosition(end, QTextCursor.KeepAnchor)
+ return tc.selectedText()
+
+ def _selection_wrapped_by(
+ self,
+ markers: tuple[str, ...],
+ *,
+ require_singletons: bool = False,
+ ) -> str | None:
+ """
+ If the current selection is wrapped by any marker in `markers`, return the marker.
+
+ Supports both cases:
+ 1) the selection itself includes the markers, e.g. "**bold**"
+ 2) the selection is the inner text, with markers immediately adjacent in the doc.
+ """
+ c = self.textCursor()
+ if not c.hasSelection():
+ return None
+
+ sel = c.selectedText()
+ start = c.selectionStart()
+ end = c.selectionEnd()
+ doc_max = self._doc_max_pos()
+
+ # Case 1: selection includes markers
+ for m in markers:
+ lm = len(m)
+ if len(sel) >= 2 * lm and sel.startswith(m) and sel.endswith(m):
+ return m
+
+ # Case 2: markers adjacent to selection
+ for m in markers:
+ lm = len(m)
+ if start < lm or end + lm > doc_max:
+ continue
+ before = self._text_range(start - lm, start)
+ after = self._text_range(end, end + lm)
+ if before != m or after != m:
+ continue
+
+ if require_singletons and lm == 1:
+ # Ensure the single marker isn't part of a double/triple (e.g. "**" or "__")
+ ch = m
+ left_marker_pos = start - 1
+ right_marker_pos = end
+
+ if (
+ left_marker_pos - 1 >= 0
+ and self._text_range(left_marker_pos - 1, left_marker_pos) == ch
+ ):
+ continue
+ if (
+ right_marker_pos + 1 <= doc_max
+ and self._text_range(right_marker_pos + 1, right_marker_pos + 2)
+ == ch
+ ):
+ continue
+
+ return m
+
+ return None
+
+ def _caret_between_markers(
+ self, marker: str, *, require_singletons: bool = False
+ ) -> bool:
+ """True if the caret is exactly between an opening and closing marker (e.g. **|**)."""
+ c = self.textCursor()
+ if c.hasSelection():
+ return False
+
+ p = c.position()
+ lm = len(marker)
+ doc_max = self._doc_max_pos()
+ if p < lm or p + lm > doc_max:
+ return False
+
+ before = self._text_range(p - lm, p)
+ after = self._text_range(p, p + lm)
+ if before != marker or after != marker:
+ return False
+
+ if require_singletons and lm == 1:
+ # Disallow if either side is adjacent to the same char (part of "**", "__", "***", etc.)
+ ch = marker
+ if p - 2 >= 0 and self._text_range(p - 2, p - 1) == ch:
+ return False
+ if p + 1 <= doc_max and self._text_range(p + 1, p + 2) == ch:
+ return False
+
+ return True
+
+ def _caret_before_marker(
+ self, marker: str, *, require_singletons: bool = False
+ ) -> bool:
+ """True if the caret is immediately before `marker` (e.g. |**)."""
+ c = self.textCursor()
+ if c.hasSelection():
+ return False
+
+ p = c.position()
+ lm = len(marker)
+ doc_max = self._doc_max_pos()
+ if p + lm > doc_max:
+ return False
+
+ after = self._text_range(p, p + lm)
+ if after != marker:
+ return False
+
+ if require_singletons and lm == 1:
+ # Disallow if it's part of a run like "**" or "___".
+ ch = marker
+ if p - 1 >= 0 and self._text_range(p - 1, p) == ch:
+ return False
+ if p + 1 <= doc_max and self._text_range(p + 1, p + 2) == ch:
+ return False
+
+ return True
+
+ def _unwrap_selection(
+ self, marker: str, *, replacement_marker: str | None = None
+ ) -> bool:
+ """
+ Remove `marker` wrapping from the selection.
+ If replacement_marker is provided, replace marker with that (e.g. ***text*** -> *text*).
+ """
+ c = self.textCursor()
+ if not c.hasSelection():
+ return False
+
+ sel = c.selectedText()
+ start = c.selectionStart()
+ end = c.selectionEnd()
+ lm = len(marker)
+ doc_max = self._doc_max_pos()
+
+ def _select_inner(
+ edit_cursor: QTextCursor, inner_start: int, inner_len: int
+ ) -> None:
+ edit_cursor.setPosition(inner_start)
+ edit_cursor.setPosition(inner_start + inner_len, QTextCursor.KeepAnchor)
+ self.setTextCursor(edit_cursor)
+
+ # Case 1: selection includes markers
+ if len(sel) >= 2 * lm and sel.startswith(marker) and sel.endswith(marker):
+ inner = sel[lm:-lm]
+ new_text = (
+ f"{replacement_marker}{inner}{replacement_marker}"
+ if replacement_marker is not None
+ else inner
+ )
+ c.beginEditBlock()
+ c.insertText(new_text)
+ c.endEditBlock()
+
+ # Re-select the inner content (not the markers)
+ inner_start = c.position() - len(new_text)
+ if replacement_marker is not None:
+ inner_start += len(replacement_marker)
+ _select_inner(c, inner_start, len(inner))
+ return True
+
+ # Case 2: marker is adjacent to selection
+ if start >= lm and end + lm <= doc_max:
+ before = self._text_range(start - lm, start)
+ after = self._text_range(end, end + lm)
+ if before == marker and after == marker:
+ new_text = (
+ f"{replacement_marker}{sel}{replacement_marker}"
+ if replacement_marker is not None
+ else sel
+ )
+ edit = QTextCursor(self.document())
+ edit.beginEditBlock()
+ edit.setPosition(start - lm)
+ edit.setPosition(end + lm, QTextCursor.KeepAnchor)
+ edit.insertText(new_text)
+ edit.endEditBlock()
+
+ inner_start = (start - lm) + (
+ len(replacement_marker) if replacement_marker else 0
+ )
+ _select_inner(edit, inner_start, len(sel))
+ return True
+
+ return False
+
+ def _wrap_selection(self, marker: str) -> None:
+ """Wrap the current selection with `marker` and keep the content selected."""
+ c = self.textCursor()
+ if not c.hasSelection():
+ return
+ sel = c.selectedText()
+ start = c.selectionStart()
+ lm = len(marker)
+
+ c.beginEditBlock()
+ c.insertText(f"{marker}{sel}{marker}")
+ c.endEditBlock()
+
+ # Re-select the original content
+ edit = QTextCursor(self.document())
+ edit.setPosition(start + lm)
+ edit.setPosition(start + lm + len(sel), QTextCursor.KeepAnchor)
+ self.setTextCursor(edit)
+
+ def _pos_inside_inline_span(
+ self,
+ patterns: list[tuple[re.Pattern, int]],
+ start_in_block: int,
+ end_in_block: int,
+ ) -> bool:
+ """True if [start_in_block, end_in_block] lies within the content region of any pattern match."""
+ block_text = self.textCursor().block().text()
+ for pat, mlen in patterns:
+ for m in pat.finditer(block_text):
+ s, e = m.span()
+ cs, ce = s + mlen, e - mlen
+ if cs <= start_in_block and end_in_block <= ce:
+ return True
+ return False
+
+ def is_markdown_bold_active(self) -> bool:
+ c = self.textCursor()
+ bold_markers = ("***", "___", "**", "__")
+
+ if c.hasSelection():
+ if self._selection_wrapped_by(bold_markers) is not None:
+ return True
+ block = c.block()
+ start_in_block = c.selectionStart() - block.position()
+ end_in_block = c.selectionEnd() - block.position()
+ patterns = [
+ (re.compile(r"(? bool:
+ c = self.textCursor()
+ italic_markers = ("*", "_", "***", "___")
+
+ if c.hasSelection():
+ if (
+ self._selection_wrapped_by(italic_markers, require_singletons=True)
+ is not None
+ ):
+ return True
+ block = c.block()
+ start_in_block = c.selectionStart() - block.position()
+ end_in_block = c.selectionEnd() - block.position()
+ patterns = [
+ (re.compile(r"(? bool:
+ c = self.textCursor()
+ if c.hasSelection():
+ if self._selection_wrapped_by(("~~",)) is not None:
+ return True
+ block = c.block()
+ start_in_block = c.selectionStart() - block.position()
+ end_in_block = c.selectionEnd() - block.position()
+ patterns = [(re.compile(r"~~(.+?)~~"), 2)]
+ return self._pos_inside_inline_span(patterns, start_in_block, end_in_block)
+
+ if self._caret_between_markers("~~"):
+ return True
+ block = c.block()
+ pos_in_block = c.position() - block.position()
+ patterns = [(re.compile(r"~~(.+?)~~"), 2)]
+ return self._pos_inside_inline_span(patterns, pos_in_block, pos_in_block)
+
+ # ------------------------ Toolbar action handlers ------------------------
+
def apply_weight(self):
- """Toggle bold formatting."""
+ """Toggle bold formatting (markdown ** / __, and *** / ___)."""
cursor = self.textCursor()
+
if cursor.hasSelection():
- selected = cursor.selectedText()
- # Check if already bold
- if selected.startswith("**") and selected.endswith("**"):
- # Remove bold
- new_text = selected[2:-2]
- else:
- # Add bold
- new_text = f"**{selected}**"
- cursor.insertText(new_text)
+ # If bold+italic, toggling bold should leave italic: ***text*** -> *text*
+ m = self._selection_wrapped_by(("***", "___"))
+ if m is not None:
+ repl = "*" if m == "***" else "_"
+ if self._unwrap_selection(m, replacement_marker=repl):
+ self.setFocus()
+ return
+
+ # Normal bold: **text** / __text__
+ m = self._selection_wrapped_by(("**", "__"))
+ if m is not None:
+ if self._unwrap_selection(m):
+ self.setFocus()
+ return
+
+ # Not bold: wrap selection with **
+ self._wrap_selection("**")
+ self.setFocus()
+ return
+
+ # No selection:
+ # - If we're between an empty pair (**|**), remove them.
+ # - If we're inside bold and sitting right before the closing marker (**text|**),
+ # jump the caret *past* the marker (end-bold) instead of inserting more.
+ # - Otherwise, insert a new empty pair and place the caret between.
+ if self._caret_between_markers("**") or self._caret_between_markers("__"):
+ marker = "**" if self._caret_between_markers("**") else "__"
+ p = cursor.position()
+ lm = len(marker)
+ edit = QTextCursor(self.document())
+ edit.beginEditBlock()
+ edit.setPosition(p - lm)
+ edit.setPosition(p + lm, QTextCursor.KeepAnchor)
+ edit.insertText("")
+ edit.endEditBlock()
+ edit.setPosition(p - lm)
+ self.setTextCursor(edit)
+ elif self.is_markdown_bold_active() and (
+ self._caret_before_marker("**") or self._caret_before_marker("__")
+ ):
+ marker = "**" if self._caret_before_marker("**") else "__"
+ cursor.movePosition(
+ QTextCursor.MoveOperation.Right,
+ QTextCursor.MoveMode.MoveAnchor,
+ len(marker),
+ )
+ self.setTextCursor(cursor)
+ self._pending_inline_marker = None
else:
# No selection - just insert markers
cursor.insertText("****")
@@ -1529,44 +1972,120 @@ class MarkdownEditor(QTextEdit):
QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.MoveAnchor, 2
)
self.setTextCursor(cursor)
+ self._pending_inline_marker = "*"
# Return focus to editor
self.setFocus()
def apply_italic(self):
- """Toggle italic formatting."""
+ """Toggle italic formatting (markdown * / _, and *** / ___)."""
cursor = self.textCursor()
+
if cursor.hasSelection():
- selected = cursor.selectedText()
- if (
- selected.startswith("*")
- and selected.endswith("*")
- and not selected.startswith("**")
- ):
- new_text = selected[1:-1]
- else:
- new_text = f"*{selected}*"
- cursor.insertText(new_text)
+ # If bold+italic, toggling italic should leave bold: ***text*** -> **text**
+ m = self._selection_wrapped_by(("***", "___"))
+ if m is not None:
+ repl = "**" if m == "***" else "__"
+ if self._unwrap_selection(m, replacement_marker=repl):
+ self.setFocus()
+ return
+
+ m = self._selection_wrapped_by(("*", "_"), require_singletons=True)
+ if m is not None:
+ if self._unwrap_selection(m):
+ self.setFocus()
+ return
+
+ self._wrap_selection("*")
+ self.setFocus()
+ return
+
+ # No selection:
+ # - If we're between an empty pair (*|*), remove them.
+ # - If we're inside italic and sitting right before the closing marker (*text|*),
+ # jump the caret past the marker (end-italic) instead of inserting more.
+ # - Otherwise, insert a new empty pair and place the caret between.
+ if self._caret_between_markers(
+ "*", require_singletons=True
+ ) or self._caret_between_markers("_", require_singletons=True):
+ marker = (
+ "*"
+ if self._caret_between_markers("*", require_singletons=True)
+ else "_"
+ )
+ p = cursor.position()
+ lm = len(marker)
+ edit = QTextCursor(self.document())
+ edit.beginEditBlock()
+ edit.setPosition(p - lm)
+ edit.setPosition(p + lm, QTextCursor.KeepAnchor)
+ edit.insertText("")
+ edit.endEditBlock()
+ edit.setPosition(p - lm)
+ self.setTextCursor(edit)
+ self._pending_inline_marker = None
+ elif self.is_markdown_italic_active() and (
+ self._caret_before_marker("*", require_singletons=True)
+ or self._caret_before_marker("_", require_singletons=True)
+ ):
+ marker = (
+ "*" if self._caret_before_marker("*", require_singletons=True) else "_"
+ )
+ cursor.movePosition(
+ QTextCursor.MoveOperation.Right,
+ QTextCursor.MoveMode.MoveAnchor,
+ len(marker),
+ )
+ self.setTextCursor(cursor)
+ self._pending_inline_marker = None
else:
cursor.insertText("**")
cursor.movePosition(
QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.MoveAnchor, 1
)
self.setTextCursor(cursor)
+ self._pending_inline_marker = "*"
# Return focus to editor
self.setFocus()
def apply_strikethrough(self):
- """Toggle strikethrough formatting."""
+ """Toggle strikethrough formatting (markdown ~~)."""
cursor = self.textCursor()
+
if cursor.hasSelection():
- selected = cursor.selectedText()
- if selected.startswith("~~") and selected.endswith("~~"):
- new_text = selected[2:-2]
- else:
- new_text = f"~~{selected}~~"
- cursor.insertText(new_text)
+ m = self._selection_wrapped_by(("~~",))
+ if m is not None:
+ if self._unwrap_selection(m):
+ self.setFocus()
+ return
+ self._wrap_selection("~~")
+ self.setFocus()
+ return
+
+ # No selection:
+ # - If we're between an empty pair (~~|~~), remove them.
+ # - If we're inside strike and sitting right before the closing marker (~~text|~~),
+ # jump the caret past the marker (end-strike) instead of inserting more.
+ # - Otherwise, insert a new empty pair and place the caret between.
+ if self._caret_between_markers("~~"):
+ p = cursor.position()
+ edit = QTextCursor(self.document())
+ edit.beginEditBlock()
+ edit.setPosition(p - 2)
+ edit.setPosition(p + 2, QTextCursor.KeepAnchor)
+ edit.insertText("")
+ edit.endEditBlock()
+ edit.setPosition(p - 2)
+ self.setTextCursor(edit)
+ elif self.is_markdown_strike_active() and self._caret_before_marker("~~"):
+ cursor.movePosition(
+ QTextCursor.MoveOperation.Right,
+ QTextCursor.MoveMode.MoveAnchor,
+ 2,
+ )
+ self.setTextCursor(cursor)
+ self._pending_inline_marker = None
else:
cursor.insertText("~~~~")
cursor.movePosition(
diff --git a/bouquin/toolbar.py b/bouquin/toolbar.py
index 92383e6..8e8c4bf 100644
--- a/bouquin/toolbar.py
+++ b/bouquin/toolbar.py
@@ -98,6 +98,7 @@ class ToolBar(QToolBar):
self.actNumbers.triggered.connect(self.numbersRequested)
self.actCheckboxes = QAction("☑", self)
self.actCheckboxes.setToolTip(strings._("toolbar_toggle_checkboxes"))
+ self.actCheckboxes.setCheckable(True)
self.actCheckboxes.triggered.connect(self.checkboxesRequested)
# Images
@@ -126,22 +127,14 @@ class ToolBar(QToolBar):
self.actDocuments = QAction("📁", self)
self.actDocuments.setToolTip(strings._("toolbar_documents"))
self.actDocuments.triggered.connect(self.documentsRequested)
-
- # Set exclusive buttons in QActionGroups
+ # Headings are mutually exclusive (like radio buttons)
self.grpHeadings = QActionGroup(self)
self.grpHeadings.setExclusive(True)
- for a in (
- self.actBold,
- self.actItalic,
- self.actStrike,
- self.actH1,
- self.actH2,
- self.actH3,
- self.actNormal,
- ):
+ for a in (self.actH1, self.actH2, self.actH3, self.actNormal):
a.setCheckable(True)
a.setActionGroup(self.grpHeadings)
+ # List types are mutually exclusive
self.grpLists = QActionGroup(self)
self.grpLists.setExclusive(True)
for a in (self.actBullets, self.actNumbers, self.actCheckboxes):
diff --git a/debian/changelog b/debian/changelog
index 0216393..c49574f 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,24 @@
+bouquin (0.8.3) unstable; urgency=medium
+
+ * Update urllib3 dependency to resolve CVE-2026-21441
+ * Fix carrying over data to next day from over-capturing data belonging to next header section
+ * Other dependency updates
+
+ -- Miguel Jacq Fri, 30 Jan 2026 16:48:00 +1100
+
+bouquin (0.8.2) unstable; urgency=medium
+
+ * Add ability to delete an invoice via 'Manage Invoices' dialog
+
+ -- Miguel Jacq Wed, 31 Dec 2025 16:00:00 +1100
+
+bouquin (0.8.1) unstable; urgency=medium
+
+ * Fix bold/italic/strikethrough styling in certain conditions when toolbar action is used.
+ * Move a code block or collapsed section (or generally, anything after a checkbox line until the next checkbox line) when moving an unchecked checkbox line to another day.
+
+ -- Miguel Jacq Tue, 26 Dec 2025 18:00:00 +1100
+
bouquin (0.8.0) unstable; urgency=medium
* Add .desktop file for Debian
diff --git a/poetry.lock b/poetry.lock
index d2932af..0cf8448 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -14,13 +14,13 @@ files = [
[[package]]
name = "certifi"
-version = "2025.11.12"
+version = "2026.1.4"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.7"
files = [
- {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"},
- {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"},
+ {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"},
+ {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"},
]
[[package]]
@@ -158,103 +158,103 @@ files = [
[[package]]
name = "coverage"
-version = "7.13.0"
+version = "7.13.2"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.10"
files = [
- {file = "coverage-7.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070"},
- {file = "coverage-7.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98"},
- {file = "coverage-7.13.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5"},
- {file = "coverage-7.13.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e"},
- {file = "coverage-7.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33"},
- {file = "coverage-7.13.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791"},
- {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032"},
- {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9"},
- {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f"},
- {file = "coverage-7.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8"},
- {file = "coverage-7.13.0-cp310-cp310-win32.whl", hash = "sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f"},
- {file = "coverage-7.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303"},
- {file = "coverage-7.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820"},
- {file = "coverage-7.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f"},
- {file = "coverage-7.13.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96"},
- {file = "coverage-7.13.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259"},
- {file = "coverage-7.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb"},
- {file = "coverage-7.13.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9"},
- {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030"},
- {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833"},
- {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8"},
- {file = "coverage-7.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753"},
- {file = "coverage-7.13.0-cp311-cp311-win32.whl", hash = "sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b"},
- {file = "coverage-7.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe"},
- {file = "coverage-7.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7"},
- {file = "coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf"},
- {file = "coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f"},
- {file = "coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb"},
- {file = "coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621"},
- {file = "coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74"},
- {file = "coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57"},
- {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8"},
- {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d"},
- {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b"},
- {file = "coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd"},
- {file = "coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef"},
- {file = "coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae"},
- {file = "coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080"},
- {file = "coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf"},
- {file = "coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a"},
- {file = "coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74"},
- {file = "coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6"},
- {file = "coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b"},
- {file = "coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232"},
- {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971"},
- {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d"},
- {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137"},
- {file = "coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511"},
- {file = "coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1"},
- {file = "coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a"},
- {file = "coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6"},
- {file = "coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a"},
- {file = "coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8"},
- {file = "coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053"},
- {file = "coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071"},
- {file = "coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e"},
- {file = "coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493"},
- {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0"},
- {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e"},
- {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c"},
- {file = "coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e"},
- {file = "coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46"},
- {file = "coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39"},
- {file = "coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e"},
- {file = "coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256"},
- {file = "coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a"},
- {file = "coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9"},
- {file = "coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19"},
- {file = "coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be"},
- {file = "coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb"},
- {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8"},
- {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b"},
- {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9"},
- {file = "coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927"},
- {file = "coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f"},
- {file = "coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc"},
- {file = "coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b"},
- {file = "coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28"},
- {file = "coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe"},
- {file = "coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657"},
- {file = "coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff"},
- {file = "coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3"},
- {file = "coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b"},
- {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d"},
- {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e"},
- {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940"},
- {file = "coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2"},
- {file = "coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7"},
- {file = "coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc"},
- {file = "coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a"},
- {file = "coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904"},
- {file = "coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936"},
+ {file = "coverage-7.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4af3b01763909f477ea17c962e2cca8f39b350a4e46e3a30838b2c12e31b81b"},
+ {file = "coverage-7.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36393bd2841fa0b59498f75466ee9bdec4f770d3254f031f23e8fd8e140ffdd2"},
+ {file = "coverage-7.13.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cc7573518b7e2186bd229b1a0fe24a807273798832c27032c4510f47ffdb896"},
+ {file = "coverage-7.13.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca9566769b69a5e216a4e176d54b9df88f29d750c5b78dbb899e379b4e14b30c"},
+ {file = "coverage-7.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c9bdea644e94fd66d75a6f7e9a97bb822371e1fe7eadae2cacd50fcbc28e4dc"},
+ {file = "coverage-7.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5bd447332ec4f45838c1ad42268ce21ca87c40deb86eabd59888859b66be22a5"},
+ {file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7c79ad5c28a16a1277e1187cf83ea8dafdcc689a784228a7d390f19776db7c31"},
+ {file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:76e06ccacd1fb6ada5d076ed98a8c6f66e2e6acd3df02819e2ee29fd637b76ad"},
+ {file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:49d49e9a5e9f4dc3d3dac95278a020afa6d6bdd41f63608a76fa05a719d5b66f"},
+ {file = "coverage-7.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed2bce0e7bfa53f7b0b01c722da289ef6ad4c18ebd52b1f93704c21f116360c8"},
+ {file = "coverage-7.13.2-cp310-cp310-win32.whl", hash = "sha256:1574983178b35b9af4db4a9f7328a18a14a0a0ce76ffaa1c1bacb4cc82089a7c"},
+ {file = "coverage-7.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:a360a8baeb038928ceb996f5623a4cd508728f8f13e08d4e96ce161702f3dd99"},
+ {file = "coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e"},
+ {file = "coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e"},
+ {file = "coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508"},
+ {file = "coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b"},
+ {file = "coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b"},
+ {file = "coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f"},
+ {file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3"},
+ {file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b"},
+ {file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1"},
+ {file = "coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059"},
+ {file = "coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031"},
+ {file = "coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e"},
+ {file = "coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28"},
+ {file = "coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d"},
+ {file = "coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3"},
+ {file = "coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99"},
+ {file = "coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f"},
+ {file = "coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f"},
+ {file = "coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa"},
+ {file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce"},
+ {file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94"},
+ {file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5"},
+ {file = "coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b"},
+ {file = "coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41"},
+ {file = "coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e"},
+ {file = "coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894"},
+ {file = "coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6"},
+ {file = "coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc"},
+ {file = "coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f"},
+ {file = "coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1"},
+ {file = "coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9"},
+ {file = "coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c"},
+ {file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5"},
+ {file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4"},
+ {file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c"},
+ {file = "coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31"},
+ {file = "coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8"},
+ {file = "coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb"},
+ {file = "coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557"},
+ {file = "coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e"},
+ {file = "coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7"},
+ {file = "coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3"},
+ {file = "coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3"},
+ {file = "coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421"},
+ {file = "coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5"},
+ {file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23"},
+ {file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c"},
+ {file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f"},
+ {file = "coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573"},
+ {file = "coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343"},
+ {file = "coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47"},
+ {file = "coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7"},
+ {file = "coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef"},
+ {file = "coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f"},
+ {file = "coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5"},
+ {file = "coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4"},
+ {file = "coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27"},
+ {file = "coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548"},
+ {file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660"},
+ {file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92"},
+ {file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82"},
+ {file = "coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892"},
+ {file = "coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe"},
+ {file = "coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859"},
+ {file = "coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6"},
+ {file = "coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b"},
+ {file = "coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417"},
+ {file = "coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee"},
+ {file = "coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1"},
+ {file = "coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d"},
+ {file = "coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6"},
+ {file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a"},
+ {file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04"},
+ {file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f"},
+ {file = "coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f"},
+ {file = "coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3"},
+ {file = "coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba"},
+ {file = "coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c"},
+ {file = "coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5"},
+ {file = "coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3"},
]
[package.dependencies]
@@ -321,28 +321,28 @@ files = [
[[package]]
name = "markdown"
-version = "3.10"
+version = "3.10.1"
description = "Python implementation of John Gruber's Markdown."
optional = false
python-versions = ">=3.10"
files = [
- {file = "markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c"},
- {file = "markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e"},
+ {file = "markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3"},
+ {file = "markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a"},
]
[package.extras]
-docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
+docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python] (>=0.28.3)"]
testing = ["coverage", "pyyaml"]
[[package]]
name = "packaging"
-version = "25.0"
+version = "26.0"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
- {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
- {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
+ {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"},
+ {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"},
]
[[package]]
@@ -560,53 +560,58 @@ files = [
[[package]]
name = "tomli"
-version = "2.3.0"
+version = "2.4.0"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
files = [
- {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"},
- {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"},
- {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"},
- {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"},
- {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"},
- {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"},
- {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"},
- {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"},
- {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"},
- {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"},
- {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"},
- {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"},
- {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"},
- {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"},
- {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"},
- {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"},
- {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"},
- {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"},
- {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"},
- {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"},
- {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"},
- {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"},
- {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"},
- {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"},
- {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"},
- {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"},
- {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"},
- {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"},
- {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"},
- {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"},
- {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"},
- {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"},
- {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"},
- {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"},
- {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"},
- {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"},
- {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"},
- {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"},
- {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"},
- {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"},
- {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"},
- {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"},
+ {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"},
+ {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"},
+ {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"},
+ {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"},
+ {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"},
+ {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"},
+ {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"},
+ {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"},
+ {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"},
+ {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"},
+ {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"},
+ {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"},
+ {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"},
+ {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"},
+ {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"},
+ {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"},
+ {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"},
+ {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"},
+ {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"},
+ {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"},
+ {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"},
+ {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"},
+ {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"},
+ {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"},
+ {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"},
+ {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"},
+ {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"},
+ {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"},
+ {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"},
+ {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"},
+ {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"},
+ {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"},
+ {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"},
+ {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"},
+ {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"},
+ {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"},
+ {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"},
+ {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"},
+ {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"},
+ {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"},
+ {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"},
+ {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"},
+ {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"},
+ {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"},
+ {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"},
+ {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"},
+ {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"},
]
[[package]]
@@ -622,13 +627,13 @@ files = [
[[package]]
name = "urllib3"
-version = "2.6.2"
+version = "2.6.3"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.9"
files = [
- {file = "urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd"},
- {file = "urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797"},
+ {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"},
+ {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"},
]
[package.extras]
@@ -640,4 +645,4 @@ zstd = ["backports-zstd (>=1.0.0)"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<3.14"
-content-hash = "0241cd7378c45e79da728a23b89defa18f776ada9af1e60f2a19b0d90f3a2c19"
+content-hash = "0ce61476b8c35f864d395ab3a4ee4645a1ad2e16aa800cba5e77e5bfee23d6a6"
diff --git a/pyproject.toml b/pyproject.toml
index f4592bc..0eef382 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "bouquin"
-version = "0.8.0"
+version = "0.8.3"
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
authors = ["Miguel Jacq "]
readme = "README.md"
@@ -13,8 +13,8 @@ include = ["bouquin/locales/*.json", "bouquin/keys/mig5.asc", "bouquin/fonts/Not
python = ">=3.10,<3.14"
pyside6 = ">=6.8.1,<7.0.0"
bouquin-sqlcipher4 = "^4.12.0"
-requests = "^2.32.5"
-markdown = "^3.10"
+requests = "^2.32.3"
+markdown = "^3.7"
[tool.poetry.scripts]
bouquin = "bouquin.__main__:main"
diff --git a/release.sh b/release.sh
index 2fab9ac..0d3945e 100755
--- a/release.sh
+++ b/release.sh
@@ -69,4 +69,50 @@ for dist in ${DISTS[@]}; do
reprepro -b /home/user/git/repo includedeb "${release}" "${debfile}"
done
-ssh wolverine.mig5.net "echo ${VERSION} | tee /opt/www/mig5.net/bouquin/version.txt"
+
+# RPM
+sudo apt-get -y install createrepo-c rpm
+BUILD_OUTPUT="${HOME}/git/bouquin/dist"
+KEYID="00AE817C24A10C2540461A9C1D7CDE0234DB458D"
+REPO_ROOT="${HOME}/git/repo_rpm"
+REMOTE="letessier.mig5.net:/opt/repo_rpm"
+
+DISTS=(
+ fedora:42
+)
+
+for dist in ${DISTS[@]}; do
+ release=$(echo ${dist} | cut -d: -f2)
+ REPO_RELEASE_ROOT="${REPO_ROOT}/${release}"
+ RPM_REPO="${REPO_RELEASE_ROOT}/rpm/x86_64"
+ mkdir -p "$RPM_REPO"
+
+ docker build \
+ --no-cache \
+ -f Dockerfile.rpmbuild \
+ -t bouquin-rpm:${release} \
+ --progress=plain \
+ --build-arg BASE_IMAGE=${dist} \
+ .
+
+ docker run --rm -v "$PWD":/src -v "$PWD/dist/rpm":/out -v "$HOME/git/bouquin-sqlcipher4/dist/rpm":/deps:ro bouquin-rpm:${release}
+ sudo chown -R "${USER}" "$PWD/dist"
+
+ for file in `ls -1 "${BUILD_OUTPUT}/rpm"`; do
+ rpmsign --addsign "${BUILD_OUTPUT}/rpm/$file"
+ done
+
+ cp "${BUILD_OUTPUT}/rpm/"*.rpm "$RPM_REPO/"
+
+ createrepo_c "$RPM_REPO"
+
+ echo "==> Signing repomd.xml..."
+ qubes-gpg-client --local-user "$KEYID" --detach-sign --armor "$RPM_REPO/repodata/repomd.xml" > "$RPM_REPO/repodata/repomd.xml.asc"
+done
+
+echo "==> Syncing repo to server..."
+rsync -aHPvz --exclude=.git --delete "$REPO_ROOT/" "$REMOTE/"
+
+echo "Done!"
+
+ssh lupin.mig5.net "echo ${VERSION} | tee /var/www/bouquin/version.txt"
diff --git a/rpm/bouquin.spec b/rpm/bouquin.spec
new file mode 100644
index 0000000..d50d461
--- /dev/null
+++ b/rpm/bouquin.spec
@@ -0,0 +1,95 @@
+# bouquin Fedora 42 RPM spec using Fedora's pyproject RPM macros (Poetry backend).
+#
+# NOTE: Bouquin depends on "bouquin-sqlcipher4" project, but the RPM actually
+# provides the Python distribution/module as "sqlcipher4". To keep Fedora's
+# auto-generated python3dist() Requires correct, we rewrite the dependency key in
+# pyproject.toml at build time.
+%global upstream_version 0.8.3
+
+Name: bouquin
+Version: %{upstream_version}
+Release: 1%{?dist}.bouquin1
+Summary: A simple, opinionated notebook application (Python/Qt/SQLCipher)
+
+License: GPL-3.0-or-later
+URL: https://git.mig5.net/mig5/bouquin
+Source0: %{name}-%{version}.tar.gz
+
+BuildArch: noarch
+
+BuildRequires: pyproject-rpm-macros
+BuildRequires: python3-devel
+BuildRequires: python3-poetry-core
+BuildRequires: desktop-file-utils
+
+# Non-Python runtime dep (Fedora equivalent of Debian's libxcb-cursor0)
+Requires: xcb-util-cursor
+
+# Make sure private repo dependency is pulled in by package name as well.
+Requires: python3-sqlcipher4 >= 4.12.0
+
+%description
+Bouquin is a simple, opinionated notebook application written in Python and Qt,
+storing data using SQLCipher.
+
+%prep
+%autosetup -n bouquin
+
+# Patch dependency name so Fedora's python dependency generator targets the
+# provider from bouquin-sqlcipher4 RPM (python3dist(sqlcipher4)).
+%{python3} - <<'PY'
+from pathlib import Path
+import re
+
+p = Path("pyproject.toml")
+txt = p.read_text(encoding="utf-8")
+
+pattern = re.compile(r'(?ms)(^\[tool\.poetry\.dependencies\]\n.*?)(^\[|\Z)')
+m = pattern.search(txt)
+if not m:
+ raise SystemExit("Could not locate [tool.poetry.dependencies] in pyproject.toml")
+
+deps_block = m.group(1)
+deps_block2 = re.sub(r'(?m)^bouquin-sqlcipher4\s*=\s*(".*?")\s*$', r'sqlcipher4 = \1', deps_block)
+if deps_block == deps_block2:
+ raise SystemExit("Did not find bouquin-sqlcipher4 dependency to rewrite")
+
+p.write_text(txt[:m.start(1)] + deps_block2 + txt[m.end(1):], encoding="utf-8")
+PY
+
+desktop-file-validate debian/bouquin.desktop
+
+%generate_buildrequires
+%pyproject_buildrequires
+
+%build
+%pyproject_wheel
+
+%install
+%pyproject_install
+%pyproject_save_files bouquin
+
+# Desktop integration (mirrors debian/bouquin.install)
+install -Dpm 0644 debian/bouquin.desktop %{buildroot}%{_datadir}/applications/bouquin.desktop
+install -Dpm 0644 bouquin/icons/bouquin.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/bouquin.svg
+
+%files -f %{pyproject_files}
+%license LICENSE
+%doc README.md CHANGELOG.md
+%{_bindir}/bouquin
+
+%{_datadir}/applications/bouquin.desktop
+%{_datadir}/icons/hicolor/scalable/apps/bouquin.svg
+
+%changelog
+* Fri Jan 30 2026 Miguel Jacq - %{version}-%{release}
+- Update urllib3 dependency to resolve CVE-2026-21441
+- Fix carrying over data to next day from over-capturing data belonging to next header section
+- Other dependency updates
+* Wed Dec 31 2025 Miguel Jacq - %{version}-%{release}
+- Add ability to delete an invoice via 'Manage Invoices' dialog
+* Fri Dec 26 2025 Miguel Jacq - %{version}-%{release}
+- Fix bold/italic/strikethrough styling in certain conditions when toolbar action is used.
+- Move a code block or collapsed section (or generally, anything after a checkbox line until the next checkbox line) when moving an unchecked checkbox line to another day.
+* Wed Dec 24 2025 Miguel Jacq - %{version}-%{release}
+- Initial RPM packaging for Fedora 42
diff --git a/tests/test_markdown_editor.py b/tests/test_markdown_editor.py
index a36a09e..9dac5d6 100644
--- a/tests/test_markdown_editor.py
+++ b/tests/test_markdown_editor.py
@@ -1,6 +1,7 @@
import base64
import pytest
+import re
from bouquin.markdown_editor import MarkdownEditor
from bouquin.markdown_highlighter import MarkdownHighlighter
from bouquin.theme import Theme, ThemeConfig, ThemeManager
@@ -72,8 +73,13 @@ def test_apply_styles_and_headings(editor, qtbot):
editor.apply_italic()
editor.apply_strikethrough()
editor.apply_heading(24)
- md = editor.to_markdown()
- assert "**" in md and "*~~~~*" in md
+ md = editor.to_markdown().strip()
+
+ assert md.startswith("# ")
+ assert "~~hello world~~" in md
+ assert re.search(
+ r"\*{2,3}~~hello world~~\*{2,3}", md
+ ) # bold or bold+italic wrapping strike
def test_toggle_lists_and_checkboxes(editor):