@@ -19,12 +19,23 @@ import React from 'react'
19
19
import remarkEmoji from 'remark-emoji'
20
20
import rehypeReact from 'rehype-react'
21
21
import CodeFence from '../components/atoms/markdown/CodeFence'
22
- import { getGlobalCss , selectTheme } from './styled/styleUtil'
22
+ import { getGlobalCss , selectTheme } from './styled/styleUtil'
23
23
24
24
const sanitizeNoteName = function ( rawNoteName : string ) : string {
25
25
return filenamify ( rawNoteName . toLowerCase ( ) . replace ( / \s + / g, '-' ) )
26
26
}
27
27
28
+ const getFrontMatter = ( note : NoteDoc ) : string => {
29
+ return [
30
+ '---' ,
31
+ `title: "${ note . title } "` ,
32
+ `tags: "${ note . tags . join ( ) } "` ,
33
+ '---' ,
34
+ '' ,
35
+ '' ,
36
+ ] . join ( '\n' )
37
+ }
38
+
28
39
const sanitizeSchema = mergeDeepRight ( gh , {
29
40
attributes : { '*' : [ 'className' ] } ,
30
41
} )
@@ -33,7 +44,7 @@ export const exportNoteAsHtmlFile = async (
33
44
note : NoteDoc ,
34
45
preferences : Preferences ,
35
46
pushMessage : ( context : any ) => any ,
36
- previewStyle ?: string ,
47
+ previewStyle ?: string
37
48
) : Promise < void > => {
38
49
await unified ( )
39
50
. use ( remarkParse )
@@ -88,18 +99,7 @@ export const exportNoteAsMarkdownFile = async (
88
99
return
89
100
}
90
101
let content = file . toString ( ) . trim ( ) + '\n'
91
- if ( includeFrontMatter ) {
92
- content =
93
- [
94
- '---' ,
95
- `title: "${ note . title } "` ,
96
- `tags: "${ note . tags . join ( ) } "` ,
97
- '---' ,
98
- '' ,
99
- '' ,
100
- ] . join ( '\n' ) + content
101
- }
102
-
102
+ content += includeFrontMatter ? getFrontMatter ( note ) : ''
103
103
downloadString (
104
104
content ,
105
105
`${ sanitizeNoteName ( note . title ) } .md` ,
@@ -118,120 +118,180 @@ const schema = mergeDeepRight(gh, {
118
118
} ,
119
119
} )
120
120
121
- const getCssLinks = ( preferences : Preferences ) => {
122
- let cssHrefs : string [ ] = [ ]
123
- const app = window . require ( 'electron' ) . remote . app ;
121
+ const fetchCorrectMdThemeName = ( theme : string ) => {
122
+ return theme === 'solarized-dark' ? 'solarized' : theme
123
+ }
124
+
125
+ const getCssLinks = ( preferences : Preferences ) => {
126
+ const cssHrefs : string [ ] = [ ]
127
+ const app = window . require ( 'electron' ) . remote . app
124
128
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
- }
129
+ const pathPrefix = 'file://' + app . getAppPath ( )
130
+ const parentPathTheme =
131
+ pathPrefix + ( isProd === true ? '/compiled/app' : '/../node_modules' )
132
+ const editorTheme = fetchCorrectMdThemeName ( preferences [ 'editor.theme' ] )
133
+ const markdownCodeBlockTheme = fetchCorrectMdThemeName (
134
+ preferences [ 'markdown.codeBlockTheme' ]
135
+ )
136
+
131
137
const editorThemePath = `${ parentPathTheme } /codemirror/theme/${ editorTheme } .css`
132
138
cssHrefs . push ( editorThemePath )
133
139
if ( editorTheme !== markdownCodeBlockTheme ) {
134
140
if ( markdownCodeBlockTheme ) {
135
- if ( markdownCodeBlockTheme === 'solarized-dark' ) {
136
- markdownCodeBlockTheme = 'solarized'
137
- }
138
141
const markdownCodeBlockThemePath = `${ parentPathTheme } /codemirror/theme/${ markdownCodeBlockTheme } .css`
139
142
cssHrefs . push ( markdownCodeBlockThemePath )
140
143
}
141
144
}
142
145
return cssHrefs
143
146
}
144
147
148
+ const cssStyleLinkGenerator = ( href : string ) =>
149
+ `<link rel="stylesheet" href="${ href } " type="text/css"/>`
150
+
151
+ const getPrintStyle = ( ) => `
152
+ <style media="print">
153
+ pre code {
154
+ white-space: pre-wrap;
155
+ }
156
+ </style>
157
+ `
158
+
159
+ const generatePrintToPdfHTML = (
160
+ markdownHTML : string | Uint8Array ,
161
+ preferences : Preferences ,
162
+ previewStyle ?: string
163
+ ) => {
164
+ const cssHrefs : string [ ] = getCssLinks ( preferences )
165
+ const generalThemeName = preferences [ 'general.theme' ]
166
+ const cssLinks = cssHrefs
167
+ . map ( ( href ) => cssStyleLinkGenerator ( href ) )
168
+ . join ( '\n' )
169
+ const appThemeCss = getGlobalCss ( selectTheme ( generalThemeName ) )
170
+ const previewStyleCssEl = previewStyle ? `<style>${ previewStyle } </style>` : ''
171
+ const appThemeCssEl = appThemeCss ? `<style>${ appThemeCss } </style>` : ''
172
+
173
+ return `<!DOCTYPE html>
174
+ <html lang="en">
175
+ <head>
176
+ <meta charset="UTF-8" />
177
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
178
+ <!-- Preview styles -->
179
+ ${ appThemeCssEl }
180
+ ${ previewStyleCssEl }
181
+
182
+ <!-- Link tag styles -->
183
+ ${ cssStyleLinkGenerator (
184
+ 'https://cdn.jsdelivr.net/npm/[email protected] /dist/katex.min.css'
185
+ ) }
186
+ ${ cssLinks }
187
+
188
+ <!-- Print Styles -->
189
+ ${ getPrintStyle ( ) }
190
+ </head>
191
+ <body>
192
+ <div class="${ generalThemeName } ">
193
+ ${ markdownHTML }
194
+ </div>
195
+ </body>
196
+ </html>
197
+ `
198
+ }
199
+
145
200
export const exportNoteAsPdfFile = async (
146
201
note : NoteDoc ,
147
202
preferences : Preferences ,
148
203
pushMessage : ( context : any ) => any ,
149
- previewStyle ?: string ,
204
+ { includeFrontMatter } : { includeFrontMatter : boolean } ,
205
+ previewStyle ?: string
150
206
) : Promise < void > => {
151
207
await unified ( )
152
208
. use ( remarkParse )
153
- . use ( remarkStringify )
209
+ . use ( remarkEmoji , { emoticon : false } )
210
+ . use ( [ remarkRehype , { allowDangerousHTML : true } ] )
211
+ . use ( rehypeRaw )
212
+ . use ( rehypeSanitize , schema )
213
+ . use ( remarkMath )
214
+ . use ( rehypeCodeMirror , {
215
+ ignoreMissing : true ,
216
+ theme : preferences [ 'markdown.codeBlockTheme' ] ,
217
+ } )
218
+ . use ( rehypeKatex , { output : 'htmlAndMathml' } )
219
+ . use ( rehypeReact , {
220
+ createElement : React . createElement ,
221
+ components : {
222
+ pre : CodeFence ,
223
+ } ,
224
+ } )
225
+ . use ( rehypeStringify )
154
226
. process ( note . content , ( err , file ) => {
155
227
if ( err != null ) {
156
228
pushMessage ( {
157
229
title : 'Note processing failed' ,
158
230
description : 'Please check markdown syntax and try again later.' ,
159
231
} )
232
+ return
160
233
}
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
234
185
- // Create new window (hidden)
235
+ const stringifiedMdContent = file . toString ( ) . trim ( ) + '\n'
186
236
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
237
const windowOptions = {
192
- webPreferences : { nodeIntegration : true , webSecurity : false } ,
193
- show : false
238
+ webPreferences : {
239
+ nodeIntegration : true ,
240
+ webSecurity : false ,
241
+ javascript : false ,
242
+ } ,
243
+ show : false ,
194
244
}
195
245
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` )
246
+ const htmlStr = generatePrintToPdfHTML (
247
+ stringifiedMdContent ,
248
+ preferences ,
249
+ previewStyle
250
+ )
251
+ const encodedStr = encodeURIComponent ( htmlStr )
252
+ win . loadURL ( 'data:text/html;charset=UTF-8,' + encodedStr )
199
253
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 )
254
+ // Enable when newer version of electron is available
255
+ const tagsStr =
256
+ note . tags . length > 0 ? `, tags: [${ note . tags . join ( ' ' ) } ]` : ''
257
+ const headerFooter : Record < string , string > = {
258
+ title : `${ note . title } ${ tagsStr } ` ,
259
+ url : `file://${ sanitizeNoteName ( note . title ) } .pdf` ,
207
260
}
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
261
+ const printOpts = {
262
+ // Needed for codemirorr themes (backgrounds)
263
+ printBackground : true ,
264
+ // Enable margins if header footer is printed
265
+ // No margins 1, default margins 0, 2 - minimum margins
266
+ marginsType : includeFrontMatter ? 0 : 1 ,
267
+ pageSize : 'A4' , // This could be chosen by user,
268
+ headerFooter : includeFrontMatter ? headerFooter : undefined ,
269
+ }
270
+ win . webContents
271
+ . printToPDF ( printOpts )
272
+ . then ( ( data ) => {
273
+ if ( data ) {
274
+ // We got the PDF - offer the user to save it
275
+ const pdfName = `${ sanitizeNoteName ( note . title ) } .pdf`
276
+ const pdfBlob = new Blob ( [ data ] , {
277
+ type : 'application/pdf' , // application/octet-stream
278
+ } )
279
+ downloadBlob ( pdfBlob , pdfName )
280
+ } else {
281
+ pushMessage ( {
282
+ title : 'PDF export failed' ,
283
+ description : 'Please try again later. Reason: Unknown' ,
284
+ } )
285
+ }
286
+ // Destroy window (not shown but disposes it)
287
+ win . destroy ( )
225
288
} )
226
- downloadBlob ( pdfBlob , pdfName )
227
- } else {
228
- pushMessage ( {
229
- title : 'PDF export failed' ,
230
- description : 'Please try again later.' + " Error: " + JSON . stringify ( error ) ,
289
+ . catch ( ( err ) => {
290
+ pushMessage ( {
291
+ title : 'PDF export failed' ,
292
+ description : 'Please try again later.' + ( err ? err : 'Unknown' ) ,
293
+ } )
231
294
} )
232
- }
233
- // Close window (it's hidden anyway, but dispose it)
234
- win . close ( )
235
295
} )
236
296
return
237
297
} )
0 commit comments