feat: implement export as PDF (#14057)

I used [pdfmake](https://www.npmjs.com/package/pdfmake) to implement an
"export as PDF" feature, and I am happy to share with you!

This should fix #13577, fix #8846, and fix #13959.

A showcase:

[Getting
Started.pdf](https://github.com/user-attachments/files/24013057/Getting.Started.pdf)

Although it might miss rendering some properties currently, it can
evolve in the long run and provide a more native experience for the
users.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
- Experimental "Export to PDF" option added to the export menu (behind a
feature flag)
- PDF export supports headings, paragraphs, lists, code blocks, tables,
images, callouts, linked documents and embedded content

* **Chores**
  - Added PDF rendering library and consolidated PDF utilities
  - Feature flag introduced to control rollout

* **Tests**
  - Comprehensive unit tests added for PDF content rendering logic

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: DarkSky <darksky2048@gmail.com>
This commit is contained in:
Xun Sun
2025-12-13 18:05:25 +08:00
committed by GitHub
parent 246e09e0cd
commit a0eeed0cdb
27 changed files with 3391 additions and 138 deletions
+23 -18
View File
@@ -4,25 +4,30 @@ function cleanupUnusedIndexedDB() {
return;
}
indexedDB.databases().then(databases => {
databases.forEach(database => {
if (database.name?.endsWith(':server-clock')) {
indexedDB.deleteDatabase(database.name);
}
if (database.name?.endsWith(':sync-metadata')) {
indexedDB.deleteDatabase(database.name);
}
if (
database.name?.startsWith('idx:') &&
(database.name.endsWith(':block') || database.name.endsWith(':doc'))
) {
indexedDB.deleteDatabase(database.name);
}
if (database.name?.startsWith('jp:')) {
indexedDB.deleteDatabase(database.name);
}
indexedDB
.databases()
.then(databases => {
databases.forEach(database => {
if (database.name?.endsWith(':server-clock')) {
indexedDB.deleteDatabase(database.name);
}
if (database.name?.endsWith(':sync-metadata')) {
indexedDB.deleteDatabase(database.name);
}
if (
database.name?.startsWith('idx:') &&
(database.name.endsWith(':block') || database.name.endsWith(':doc'))
) {
indexedDB.deleteDatabase(database.name);
}
if (database.name?.startsWith('jp:')) {
indexedDB.deleteDatabase(database.name);
}
});
})
.catch(error => {
console.error('Failed to cleanup unused IndexedDB databases:', error);
});
});
}
cleanupUnusedIndexedDB();