@@ -11,10 +11,19 @@ import rehypeKatex from 'rehype-katex'
1111import { mergeDeepRight } from 'ramda'
1212import gh from 'hast-util-sanitize/lib/github.json'
1313import { rehypeCodeMirror } from './../components/atoms/MarkdownPreviewer'
14- import { downloadString } from './download'
14+ import { downloadBlob , downloadString } from './download'
1515import { NoteDoc } from './db/types'
1616import { Preferences } from './preferences'
1717import { filenamify } from './string'
18+ import React from 'react'
19+ import remarkEmoji from 'remark-emoji'
20+ import rehypeReact from 'rehype-react'
21+ import CodeFence from '../components/atoms/markdown/CodeFence'
22+ import { getGlobalCss , selectTheme } from './styled/styleUtil'
23+
24+ const sanitizeNoteName = function ( rawNoteName : string ) : string {
25+ return filenamify ( rawNoteName . toLowerCase ( ) . replace ( / \s + / g, '-' ) )
26+ }
1827
1928const sanitizeSchema = mergeDeepRight ( gh , {
2029 attributes : { '*' : [ 'className' ] } ,
@@ -23,7 +32,8 @@ const sanitizeSchema = mergeDeepRight(gh, {
2332export const exportNoteAsHtmlFile = async (
2433 note : NoteDoc ,
2534 preferences : Preferences ,
26- previewStyle ?: string
35+ pushMessage : ( context : any ) => any ,
36+ previewStyle ?: string ,
2737) : Promise < void > => {
2838 await unified ( )
2939 . use ( remarkParse )
@@ -45,14 +55,16 @@ export const exportNoteAsHtmlFile = async (
4555 . use ( rehypeKatex )
4656 . process ( note . content , ( err , file ) => {
4757 if ( err != null ) {
48- /* TODO: Toast error */
49- console . error ( err )
58+ pushMessage ( {
59+ title : 'Note processing failed' ,
60+ description : 'Please check markdown syntax and try again later.' ,
61+ } )
5062 return
5163 }
5264
5365 downloadString (
5466 file . toString ( ) ,
55- `${ filenamify ( note . title . toLowerCase ( ) . replace ( / \s + / g , '-' ) ) } .html` ,
67+ `${ sanitizeNoteName ( note . title ) } .html` ,
5668 'text/html'
5769 )
5870 return
@@ -61,15 +73,18 @@ export const exportNoteAsHtmlFile = async (
6173
6274export const exportNoteAsMarkdownFile = async (
6375 note : NoteDoc ,
76+ pushMessage : ( context : any ) => any ,
6477 { includeFrontMatter } : { includeFrontMatter : boolean }
6578) : Promise < void > => {
6679 await unified ( )
6780 . use ( remarkParse )
6881 . use ( remarkStringify )
6982 . process ( note . content , ( err , file ) => {
7083 if ( err != null ) {
71- /* TODO: Toast error */
72- console . error ( err )
84+ pushMessage ( {
85+ title : 'Note processing failed' ,
86+ description : 'Please check markdown syntax and try again later.' ,
87+ } )
7388 return
7489 }
7590 let content = file . toString ( ) . trim ( ) + '\n'
@@ -87,10 +102,138 @@ export const exportNoteAsMarkdownFile = async (
87102
88103 downloadString (
89104 content ,
90- `${ filenamify ( note . title . toLowerCase ( ) . replace ( / \s + / g , '-' ) ) } .md` ,
105+ `${ sanitizeNoteName ( note . title ) } .md` ,
91106 'text/markdown'
92107 )
93108 return
94109 } )
95110 return
96111}
112+
113+ const schema = mergeDeepRight ( gh , {
114+ attributes : {
115+ '*' : [ ...gh . attributes [ '*' ] , 'className' , 'align' ] ,
116+ input : [ ...gh . attributes . input , 'checked' ] ,
117+ pre : [ 'dataRaw' ] ,
118+ } ,
119+ } )
120+
121+ const getCssLinks = ( preferences : Preferences ) => {
122+ let cssHrefs : string [ ] = [ ]
123+ const app = window . require ( 'electron' ) . remote . app ;
124+ const isProd = app . isPackaged
125+ const parentPathTheme = app . getAppPath ( ) + ( ( isProd === true ) ? "/compiled/app" : "/../node_modules" )
126+ let editorTheme = preferences [ 'editor.theme' ]
127+ let markdownCodeBlockTheme = preferences [ 'markdown.codeBlockTheme' ]
128+ if ( editorTheme === 'solarized-dark' ) {
129+ editorTheme = 'solarized'
130+ }
131+ const editorThemePath = `${ parentPathTheme } /codemirror/theme/${ editorTheme } .css`
132+ cssHrefs . push ( editorThemePath )
133+ if ( editorTheme !== markdownCodeBlockTheme ) {
134+ if ( markdownCodeBlockTheme ) {
135+ if ( markdownCodeBlockTheme === 'solarized-dark' ) {
136+ markdownCodeBlockTheme = 'solarized'
137+ }
138+ const markdownCodeBlockThemePath = `${ parentPathTheme } /codemirror/theme/${ markdownCodeBlockTheme } .css`
139+ cssHrefs . push ( markdownCodeBlockThemePath )
140+ }
141+ }
142+ return cssHrefs
143+ }
144+
145+ export const exportNoteAsPdfFile = async (
146+ note : NoteDoc ,
147+ preferences : Preferences ,
148+ pushMessage : ( context : any ) => any ,
149+ previewStyle ?: string ,
150+ ) : Promise < void > => {
151+ await unified ( )
152+ . use ( remarkParse )
153+ . use ( remarkStringify )
154+ . process ( note . content , ( err , file ) => {
155+ if ( err != null ) {
156+ pushMessage ( {
157+ title : 'Note processing failed' ,
158+ description : 'Please check markdown syntax and try again later.' ,
159+ } )
160+ }
161+ let content = file . toString ( ) . trim ( ) + '\n'
162+ const markdownProcessor = unified ( )
163+ . use ( remarkParse )
164+ . use ( remarkEmoji , { emoticon : false } )
165+ . use ( [ remarkRehype , { allowDangerousHTML : true } ] )
166+ . use ( rehypeRaw )
167+ . use ( rehypeSanitize , schema )
168+ . use ( remarkMath )
169+ . use ( rehypeCodeMirror , {
170+ ignoreMissing : true ,
171+ theme : preferences [ 'markdown.codeBlockTheme' ] ,
172+ } )
173+ . use ( rehypeKatex )
174+ . use ( rehypeReact , {
175+ createElement : React . createElement ,
176+ components : {
177+ pre : CodeFence ,
178+ } ,
179+ } )
180+ . use ( rehypeStringify )
181+
182+ // Process note-markdown content into react string
183+ let resultObj = markdownProcessor . processSync ( content )
184+
185+ // Create new window (hidden)
186+ const { BrowserWindow } = window . require ( 'electron' ) . remote
187+ const app = window . require ( 'electron' ) . remote . app ;
188+ const ipcMain = window . require ( 'electron' ) . remote . ipcMain ;
189+ const isProd = app . isPackaged === true
190+ const parentPathHTML = app . getAppPath ( ) + ( ( isProd === true ) ? "/compiled/app/static" : "/../static" )
191+ const windowOptions = {
192+ webPreferences : { nodeIntegration : true , webSecurity : false } ,
193+ show : false
194+ }
195+ const win = new BrowserWindow ( windowOptions )
196+
197+ // Load HTML for rendering react string for markdown content created earlier
198+ win . loadFile ( `${ parentPathHTML } /render_md_to_pdf.html` )
199+ win . webContents . on ( 'did-finish-load' , function ( ) {
200+ // Fetch needed CSS styles
201+ const generalThemeName = preferences [ 'general.theme' ]
202+ const appThemeCss = getGlobalCss ( selectTheme ( generalThemeName ) )
203+ let cssHrefs : string [ ] = getCssLinks ( preferences )
204+ if ( previewStyle ) {
205+ win . webContents . insertCSS ( previewStyle )
206+ win . webContents . insertCSS ( appThemeCss )
207+ }
208+ // Do not show the window while exporting (for debugging purposes only)
209+ // win.show()
210+ setTimeout ( ( ) => {
211+ // Send message to window to render the markdown content with the applied css and theme class
212+ win . webContents . send ( 'render-markdown-to-pdf' , resultObj . contents , cssHrefs , generalThemeName )
213+ } , 500 )
214+ } )
215+
216+ // When PDF rendered, notify me (doing this only once removes it for further messages)
217+ // this way no need to remove it manually after receving the message
218+ // another click on PDF export would once again bind the current note markdown to HTML rendered page
219+ ipcMain . once ( 'pdf-notify-export-data' , ( _ : object , data : string , error : any ) => {
220+ if ( data && ! error ) {
221+ // We got the PDF offer user to save it
222+ const pdfName = `${ sanitizeNoteName ( note . title ) } .pdf`
223+ const pdfBlob = new Blob ( [ data ] , {
224+ type : "application/pdf" // application/octet-stream
225+ } )
226+ downloadBlob ( pdfBlob , pdfName )
227+ } else {
228+ pushMessage ( {
229+ title : 'PDF export failed' ,
230+ description : 'Please try again later.' + " Error: " + JSON . stringify ( error ) ,
231+ } )
232+ }
233+ // Close window (it's hidden anyway, but dispose it)
234+ win . close ( )
235+ } )
236+ return
237+ } )
238+ return
239+ }
0 commit comments