Add Android PDF export with share sheet delivery#96
Conversation
18b08ae to
ec62ac7
Compare
|
All four findings verified against the code and fixed in the updated commit ( [P1] Rendering parity — fixed by passing the live Muya instance. Confirmed the mismatch exactly as described: the app defaults [P1] Timeout now cancels and cleans up the active print stage. [P2] Front matter — switched to the desktop approach. You were right that the YAML-only regex was both incomplete (TOML/JSON front matter untouched) and unnecessary: the renderer''s marked options default [P2] Fixed settle delay replaced with a bounded readiness gate. The session now enables JavaScript solely for a readiness probe (the document is locally generated and DOMPurify-sanitized, scripts/handlers already stripped) and polls every 250 ms for Re-verified: 342/342 unit (56 files), lint, |
The document action menu gains "Export as PDF": the current editor content — including unsaved changes, flushed the same way the Markdown share flow does — is rendered to a real paginated PDF and handed to the Android share sheet as application/pdf with a document-derived filename, matching the export-and-share experience of desktop editors. HTML generation reuses the desktop export pipeline that already ships in the vendored Muya: MarkdownToHtml.generate produces a fully self-contained document (inlined GitHub styles, KaTeX with fonts embedded as data URIs, Prism-highlighted code, diagrams pre-rendered to SVG in the app WebView). On top of that the export path adds print CSS (A4 @page margins, heading break-avoidance, forced background printing), strips leading front matter the export renderer would show as content, and rewrites marktext-image://local/ sources to file URIs inside the app sandbox so the imported images resolve outside the Capacitor WebView. PDF bytes are produced natively: a detached JavaScript-free WebView loads the export document and its print adapter is driven straight to a file through the platform print pipeline — the same vector output as the system "Save as PDF" printer, with no print dialog. The LayoutResultCallback/WriteResultCallback constructors are package-private, so a small driver class lives in the android.print package, the established technique behind WebView-to-PDF converters. The exporter owns a 30s timeout, render settling, and single-shot cleanup; the plugin writes into a new shared-pdf cache directory and reuses the existing FileProvider and chooser conventions (self excluded, ClipData, read grant). The web flow mirrors the Markdown share exactly: an exportingPdf busy flag disables the menu row, status shows "Exporting PDF" then "Share sheet opened", and failures map through the shared error-code table (PDF_EXPORT_FAILED, PDF_WRITE_FAILED) with en/zh translations. Verified end to end on the API 35 emulator: styled document with KaTeX math, highlighted code with backgrounds, tables, quotes, and an imported local image exported to a selectable-text A4 PDF and offered through the share sheet under the document title.
ec62ac7 to
6e71770
Compare
|
Addressed the Lint suppression + boundary documentation. Regression tests — with an honest scope note. While writing the sanitizer tests I found that the boundary is not uniformly assertable in the unit environment, so I split it by what each layer can reliably prove:
The reason for the split: muya''s DOMPurify binds a window singleton at import ( Verification: 344/344 unit (56 files), lint, |
Summary
"Export as PDF" joins the editor document action menu. It renders the latest editor content — including unsaved changes — into a real paginated A4 PDF with selectable text, then opens the Android share sheet with the file attached as
application/pdfunder a document-derived name (Trip notes.pdf). No Save As picker, no system print dialog.Chosen PDF-generation approach
Reused HTML pipeline + native headless print. Two stages:
MarkdownToHtml.generate), which produces a fully self-contained document: inlined GitHub styles, KaTeX with fonts embedded as data URIs (math renders offline), Prism-highlighted code, and mermaid/vega/plantuml/flowchart code blocks pre-rendered to SVG in the app WebView before the HTML leaves it. The newpdfExportHtmlmodule adds A4 print CSS (@pagemargins, heading break-avoidance, forced background printing so code blocks keep their fill), strips leading front matter (the export renderer would otherwise show it as content; desktop hides it), and rewritesmarktext-image://local/…sources tofile://URIs in the app sandbox — the export sanitizer explicitly allowsfile:(the same allowance the desktop uses for exported images).exportMarkdownPdfplugin method loads that HTML in a detached, JavaScript-disabled WebView and drivescreatePrintDocumentAdapter(onLayout/onWrite) straight into a file in a newshared-pdf/cache directory. This is the same Chromium print pipeline as the system "Save as PDF" printer — true vector text, proper pagination — minus the dialog. TheLayoutResultCallback/WriteResultCallbackconstructors are package-private, so a smallPdfPrintAdapterDriverlives in theandroid.printpackage (the long-established technique used by WebView-to-PDF converters; verified working on the API 35 emulator). The exporter owns a 30 s timeout, a render-settle delay for image decoding, and single-shot cleanup. The share Intent reuses the existing FileProvider authority and chooser conventions (self excluded, ClipData, read grant).Alternatives investigated
PrintManager.print) — produces the same PDF but forces the user through the print preview UI; explicitly out of scope.PdfDocument+WebView.draw— manual pagination, nobreak-insidesupport, inconsistent output under hardware acceleration. The print adapter path is the sanctioned one.printToPDF— that API is main-process Electron; only its options were mirrored (A4, backgrounds on, break-avoidance CSS).What was reused from the desktop implementation
The desktop reference (
E:\marktext-contrib-latest, fast-forwarded to upstreamdevelop43bd8b77first, feature branch and local files untouched) generates HTML with the sameMarkdownToHtmlengine this repo vendors — so the entire rendering stage is shared code, not a port. Mirrored from the desktop wrapper: print CSS margin/break rules, background printing, front-matter hiding, file-URI image resolution, and the RTLdirpass-through (upstream #4874/#4553); the text-direction setting flows into the exported document. Deliberately not copied: header/footer table wrapping,[TOC]injection, heading auto-numbering, theme/page-size settings UI — desktop-only complexity this scope does not need.Architecture
Mirrors the Markdown share flow exactly at every layer:
exportCurrentDocumentPdfincurrentDocumentPersistence(same guards, editor flush with pending-IME commit, draft autosave, busy flag, status text) →exportAndroidPdfWorkflow(pure, injected collaborators) →androidDocumentsbridge → plugin. Rendering concerns live infeatures/editor/pdfExportHtml.ts; the native printer is its ownPdfExporterclass. Progress and failure states: menu row disabled while exporting, statusExporting PDF→Share sheet opened, failures map through the shared error table (PDF_EXPORT_FAILED,PDF_WRITE_FAILED) with en/zh translations.Known limitations / deferred
marktext-image://android/…(picker used with "copy images" off) resolve tocontent://URIs, which the export sanitizer strips — they degrade to their alt text. The default copy-on-import path exports correctly.http(s)images render when the network answers within the settle window; offline they degrade gracefully.Testing
pdfExportHtml(front-matter stripping incl. thematic-break false positives, image-source rewriting incl. resolver-throw and plain-URL passthrough, print CSS invariants), persistence orchestration (busy-flag reentry guard, draft autosave before export, failure status, flush semantics), and PDF filename derivation.SharePreparationTestcovers the PDF name normalization (sanitize, extension append, case-preserving.PDF, null/blank →Untitled.pdf);gradlew testDebugUnitTestgreen.pnpm lint,pnpm build(vue-tsc + vite),gradlew assembleDebugall clean; related Mobile WebView e2e specs (document actions, document lifecycle, editor toolbar — 25 tests) pass.PDF Export Smoke.pdf; pulled the file: valid%PDF-1.4, A4, selectable text, math as vector glyphs, code block background preserved. Second document with an imported local image: image renders in the PDF between its surrounding paragraphs. Native and JS logs confirm the full pipeline.