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
@@ -15,6 +15,7 @@ export * from './insert';
export * from './is-abort-error';
export * from './math';
export * from './model';
export * from './number-prefix';
export * from './popper-position';
export * from './print-to-pdf';
export * from './reference';
@@ -0,0 +1,53 @@
function number2letter(n: number) {
const ordA = 'a'.charCodeAt(0);
const ordZ = 'z'.charCodeAt(0);
const len = ordZ - ordA + 1;
let s = '';
while (n >= 0) {
s = String.fromCharCode((n % len) + ordA) + s;
n = Math.floor(n / len) - 1;
}
return s;
}
// Derive from https://gist.github.com/imilu/00f32c61e50b7ca296f91e9d96d8e976
function number2roman(num: number) {
const lookup: Record<string, number> = {
M: 1000,
CM: 900,
D: 500,
CD: 400,
C: 100,
XC: 90,
L: 50,
XL: 40,
X: 10,
IX: 9,
V: 5,
IV: 4,
I: 1,
};
let romanStr = '';
for (const [key, value] of Object.entries(lookup)) {
while (num >= value) {
romanStr += key;
num -= value;
}
}
return romanStr;
}
function getPrefix(depth: number, index: number) {
const map = [
() => index,
() => number2letter(index - 1),
() => number2roman(index),
];
return map[depth % map.length]();
}
export function getNumberPrefix(index: number, depth: number) {
const prefix = getPrefix(depth, index);
return `${prefix}.`;
}