Add ability to export to Markdown (and fix heading styles)
This commit is contained in:
parent
19593403b9
commit
76806bca08
6 changed files with 124 additions and 11 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
61
poetry.lock
generated
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue