Add ability to export to Markdown (and fix heading styles)

This commit is contained in:
Miguel Jacq 2025-11-05 18:58:38 +11:00
parent 19593403b9
commit 76806bca08
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
6 changed files with 124 additions and 11 deletions

View file

@ -2,6 +2,7 @@
* Improve search results window and highlight in calendar when there are matches. * Improve search results window and highlight in calendar when there are matches.
* Fix styling issue with text that comes after a URL, so it doesn't appear as part of the URL. * Fix styling issue with text that comes after a URL, so it doesn't appear as part of the URL.
* Add ability to export to Markdown (and fix heading styles)
# 0.1.9 # 0.1.9

View file

@ -6,6 +6,7 @@ import json
import os import os
from dataclasses import dataclass from dataclasses import dataclass
from markdownify import markdownify as md
from pathlib import Path from pathlib import Path
from sqlcipher3 import dbapi2 as sqlite from sqlcipher3 import dbapi2 as sqlite
from typing import List, Sequence, Tuple from typing import List, Sequence, Tuple
@ -430,6 +431,29 @@ class DBManager:
with open(file_path, "w", encoding="utf-8") as f: with open(file_path, "w", encoding="utf-8") as f:
f.write("\n".join(parts)) f.write("\n".join(parts))
def export_markdown(
self, entries: Sequence[Entry], file_path: str, title: str = "Bouquin export"
) -> None:
parts = [
"<!doctype html>",
'<html lang="en">',
"<body>",
f"<h1>{html.escape(title)}</h1>",
]
for d, c in entries:
parts.append(
f"<article><header><time>{html.escape(d)}</time></header><section>{c}</section></article>"
)
parts.append("</body></html>")
# Convert html to markdown
md_items = []
for item in parts:
md_items.append(md(item, heading_style="ATX"))
with open(file_path, "w", encoding="utf-8") as f:
f.write("\n".join(md_items))
def export_sql(self, file_path: str) -> None: def export_sql(self, file_path: str) -> None:
""" """
Exports the encrypted database as plaintext SQL. Exports the encrypted database as plaintext SQL.

View file

@ -65,8 +65,9 @@ class Editor(QTextEdit):
def _is_heading_typing(self) -> bool: def _is_heading_typing(self) -> bool:
"""Is the current *insertion* format using a heading size?""" """Is the current *insertion* format using a heading size?"""
s = self.currentCharFormat().fontPointSize() or self.font().pointSizeF() bf = self.textCursor().blockFormat()
return any(self._approx(s, h) for h in self._HEADING_SIZES) if bf.headingLevel() > 0:
return True
def _apply_normal_typing(self): def _apply_normal_typing(self):
"""Switch the *insertion* format to Normal (default size, normal weight).""" """Switch the *insertion* format to Normal (default size, normal weight)."""
@ -611,20 +612,43 @@ class Editor(QTextEdit):
Set heading point size for typing. If there's a selection, also apply bold Set heading point size for typing. If there's a selection, also apply bold
to that selection (for H1..H3). "Normal" clears bold on the selection. to that selection (for H1..H3). "Normal" clears bold on the selection.
""" """
base_size = size if size else self.font().pointSizeF() # Map toolbar's sizes to heading levels
level = 1 if size >= 24 else 2 if size >= 18 else 3 if size >= 14 else 0
c = self.textCursor() c = self.textCursor()
# Update the typing (insertion) format to be size only, but don't represent # On-screen look
# it as if the Bold style has been toggled on
ins = QTextCharFormat() ins = QTextCharFormat()
ins.setFontPointSize(base_size) if size:
ins.setFontPointSize(float(size))
ins.setFontWeight(QFont.Weight.Bold)
else:
ins.setFontPointSize(self.font().pointSizeF())
ins.setFontWeight(QFont.Weight.Normal)
self.mergeCurrentCharFormat(ins) self.mergeCurrentCharFormat(ins)
# If user selected text, style that text visually as a heading # Apply heading level to affected block(s)
def set_level_for_block(cur):
bf = cur.blockFormat()
if hasattr(bf, "setHeadingLevel"):
bf.setHeadingLevel(level) # 0 clears heading
cur.mergeBlockFormat(bf)
if c.hasSelection(): if c.hasSelection():
sel = QTextCharFormat(ins) start, end = c.selectionStart(), c.selectionEnd()
sel.setFontWeight(QFont.Weight.Bold if size else QFont.Weight.Normal) bc = QTextCursor(self.document())
c.mergeCharFormat(sel) bc.setPosition(start)
while True:
set_level_for_block(bc)
if bc.position() >= end:
break
bc.movePosition(QTextCursor.EndOfBlock)
if bc.position() >= end:
break
bc.movePosition(QTextCursor.NextBlock)
else:
bc = QTextCursor(c)
set_level_for_block(bc)
def toggle_bullets(self): def toggle_bullets(self):
c = self.textCursor() c = self.textCursor()

View file

@ -616,6 +616,7 @@ If you want an encrypted backup, choose Backup instead of Export.
"JSON (*.json);;" "JSON (*.json);;"
"CSV (*.csv);;" "CSV (*.csv);;"
"HTML (*.html);;" "HTML (*.html);;"
"Markdown (*.md);;"
"SQL (*.sql);;" "SQL (*.sql);;"
) )
@ -631,6 +632,7 @@ If you want an encrypted backup, choose Backup instead of Export.
"JSON (*.json)": ".json", "JSON (*.json)": ".json",
"CSV (*.csv)": ".csv", "CSV (*.csv)": ".csv",
"HTML (*.html)": ".html", "HTML (*.html)": ".html",
"Markdown (*.md)": ".md",
"SQL (*.sql)": ".sql", "SQL (*.sql)": ".sql",
}.get(selected_filter, ".txt") }.get(selected_filter, ".txt")
@ -647,6 +649,8 @@ If you want an encrypted backup, choose Backup instead of Export.
self.db.export_csv(entries, filename) self.db.export_csv(entries, filename)
elif selected_filter.startswith("HTML"): elif selected_filter.startswith("HTML"):
self.db.export_html(entries, filename) self.db.export_html(entries, filename)
elif selected_filter.startswith("Markdown"):
self.db.export_markdown(entries, filename)
elif selected_filter.startswith("SQL"): elif selected_filter.startswith("SQL"):
self.db.export_sql(filename) self.db.export_sql(filename)
else: else:

61
poetry.lock generated
View file

@ -1,5 +1,27 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "beautifulsoup4"
version = "4.14.2"
description = "Screen-scraping library"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515"},
{file = "beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e"},
]
[package.dependencies]
soupsieve = ">1.2"
typing-extensions = ">=4.0.0"
[package.extras]
cchardet = ["cchardet"]
chardet = ["chardet"]
charset-normalizer = ["charset-normalizer"]
html5lib = ["html5lib"]
lxml = ["lxml"]
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
@ -158,6 +180,21 @@ files = [
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
] ]
[[package]]
name = "markdownify"
version = "1.2.0"
description = "Convert HTML to markdown."
optional = false
python-versions = "*"
files = [
{file = "markdownify-1.2.0-py3-none-any.whl", hash = "sha256:48e150a1c4993d4d50f282f725c0111bd9eb25645d41fa2f543708fd44161351"},
{file = "markdownify-1.2.0.tar.gz", hash = "sha256:f6c367c54eb24ee953921804dfe6d6575c5e5b42c643955e7242034435de634c"},
]
[package.dependencies]
beautifulsoup4 = ">=4.9,<5"
six = ">=1.15,<2"
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "25.0" version = "25.0"
@ -345,6 +382,28 @@ files = [
{file = "shiboken6-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:dfc4beab5fec7dbbebbb418f3bf99af865d6953aa0795435563d4cbb82093b61"}, {file = "shiboken6-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:dfc4beab5fec7dbbebbb418f3bf99af865d6953aa0795435563d4cbb82093b61"},
] ]
[[package]]
name = "six"
version = "1.17.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
]
[[package]]
name = "soupsieve"
version = "2.8"
description = "A modern CSS selector implementation for Beautiful Soup."
optional = false
python-versions = ">=3.9"
files = [
{file = "soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c"},
{file = "soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f"},
]
[[package]] [[package]]
name = "sqlcipher3-wheels" name = "sqlcipher3-wheels"
version = "0.5.5.post0" version = "0.5.5.post0"
@ -541,4 +600,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.9,<3.14" python-versions = ">=3.9,<3.14"
content-hash = "f5a670c96c370ce7d70dd76c7e2ebf98f7443e307b446779ea0c748db1019dd4" content-hash = "939d9d62fbe685cc74a64d6cca3584b0876142c655093f1c18da4d21fbb0718d"

View file

@ -11,6 +11,7 @@ repository = "https://git.mig5.net/mig5/bouquin"
python = ">=3.9,<3.14" python = ">=3.9,<3.14"
pyside6 = ">=6.8.1,<7.0.0" pyside6 = ">=6.8.1,<7.0.0"
sqlcipher3-wheels = "^0.5.5.post0" sqlcipher3-wheels = "^0.5.5.post0"
markdownify = "^1.2.0"
[tool.poetry.scripts] [tool.poetry.scripts]
bouquin = "bouquin.__main__:main" bouquin = "bouquin.__main__:main"