Skip to content

Commit a7a306a

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
feat(files): add Markdown PDF export
1 parent 328d985 commit a7a306a

3 files changed

Lines changed: 248 additions & 1 deletion

File tree

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,3 +560,201 @@
560560
border-radius: 2px;
561561
pointer-events: none;
562562
}
563+
564+
@media print {
565+
@page {
566+
margin: 18mm;
567+
}
568+
569+
html[data-printing-markdown],
570+
html[data-printing-markdown] body {
571+
width: auto !important;
572+
height: auto !important;
573+
min-height: 0 !important;
574+
margin: 0 !important;
575+
padding: 0 !important;
576+
overflow: visible !important;
577+
background: #fff !important;
578+
color-scheme: light;
579+
}
580+
581+
html[data-printing-markdown]
582+
body
583+
*:not(:has([data-markdown-print-active])):not([data-markdown-print-active]):not(
584+
[data-markdown-print-active] *
585+
) {
586+
display: none !important;
587+
}
588+
589+
html[data-printing-markdown] body :has([data-markdown-print-active]),
590+
html[data-printing-markdown] [data-markdown-print-active] {
591+
position: static !important;
592+
display: block !important;
593+
width: auto !important;
594+
height: auto !important;
595+
min-height: 0 !important;
596+
max-height: none !important;
597+
margin: 0 !important;
598+
padding: 0 !important;
599+
overflow: visible !important;
600+
border: 0 !important;
601+
background: #fff !important;
602+
box-shadow: none !important;
603+
transform: none !important;
604+
}
605+
606+
html[data-printing-markdown] [data-markdown-print-active] {
607+
--bg: #fff;
608+
--border: #d4d4d8;
609+
--border-1: #a1a1aa;
610+
--brand-secondary: #2563eb;
611+
--code-bg: #f4f4f5;
612+
--surface-1: #fff;
613+
--surface-2: #fff;
614+
--surface-5: #f4f4f5;
615+
--text-muted: #71717a;
616+
--text-primary: #18181b;
617+
--text-secondary: #52525b;
618+
--text-tertiary: #71717a;
619+
flex: none !important;
620+
background: #fff !important;
621+
color: #18181b !important;
622+
color-scheme: light;
623+
print-color-adjust: exact;
624+
-webkit-print-color-adjust: exact;
625+
}
626+
627+
html[data-printing-markdown] [data-markdown-print-active] > :has(> .rich-markdown-prose) {
628+
display: block !important;
629+
width: 100% !important;
630+
max-width: none !important;
631+
min-height: 0 !important;
632+
padding: 0 !important;
633+
overflow: visible !important;
634+
}
635+
636+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose {
637+
display: block !important;
638+
width: 100% !important;
639+
max-width: 100% !important;
640+
min-height: 0 !important;
641+
overflow: visible !important;
642+
color: #18181b !important;
643+
caret-color: transparent;
644+
font-size: 11pt;
645+
line-height: 1.6;
646+
}
647+
648+
html[data-printing-markdown] [data-markdown-print-active] button,
649+
html[data-printing-markdown] [data-markdown-print-active] input:not([type="checkbox"]),
650+
html[data-printing-markdown] [data-markdown-print-active] select,
651+
html[data-printing-markdown] [data-markdown-print-active] textarea,
652+
html[data-printing-markdown] [data-markdown-print-active] [role="listbox"],
653+
html[data-printing-markdown] [data-markdown-print-active] [role="menu"],
654+
html[data-printing-markdown] [data-markdown-print-active] [role="toolbar"],
655+
html[data-printing-markdown] [data-markdown-print-active] [contenteditable="false"]:has(button),
656+
html[data-printing-markdown] [data-markdown-print-active] .collaboration-carets__caret,
657+
html[data-printing-markdown] [data-markdown-print-active] .collaboration-carets__selection,
658+
html[data-printing-markdown] [data-markdown-print-active] .ProseMirror-gapcursor,
659+
html[data-printing-markdown] [data-markdown-print-active] .column-resize-handle,
660+
html[data-printing-markdown] [data-markdown-print-active] .selectedCell::after {
661+
display: none !important;
662+
}
663+
664+
html[data-printing-markdown]
665+
[data-markdown-print-active]
666+
.rich-markdown-prose
667+
.ProseMirror-selectednode,
668+
html[data-printing-markdown]
669+
[data-markdown-print-active]
670+
.rich-markdown-prose
671+
.ProseMirror-selectednode
672+
img,
673+
html[data-printing-markdown]
674+
[data-markdown-print-active]
675+
.rich-markdown-prose
676+
.rich-leaf-in-selection {
677+
outline: none !important;
678+
box-shadow: none !important;
679+
}
680+
681+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose h1,
682+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose h2,
683+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose h3,
684+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose h4,
685+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose h5,
686+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose h6 {
687+
color: #18181b !important;
688+
break-after: avoid-page;
689+
}
690+
691+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose a {
692+
color: #2563eb !important;
693+
text-decoration: underline;
694+
}
695+
696+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose pre,
697+
html[data-printing-markdown]
698+
[data-markdown-print-active]
699+
.rich-markdown-prose
700+
pre.code-editor-theme,
701+
html[data-printing-markdown]
702+
[data-markdown-print-active]
703+
.rich-markdown-prose
704+
pre.code-editor-theme
705+
code,
706+
html[data-printing-markdown]
707+
[data-markdown-print-active]
708+
.rich-markdown-prose
709+
.raw-markdown-block,
710+
html[data-printing-markdown]
711+
[data-markdown-print-active]
712+
.rich-markdown-prose
713+
.raw-markdown-inline {
714+
max-width: 100% !important;
715+
overflow: visible !important;
716+
white-space: pre-wrap !important;
717+
overflow-wrap: anywhere !important;
718+
word-break: break-word !important;
719+
background: #f4f4f5 !important;
720+
}
721+
722+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose pre,
723+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose blockquote,
724+
html[data-printing-markdown]
725+
[data-markdown-print-active]
726+
.rich-markdown-prose
727+
.mermaid-diagram-frame,
728+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose img {
729+
break-inside: avoid-page;
730+
}
731+
732+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose img,
733+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose svg {
734+
max-width: 100% !important;
735+
height: auto !important;
736+
}
737+
738+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose table {
739+
width: 100% !important;
740+
max-width: 100% !important;
741+
table-layout: auto !important;
742+
overflow: visible !important;
743+
}
744+
745+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose th,
746+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose td {
747+
min-width: 0 !important;
748+
overflow-wrap: anywhere !important;
749+
color: #18181b !important;
750+
border-color: #d4d4d8 !important;
751+
}
752+
753+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose th {
754+
background: #f4f4f5 !important;
755+
}
756+
757+
html[data-printing-markdown] [data-markdown-print-active] .rich-markdown-prose td {
758+
background: #fff !important;
759+
}
760+
}

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,6 +1204,7 @@ export function LoadedRichMarkdownEditor({
12041204
return (
12051205
<div
12061206
ref={containerRef}
1207+
data-markdown-print-root={file.id}
12071208
className={cn('relative flex flex-1 flex-col overflow-y-auto', isEditable && 'cursor-text')}
12081209
>
12091210
{editor && (

apps/sim/app/workspace/[workspaceId]/files/files.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
toast,
1919
Upload,
2020
} from '@sim/emcn'
21-
import { Download, Send } from '@sim/emcn/icons'
21+
import { Download, FileText, Send } from '@sim/emcn/icons'
2222
import { createLogger } from '@sim/logger'
2323
import { getErrorMessage, toError } from '@sim/utils/errors'
2424
import { useParams, useRouter } from 'next/navigation'
@@ -1159,6 +1159,44 @@ export function Files() {
11591159
if (file) handleDownload(file)
11601160
}, [handleDownload])
11611161

1162+
const handleSaveMarkdownAsPdf = useCallback(() => {
1163+
const file = selectedFileRef.current
1164+
const printRoot = Array.from(
1165+
document.querySelectorAll<HTMLElement>('[data-markdown-print-root]')
1166+
).find((root) => root.dataset.markdownPrintRoot === file?.id)
1167+
1168+
if (!file || !isMarkdownFile(file) || !printRoot) {
1169+
toast.error('Failed to prepare the Markdown file for PDF')
1170+
return
1171+
}
1172+
1173+
const previousTitle = document.title
1174+
const documentElement = document.documentElement
1175+
const suggestedTitle = file.name.replace(/\.(?:md|markdown)$/i, '') || file.name
1176+
let cleanedUp = false
1177+
1178+
const cleanup = () => {
1179+
if (cleanedUp) return
1180+
cleanedUp = true
1181+
printRoot.removeAttribute('data-markdown-print-active')
1182+
documentElement.removeAttribute('data-printing-markdown')
1183+
document.title = previousTitle
1184+
window.removeEventListener('afterprint', cleanup)
1185+
}
1186+
1187+
try {
1188+
document.title = suggestedTitle
1189+
printRoot.setAttribute('data-markdown-print-active', '')
1190+
documentElement.setAttribute('data-printing-markdown', '')
1191+
window.addEventListener('afterprint', cleanup, { once: true })
1192+
window.print()
1193+
} catch (error) {
1194+
cleanup()
1195+
logger.error('Failed to prepare Markdown file for PDF:', toError(error))
1196+
toast.error('Failed to prepare the Markdown file for PDF')
1197+
}
1198+
}, [])
1199+
11621200
const handleDeleteSelected = useCallback(() => {
11631201
const file = selectedFileRef.current
11641202
if (file) {
@@ -1645,6 +1683,15 @@ export function Files() {
16451683
icon: Download,
16461684
onSelect: handleDownloadSelected,
16471685
},
1686+
...(isInlineMarkdown
1687+
? [
1688+
{
1689+
text: 'Save as PDF…',
1690+
icon: FileText,
1691+
onSelect: handleSaveMarkdownAsPdf,
1692+
},
1693+
]
1694+
: []),
16481695
...(canEdit
16491696
? [
16501697
{
@@ -1668,6 +1715,7 @@ export function Files() {
16681715
handleCyclePreviewMode,
16691716
handleTogglePreview,
16701717
handleDownloadSelected,
1718+
handleSaveMarkdownAsPdf,
16711719
handleShareSelected,
16721720
handleDeleteSelected,
16731721
])

0 commit comments

Comments
 (0)