feat: Super notes can now be exported as PDF (#2776)
This commit is contained in:
@@ -142,6 +142,22 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
downloadSelectedItems().catch(console.error)
|
||||
}, [downloadSelectedItems, notes])
|
||||
|
||||
const shareSelectedItems = useCallback(() => {
|
||||
createNoteExport(application, notes)
|
||||
.then((result) => {
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
const { blob, fileName } = result
|
||||
|
||||
shareBlobOnMobile(application.mobileDevice, application.isNativeMobileWeb(), blob, fileName).catch(
|
||||
console.error,
|
||||
)
|
||||
})
|
||||
.catch(console.error)
|
||||
}, [application, notes])
|
||||
|
||||
const closeMenuAndToggleNotesList = useCallback(() => {
|
||||
const isMobileScreen = matchMedia(MutuallyExclusiveMediaQueryBreakpoints.sm).matches
|
||||
if (isMobileScreen) {
|
||||
@@ -347,34 +363,14 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||
{pinShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={pinShortcut} />}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (application.isNativeMobileWeb()) {
|
||||
createNoteExport(application, notes)
|
||||
.then((result) => {
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
const { blob, fileName } = result
|
||||
|
||||
shareBlobOnMobile(application.mobileDevice, application.isNativeMobileWeb(), blob, fileName).catch(
|
||||
console.error,
|
||||
)
|
||||
})
|
||||
.catch(console.error)
|
||||
} else {
|
||||
exportSelectedItems()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon type={application.platform === Platform.Android ? 'share' : 'download'} className={iconClass} />
|
||||
{application.platform === Platform.Android ? 'Share' : 'Export'}
|
||||
<MenuItem onClick={exportSelectedItems}>
|
||||
<Icon type="download" className={iconClass} />
|
||||
Export
|
||||
</MenuItem>
|
||||
{application.platform === Platform.Android && (
|
||||
<MenuItem onClick={exportSelectedItems}>
|
||||
<Icon type="download" className={iconClass} />
|
||||
Export
|
||||
<MenuItem onClick={shareSelectedItems}>
|
||||
<Icon type="share" className={iconClass} />
|
||||
Share
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={duplicateSelectedItems} disabled={areSomeNotesInReadonlySharedVault}>
|
||||
|
||||
@@ -15,6 +15,7 @@ type Props = {
|
||||
|
||||
const SuperExportModal = ({ notes, exportNotes, close }: Props) => {
|
||||
const application = useApplication()
|
||||
|
||||
const superNoteExportFormat = usePreference(PrefKey.SuperNoteExportFormat)
|
||||
const superNoteExportEmbedBehavior = usePreference(PrefKey.SuperNoteExportEmbedBehavior)
|
||||
const superNoteExportUseMDFrontmatter = usePreference(PrefKey.SuperNoteExportUseMDFrontmatter)
|
||||
@@ -26,10 +27,15 @@ const SuperExportModal = ({ notes, exportNotes, close }: Props) => {
|
||||
if (superNoteExportFormat === 'md' && superNoteExportEmbedBehavior === 'reference') {
|
||||
void application.setPreference(PrefKey.SuperNoteExportEmbedBehavior, 'separate')
|
||||
}
|
||||
if (superNoteExportFormat === 'pdf' && superNoteExportEmbedBehavior !== 'inline') {
|
||||
void application.setPreference(PrefKey.SuperNoteExportEmbedBehavior, 'inline')
|
||||
}
|
||||
}, [application, superNoteExportEmbedBehavior, superNoteExportFormat])
|
||||
|
||||
const someNotesHaveEmbeddedFiles = notes.some(noteHasEmbeddedFiles)
|
||||
|
||||
const canShowEmbeddedFileOptions = !['json', 'pdf'].includes(superNoteExportFormat)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Export notes"
|
||||
@@ -61,6 +67,7 @@ const SuperExportModal = ({ notes, exportNotes, close }: Props) => {
|
||||
{ label: 'Super (.json)', value: 'json' },
|
||||
{ label: 'Markdown (.md)', value: 'md' },
|
||||
{ label: 'HTML', value: 'html' },
|
||||
{ label: 'PDF', value: 'pdf' },
|
||||
]}
|
||||
value={superNoteExportFormat}
|
||||
onChange={(value) => {
|
||||
@@ -93,7 +100,7 @@ const SuperExportModal = ({ notes, exportNotes, close }: Props) => {
|
||||
</Switch>
|
||||
</div>
|
||||
)}
|
||||
{superNoteExportFormat !== 'json' && someNotesHaveEmbeddedFiles && (
|
||||
{canShowEmbeddedFileOptions && someNotesHaveEmbeddedFiles && (
|
||||
<div className="mb-2 mt-4">
|
||||
<div className="mb-1">How do you want embedded files to be handled?</div>
|
||||
<RadioButtonGroup
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
import { StyleSheet } from '@react-pdf/renderer'
|
||||
import {
|
||||
$getRoot,
|
||||
$isElementNode,
|
||||
$isParagraphNode,
|
||||
$isLineBreakNode,
|
||||
$isTextNode,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
ElementNode,
|
||||
} from 'lexical'
|
||||
import { $isLinkNode } from '@lexical/link'
|
||||
import { $isHeadingNode, type HeadingNode, $isQuoteNode } from '@lexical/rich-text'
|
||||
import { $isListNode, $isListItemNode, ListType } from '@lexical/list'
|
||||
import { $isTableNode, $isTableRowNode, $isTableCellNode } from '@lexical/table'
|
||||
import { $isCodeNode } from '@lexical/code'
|
||||
import { $isInlineFileNode } from '../../../Plugins/InlineFilePlugin/InlineFileNode'
|
||||
import { $isRemoteImageNode } from '../../../Plugins/RemoteImagePlugin/RemoteImageNode'
|
||||
import { $isCollapsibleContainerNode } from '../../../Plugins/CollapsiblePlugin/CollapsibleContainerNode'
|
||||
import { $isCollapsibleContentNode } from '../../../Plugins/CollapsiblePlugin/CollapsibleContentNode'
|
||||
import { $isCollapsibleTitleNode } from '../../../Plugins/CollapsiblePlugin/CollapsibleTitleNode'
|
||||
import { PDFDataNode, PDFWorker } from './PDFWorker'
|
||||
import { wrap } from 'comlink'
|
||||
import { getBase64FromBlob } from '@/Utils'
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
page: {
|
||||
paddingVertical: 35,
|
||||
paddingHorizontal: 35,
|
||||
lineHeight: 1.5,
|
||||
fontSize: 12,
|
||||
gap: 14,
|
||||
},
|
||||
block: {
|
||||
gap: 14,
|
||||
},
|
||||
wrap: {
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
column: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
listMarker: {
|
||||
flexShrink: 0,
|
||||
height: '100%',
|
||||
marginRight: 2,
|
||||
},
|
||||
collapsibleTitle: {
|
||||
backgroundColor: 'rgba(0,0,0,0.05)',
|
||||
paddingTop: 4,
|
||||
paddingBottom: 2,
|
||||
paddingHorizontal: 6,
|
||||
borderTopLeftRadius: 6,
|
||||
borderTopRightRadius: 6,
|
||||
},
|
||||
quote: {
|
||||
borderLeftWidth: 4,
|
||||
color: 'rgba(46, 46, 46)',
|
||||
borderLeftColor: '#72767e',
|
||||
paddingLeft: 12,
|
||||
paddingVertical: 4,
|
||||
gap: 4,
|
||||
},
|
||||
})
|
||||
|
||||
const getListItemNode = ({
|
||||
children,
|
||||
value,
|
||||
listType,
|
||||
checked,
|
||||
}: {
|
||||
children: PDFDataNode[] | undefined
|
||||
value: number
|
||||
listType: ListType
|
||||
checked?: boolean
|
||||
}): PDFDataNode => {
|
||||
const marker = listType === 'bullet' ? '\u2022' : `${value}.`
|
||||
|
||||
return {
|
||||
type: 'View',
|
||||
style: styles.row,
|
||||
children: [
|
||||
listType === 'check'
|
||||
? {
|
||||
type: 'View',
|
||||
style: {
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: 2,
|
||||
borderWidth: 1,
|
||||
borderColor: checked ? '#086dd6' : '#000',
|
||||
backgroundColor: checked ? '#086dd6' : 'transparent',
|
||||
marginRight: 6,
|
||||
},
|
||||
children: checked
|
||||
? [
|
||||
{
|
||||
type: 'Svg',
|
||||
viewBox: '0 0 20 20',
|
||||
fill: '#ffffff',
|
||||
children: [
|
||||
{
|
||||
type: 'Path',
|
||||
d: 'M17.5001 5.83345L7.50008 15.8334L2.91675 11.2501L4.09175 10.0751L7.50008 13.4751L16.3251 4.65845L17.5001 5.83345Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
}
|
||||
: {
|
||||
type: 'View',
|
||||
style: styles.listMarker,
|
||||
children: [
|
||||
{
|
||||
type: 'Text',
|
||||
children: marker + ' ',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'Text',
|
||||
style: {
|
||||
flex: 1,
|
||||
},
|
||||
children,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const MinimumHeadingFontSize = 13
|
||||
const MaxHeadingLevel = 6
|
||||
const getFontSizeForHeading = (heading: HeadingNode) => {
|
||||
const level = parseInt(heading.getTag().slice(1))
|
||||
const multiplier = (MaxHeadingLevel - level) * 2
|
||||
|
||||
return MinimumHeadingFontSize + multiplier
|
||||
}
|
||||
|
||||
const getNodeTextAlignment = (node: ElementNode) => {
|
||||
const formatType = node.getFormatType()
|
||||
|
||||
if (!formatType) {
|
||||
return 'left'
|
||||
}
|
||||
|
||||
if (formatType === 'start') {
|
||||
return 'left'
|
||||
}
|
||||
|
||||
if (formatType === 'end') {
|
||||
return 'right'
|
||||
}
|
||||
|
||||
return formatType
|
||||
}
|
||||
|
||||
const getPDFDataNodeFromLexicalNode = (node: LexicalNode): PDFDataNode => {
|
||||
const parent = node.getParent()
|
||||
|
||||
if ($isLineBreakNode(node)) {
|
||||
return {
|
||||
type: 'Text',
|
||||
children: '\n',
|
||||
}
|
||||
}
|
||||
|
||||
if ($isTextNode(node)) {
|
||||
const isInlineCode = node.hasFormat('code')
|
||||
const isCodeNodeText = $isCodeNode(parent)
|
||||
const isBold = node.hasFormat('bold')
|
||||
const isItalic = node.hasFormat('italic')
|
||||
const isHighlight = node.hasFormat('highlight')
|
||||
|
||||
let font = isInlineCode || isCodeNodeText ? 'Courier' : 'Helvetica'
|
||||
if (isBold || isItalic) {
|
||||
font += '-'
|
||||
if (isBold) {
|
||||
font += 'Bold'
|
||||
}
|
||||
if (isItalic) {
|
||||
font += 'Oblique'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Text',
|
||||
children: node.getTextContent(),
|
||||
style: {
|
||||
fontFamily: font,
|
||||
textDecoration: node.hasFormat('underline')
|
||||
? 'underline'
|
||||
: node.hasFormat('strikethrough')
|
||||
? 'line-through'
|
||||
: undefined,
|
||||
backgroundColor: isInlineCode ? '#f1f1f1' : isHighlight ? 'rgb(255,255,0)' : undefined,
|
||||
fontSize: isInlineCode || isCodeNodeText ? 11 : undefined,
|
||||
textAlign: $isElementNode(parent) ? getNodeTextAlignment(parent) : 'left',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if ($isCodeNode(node)) {
|
||||
const children = node.getChildren()
|
||||
const lines: LexicalNode[][] = [[]]
|
||||
|
||||
for (let i = 0, currentLine = 0; i < children.length; i++) {
|
||||
const child = children[i]
|
||||
|
||||
if (!$isLineBreakNode(child)) {
|
||||
lines[currentLine].push(child)
|
||||
} else {
|
||||
lines.push([])
|
||||
currentLine++
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'View',
|
||||
style: [
|
||||
styles.column,
|
||||
{
|
||||
backgroundColor: 'rgba(0,0,0,0.05)',
|
||||
padding: 12,
|
||||
borderRadius: 6,
|
||||
fontFamily: 'Courier',
|
||||
},
|
||||
],
|
||||
children: lines.map((line) => {
|
||||
return {
|
||||
type: 'View',
|
||||
style: [styles.row, styles.wrap],
|
||||
children: line.map((child) => {
|
||||
return getPDFDataNodeFromLexicalNode(child)
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
if ($isInlineFileNode(node) || $isRemoteImageNode(node)) {
|
||||
if (!node.__src.startsWith('data:')) {
|
||||
return {
|
||||
type: 'View',
|
||||
style: styles.block,
|
||||
children: [
|
||||
{
|
||||
type: 'Link',
|
||||
src: node.__src,
|
||||
children: node.__src,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'Image',
|
||||
src: node.__src,
|
||||
}
|
||||
}
|
||||
|
||||
const children =
|
||||
$isElementNode(node) || $isTableNode(node) || $isTableCellNode(node) || $isTableRowNode(node)
|
||||
? node.getChildren().map((child) => {
|
||||
return getPDFDataNodeFromLexicalNode(child)
|
||||
})
|
||||
: undefined
|
||||
|
||||
if ($isLinkNode(node)) {
|
||||
return {
|
||||
type: 'Link',
|
||||
src: node.getURL(),
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
if ($isListItemNode(node)) {
|
||||
if (!$isListNode(parent)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const listType = parent.getListType()
|
||||
|
||||
const isNestedList = node.getChildren().some((child) => $isListNode(child))
|
||||
|
||||
if (isNestedList) {
|
||||
return {
|
||||
type: 'View',
|
||||
style: [
|
||||
styles.column,
|
||||
{
|
||||
marginLeft: 10,
|
||||
},
|
||||
],
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
return getListItemNode({
|
||||
children,
|
||||
listType,
|
||||
value: node.getValue(),
|
||||
checked: node.getChecked(),
|
||||
})
|
||||
}
|
||||
|
||||
if ($isListNode(node)) {
|
||||
return {
|
||||
type: 'View',
|
||||
style: [
|
||||
styles.column,
|
||||
{
|
||||
gap: 7,
|
||||
},
|
||||
],
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
if ($isCollapsibleContentNode(node)) {
|
||||
return {
|
||||
type: 'View',
|
||||
style: [
|
||||
styles.block,
|
||||
styles.column,
|
||||
{
|
||||
padding: 6,
|
||||
},
|
||||
],
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
if ($isCollapsibleContainerNode(node)) {
|
||||
return {
|
||||
type: 'View',
|
||||
style: [
|
||||
styles.column,
|
||||
{
|
||||
backgroundColor: 'rgba(0,0,0,0.05)',
|
||||
borderRadius: 6,
|
||||
},
|
||||
],
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
if ($isParagraphNode(node) && node.getTextContent().length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if ($isTableCellNode(node)) {
|
||||
return {
|
||||
type: 'View',
|
||||
style: {
|
||||
backgroundColor: node.hasHeader() ? '#f4f5f7' : undefined,
|
||||
borderColor: '#e3e3e3',
|
||||
borderWidth: 1,
|
||||
flex: 1,
|
||||
padding: 2,
|
||||
},
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
if ($isTableRowNode(node)) {
|
||||
return {
|
||||
type: 'View',
|
||||
style: styles.row,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
if ($isTableNode(node)) {
|
||||
return {
|
||||
type: 'View',
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
if ($isElementNode(node)) {
|
||||
return {
|
||||
type: 'View',
|
||||
style: [
|
||||
styles.block,
|
||||
styles.row,
|
||||
styles.wrap,
|
||||
{
|
||||
fontSize: $isHeadingNode(node) ? getFontSizeForHeading(node) : undefined,
|
||||
},
|
||||
$isCollapsibleTitleNode(node) ? styles.collapsibleTitle : {},
|
||||
$isQuoteNode(node) ? styles.quote : {},
|
||||
],
|
||||
children: [
|
||||
{
|
||||
type: 'Text',
|
||||
style: {
|
||||
lineHeight: $isHeadingNode(node) ? 1 : 1.5,
|
||||
},
|
||||
children,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'View',
|
||||
style: [styles.block, styles.row, styles.wrap],
|
||||
children: [{ type: 'Text', children: node.getTextContent() }],
|
||||
}
|
||||
}
|
||||
|
||||
const getPDFDataNodesFromLexicalNodes = (nodes: LexicalNode[]): PDFDataNode[] => {
|
||||
return nodes.map(getPDFDataNodeFromLexicalNode)
|
||||
}
|
||||
|
||||
const PDFWorkerComlink = wrap<PDFWorker>(new Worker(new URL('./PDFWorker.tsx', import.meta.url)))
|
||||
|
||||
/**
|
||||
* @returns The PDF as a base64 string
|
||||
*/
|
||||
export function $generatePDFFromNodes(editor: LexicalEditor) {
|
||||
return new Promise<string>((resolve) => {
|
||||
editor.getEditorState().read(() => {
|
||||
const root = $getRoot()
|
||||
const nodes = root.getChildren()
|
||||
|
||||
const pdfDataNodes = getPDFDataNodesFromLexicalNodes(nodes)
|
||||
|
||||
void PDFWorkerComlink.renderPDF(pdfDataNodes).then((blob) => {
|
||||
void getBase64FromBlob(blob).then((base64) => {
|
||||
resolve(base64)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
Document,
|
||||
Page,
|
||||
View,
|
||||
Text,
|
||||
pdf,
|
||||
Link,
|
||||
Image,
|
||||
Svg,
|
||||
Path,
|
||||
ViewProps,
|
||||
LinkProps,
|
||||
PathProps,
|
||||
TextProps,
|
||||
SVGProps,
|
||||
ImageWithSrcProp,
|
||||
} from '@react-pdf/renderer'
|
||||
import { expose } from 'comlink'
|
||||
|
||||
export type PDFDataNode =
|
||||
| ((
|
||||
| ({
|
||||
type: 'View'
|
||||
} & Omit<ViewProps, 'children'>)
|
||||
| ({
|
||||
type: 'Text'
|
||||
} & Omit<TextProps, 'children'>)
|
||||
| ({
|
||||
type: 'Link'
|
||||
} & Omit<LinkProps, 'children'>)
|
||||
| ({
|
||||
type: 'Image'
|
||||
} & Omit<ImageWithSrcProp, 'children'>)
|
||||
| ({
|
||||
type: 'Svg'
|
||||
} & Omit<SVGProps, 'children'>)
|
||||
| ({
|
||||
type: 'Path'
|
||||
} & Omit<PathProps, 'children'>)
|
||||
) & {
|
||||
children?: PDFDataNode[] | string
|
||||
})
|
||||
| null
|
||||
|
||||
const Node = ({ node }: { node: PDFDataNode }) => {
|
||||
if (!node) {
|
||||
return null
|
||||
}
|
||||
|
||||
const children =
|
||||
typeof node.children === 'string'
|
||||
? node.children
|
||||
: node.children?.map((child, index) => {
|
||||
return <Node node={child} key={index} />
|
||||
})
|
||||
|
||||
switch (node.type) {
|
||||
case 'View':
|
||||
return <View {...node}>{children}</View>
|
||||
case 'Text':
|
||||
return <Text {...node}>{children}</Text>
|
||||
case 'Link':
|
||||
return <Link {...node}>{children}</Link>
|
||||
case 'Image':
|
||||
return <Image {...node} />
|
||||
case 'Svg':
|
||||
return <Svg {...node}>{children}</Svg>
|
||||
case 'Path': {
|
||||
const { children: _, ...props } = node
|
||||
return <Path {...props} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const PDFDocument = ({ nodes }: { nodes: PDFDataNode[] }) => {
|
||||
return (
|
||||
<Document>
|
||||
<Page
|
||||
style={{
|
||||
paddingVertical: 35,
|
||||
paddingHorizontal: 35,
|
||||
lineHeight: 1.5,
|
||||
fontSize: 12,
|
||||
gap: 14,
|
||||
}}
|
||||
>
|
||||
{nodes.map((node, index) => {
|
||||
return <Node node={node} key={index} />
|
||||
})}
|
||||
</Page>
|
||||
</Document>
|
||||
)
|
||||
}
|
||||
|
||||
const renderPDF = (nodes: PDFDataNode[]) => {
|
||||
return pdf(<PDFDocument nodes={nodes} />).toBlob()
|
||||
}
|
||||
|
||||
expose({
|
||||
renderPDF,
|
||||
})
|
||||
|
||||
export type PDFWorker = {
|
||||
renderPDF: typeof renderPDF
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { $createFileExportNode } from '../Lexical/Nodes/FileExportNode'
|
||||
import { $createInlineFileNode } from '../Plugins/InlineFilePlugin/InlineFileNode'
|
||||
import { $convertFromMarkdownString } from '../Lexical/Utils/MarkdownImport'
|
||||
import { $convertToMarkdownString } from '../Lexical/Utils/MarkdownExport'
|
||||
|
||||
export class HeadlessSuperConverter implements SuperConverterServiceInterface {
|
||||
private importEditor: LexicalEditor
|
||||
private exportEditor: LexicalEditor
|
||||
@@ -50,7 +51,7 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface {
|
||||
|
||||
async convertSuperStringToOtherFormat(
|
||||
superString: string,
|
||||
toFormat: 'txt' | 'md' | 'html' | 'json',
|
||||
toFormat: 'txt' | 'md' | 'html' | 'json' | 'pdf',
|
||||
config?: {
|
||||
embedBehavior?: PrefValue[PrefKey.SuperNoteExportEmbedBehavior]
|
||||
getFileItem?: (id: string) => FileItem | undefined
|
||||
@@ -92,7 +93,8 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface {
|
||||
if (!fileItem) {
|
||||
return
|
||||
}
|
||||
if (embedBehavior === 'inline' && getFileBase64) {
|
||||
const canInlineFileType = toFormat === 'pdf' ? fileItem.mimeType.startsWith('image/') : true
|
||||
if (embedBehavior === 'inline' && getFileBase64 && canInlineFileType) {
|
||||
const fileBase64 = await getFileBase64(fileNode.getId())
|
||||
if (!fileBase64) {
|
||||
return
|
||||
@@ -122,31 +124,45 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface {
|
||||
)
|
||||
})
|
||||
|
||||
this.exportEditor.update(
|
||||
() => {
|
||||
switch (toFormat) {
|
||||
case 'txt':
|
||||
case 'md': {
|
||||
const paragraphs = $nodesOfType(ParagraphNode)
|
||||
for (const paragraph of paragraphs) {
|
||||
if (paragraph.isEmpty()) {
|
||||
paragraph.remove()
|
||||
await new Promise<void>((resolve) => {
|
||||
this.exportEditor.update(
|
||||
() => {
|
||||
switch (toFormat) {
|
||||
case 'txt':
|
||||
case 'md': {
|
||||
const paragraphs = $nodesOfType(ParagraphNode)
|
||||
for (const paragraph of paragraphs) {
|
||||
if (paragraph.isEmpty()) {
|
||||
paragraph.remove()
|
||||
}
|
||||
}
|
||||
content = $convertToMarkdownString(MarkdownTransformers)
|
||||
resolve()
|
||||
break
|
||||
}
|
||||
content = $convertToMarkdownString(MarkdownTransformers)
|
||||
break
|
||||
case 'html':
|
||||
content = $generateHtmlFromNodes(this.exportEditor)
|
||||
resolve()
|
||||
break
|
||||
case 'pdf': {
|
||||
void import('../Lexical/Utils/PDFExport/PDFExport').then(({ $generatePDFFromNodes }) => {
|
||||
void $generatePDFFromNodes(this.exportEditor).then((pdf) => {
|
||||
content = pdf
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'json':
|
||||
default:
|
||||
content = superString
|
||||
resolve()
|
||||
break
|
||||
}
|
||||
case 'html':
|
||||
content = $generateHtmlFromNodes(this.exportEditor)
|
||||
break
|
||||
case 'json':
|
||||
default:
|
||||
content = superString
|
||||
break
|
||||
}
|
||||
},
|
||||
{ discrete: true },
|
||||
)
|
||||
},
|
||||
{ discrete: true },
|
||||
)
|
||||
})
|
||||
|
||||
if (typeof content !== 'string') {
|
||||
throw new Error('Could not export note')
|
||||
|
||||
@@ -77,6 +77,9 @@ export const getNoteBlob = async (
|
||||
case 'md':
|
||||
type = 'text/markdown'
|
||||
break
|
||||
case 'pdf':
|
||||
type = 'application/pdf'
|
||||
break
|
||||
default:
|
||||
type = 'text/plain'
|
||||
break
|
||||
@@ -103,11 +106,15 @@ export const getNoteBlob = async (
|
||||
PrefKey.SuperNoteExportUseMDFrontmatter,
|
||||
PrefDefaults[PrefKey.SuperNoteExportUseMDFrontmatter],
|
||||
)
|
||||
// result is a data url string if format is pdf
|
||||
const result =
|
||||
format === 'html' ? superHTML(note, content) : useMDFrontmatter ? superMarkdown(note, content) : content
|
||||
const blob = new Blob([result], {
|
||||
type,
|
||||
})
|
||||
const blob =
|
||||
format === 'pdf'
|
||||
? await fetch(result).then((res) => res.blob())
|
||||
: new Blob([result], {
|
||||
type,
|
||||
})
|
||||
return blob
|
||||
}
|
||||
const blob = new Blob([note.text], {
|
||||
@@ -132,7 +139,7 @@ const noteRequiresFolder = (
|
||||
if (!isSuperNote(note)) {
|
||||
return false
|
||||
}
|
||||
if (superExportFormat === 'json') {
|
||||
if (superExportFormat === 'json' || superExportFormat === 'pdf') {
|
||||
return false
|
||||
}
|
||||
if (superEmbedBehavior !== 'separate') {
|
||||
@@ -178,10 +185,13 @@ export const createNoteExport = async (
|
||||
PrefKey.SuperNoteExportFormat,
|
||||
PrefDefaults[PrefKey.SuperNoteExportFormat],
|
||||
)
|
||||
const superEmbedBehaviorPref = application.getPreference(
|
||||
PrefKey.SuperNoteExportEmbedBehavior,
|
||||
PrefDefaults[PrefKey.SuperNoteExportEmbedBehavior],
|
||||
)
|
||||
const superEmbedBehaviorPref =
|
||||
superExportFormatPref === 'pdf'
|
||||
? 'inline'
|
||||
: application.getPreference(
|
||||
PrefKey.SuperNoteExportEmbedBehavior,
|
||||
PrefDefaults[PrefKey.SuperNoteExportEmbedBehavior],
|
||||
)
|
||||
|
||||
if (notes.length === 1 && !noteRequiresFolder(notes[0], superExportFormatPref, superEmbedBehaviorPref)) {
|
||||
const blob = await getNoteBlob(application, notes[0], superEmbedBehaviorPref)
|
||||
|
||||
Reference in New Issue
Block a user