Skip to content

Commit

Permalink
read out header-includes from PanWriterUserData/{document-type}.yaml
Browse files Browse the repository at this point in the history
  • Loading branch information
mb21 committed Jun 5, 2021
1 parent fc19a8a commit f55140b
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 38 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ If the directory does not exist, you can create it.

If you put a `default.yaml` file in the data directory, PanWriter will merge this with the YAML in your input file (to determine the command-line arguments to call pandoc with) and add the `--metadata-file` option. The YAML should be in the same format as above.

To include CSS in your `default.yaml`, you can also use the same format as in-document metadata, for example:

```yaml
header-includes: |-
<style>
blockquote {
font-style: italic;
}
</style>
```
### Document types / themes
You can e.g. put `type: letter` in the YAML of your input document. In that case, PanWriter will look for `letter.yaml` instead of `default.yaml` in the user data directory.
Expand Down
28 changes: 28 additions & 0 deletions electron/dataDir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { readFile } from 'fs/promises'
import * as jsYaml from 'js-yaml'
import { app } from 'electron'
import { basename, sep } from 'path'
import { Meta } from '../src/appState/AppState'

export const dataDir = [app.getPath('appData'), 'PanWriterUserData', ''].join(sep)

/**
* reads the right default yaml file
* make sure this function is safe to expose in `preload.ts`
*/
export const readDataDirFile = async (fileName: string): Promise<[Meta | undefined, string]> => {
try {
// make sure only PanWriterUserData directory can be accessed
fileName = dataDir + basename(fileName)

const str = await readFile(fileName, 'utf8')
const yaml = jsYaml.safeLoad(str)
return [
typeof yaml === 'object' ? (yaml as Meta) : {},
fileName
]
} catch(e) {
console.warn("Error loading or parsing YAML file." + e.message)
return [ undefined, fileName ]
}
}
6 changes: 6 additions & 0 deletions electron/ipc.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BrowserWindow, ipcMain, shell } from 'electron'
import { Doc } from '../src/appState/AppState'
import { readDataDirFile } from './dataDir'
import { Message } from './preload'

// this file contains the IPC functionality of the main process.
Expand All @@ -25,6 +26,11 @@ export const init = () => {
ipcMain.on('openLink', (_event, link: string) => {
shell.openExternal(link)
})

ipcMain.handle('readDataDirFile', async (_event, fileName: string) => {
const [ meta ] = await readDataDirFile(fileName)
return meta
})
}

export const getDoc = async (win: BrowserWindow): Promise<Doc> => {
Expand Down
37 changes: 6 additions & 31 deletions electron/pandoc/export.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { spawn, SpawnOptionsWithoutStdio } from 'child_process'
import { app, BrowserWindow, clipboard, dialog } from 'electron'
import { readFile } from 'fs'
import * as jsYaml from 'js-yaml'
import { basename, dirname, extname, sep } from 'path'
import { promisify } from 'util'
import { BrowserWindow, clipboard, dialog } from 'electron'
import { basename, dirname, extname } from 'path'
import { Doc, JSON, Meta } from '../../src/appState/AppState'
import { readDataDirFile } from '../dataDir';

interface ExportOptions {
outputPath?: string;
Expand All @@ -25,8 +23,6 @@ declare class CustomBrowserWindow extends Electron.BrowserWindow {
previousExportConfig?: ExportOptions;
}

export const dataDir = [app.getPath('appData'), 'PanWriterUserData', ''].join(sep)

export const fileExportDialog = async (win: CustomBrowserWindow, doc: Doc) => {
const spawnOpts: SpawnOptionsWithoutStdio = {}
const inputPath = doc.filePath
Expand Down Expand Up @@ -95,11 +91,11 @@ const fileExport = async (win: BrowserWindow, doc: Doc, exp: ExportOptions) => {
const type = typeof docMeta.type === 'string'
? docMeta.type
: 'default'
const [extMeta, fileArg] = await defaultMeta(type)
const out = mergeAndValidate(docMeta, extMeta, exp.outputPath, exp.toClipboardFormat)
const [extMeta, fileName] = await readDataDirFile(type + '.yaml')
const out = mergeAndValidate(docMeta, extMeta || {}, exp.outputPath, exp.toClipboardFormat)

const cmd = 'pandoc'
const args = fileArg.concat( toArgs(out) )
const args = (extMeta ? ['--metadata-file', fileName] : []).concat( toArgs(out) )
const cmdDebug = cmd + ' ' + args.map(a => a.includes(' ') ? `'${a}'` : a).join(' ')
let receivedError = false

Expand Down Expand Up @@ -224,27 +220,6 @@ const mergeAndValidate = (docMeta: Meta, extMeta: Meta, outputPath?: string, toC
return out;
}

/**
* reads the right default yaml file
*/
const defaultMeta = async (type: string): Promise<[Meta, string[]]> => {
try {
const [str, fileName] = await readDataDirFile(type, '.yaml');
const yaml = jsYaml.safeLoad(str)
return [ typeof yaml === 'object' ? (yaml as Meta) : {}, ['--metadata-file', fileName] ]
} catch(e) {
console.warn("Error loading or parsing YAML file." + e.message);
return [ {}, [] ];
}
}

// reads file from data directory, throws exception when not found
const readDataDirFile = async (type: string, suffix: string) => {
const fileName = dataDir + type + suffix
const str = await promisify(readFile)(fileName, 'utf8')
return [str, fileName]
}

// constructs commandline arguments from object
const toArgs = (out: Out) => {
const args: string[] = [];
Expand Down
6 changes: 5 additions & 1 deletion electron/preload.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { contextBridge, ipcRenderer } from 'electron'
import { AppState, Doc, ViewSplit } from '../src/appState/AppState'
import { AppState, Doc, Meta, ViewSplit } from '../src/appState/AppState'
import { Action } from '../src/appState/Action'

export type IpcApi = typeof ipcApi
Expand Down Expand Up @@ -33,6 +33,9 @@ ipcRenderer.on('dispatch', (_e, action: Message) => {
}
})

const readDataDirFile = async (fileName: string): Promise<Meta | undefined> =>
ipcRenderer.invoke('readDataDirFile', fileName)

const ipcApi = {
setStateAndDispatch: (s: AppState, d: Disp) => {
state = s
Expand All @@ -54,6 +57,7 @@ const ipcApi = {
, printFile: (cb: () => void) => ipcRenderer.on('printFile', cb)
, sendPlatform: (cb: (p: string) => void) => ipcRenderer.once('sendPlatform', (_e, p) => cb(p))
}
, readDataDirFile
}

contextBridge.exposeInMainWorld('ipcApi', ipcApi)
4 changes: 2 additions & 2 deletions src/components/MetaEditor/MetaEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Fragment } from 'react'
import { AppState } from '../../appState/AppState'
import { Action } from '../../appState/Action'
import { defaultVars } from '../../renderPreview/templates/getCss'
import { defaultVars, stripSurroundingStyleTags } from '../../renderPreview/templates/getCss'
import { ColorPicker } from '../ColorPicker/ColorPicker'

import back from './back.svg'
Expand Down Expand Up @@ -164,7 +164,7 @@ const layoutKvs: Kv[] = [{
name: 'header-includes'
, label: 'Include CSS'
, type: 'textarea'
, onLoad: s => s.startsWith('<style>\n') && s.endsWith('\n</style>') ? s.slice(8, -9) : s
, onLoad: stripSurroundingStyleTags
, onDone: s => `<style>\n${s}\n</style>`
, placeholder: `blockquote {
font-style: italic;
Expand Down
4 changes: 2 additions & 2 deletions src/renderPreview/renderPreviewImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const renderAndSwap = async (
export const renderPlain = async (doc: Doc, previewDiv: HTMLDivElement): Promise<Window> => {
const { contentWindow } = await setupSingleFrame(previewDiv);
const content = [
'<style>', getCss(doc), '</style>'
'<style>', await getCss(doc), '</style>'
, doc.meta['header-includes']
, doc.html
].join('')
Expand Down Expand Up @@ -156,7 +156,7 @@ const pagedjsStyleEl = createStyleEl(`
export const renderPaged = async (doc: Doc, previewDiv: HTMLDivElement): Promise<Window> => {
return renderAndSwap(previewDiv, async frameWindow => {

const cssStr = getCss(doc)
const cssStr = await getCss(doc)
, metaHtml = doc.meta['header-includes']
, content = doc.html
, frameHead = frameWindow.document.head
Expand Down
26 changes: 24 additions & 2 deletions src/renderPreview/templates/getCss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,27 @@ const template = parseToTemplate(styles)

export const defaultVars = extractDefaultVars(template)

export const getCss = (doc: Doc) =>
interpolateTemplate(template, doc.meta)
let headerIncludes = ''
let docType: string | undefined
const getHeaderIncludesCss = async (doc: Doc): Promise<string> => {
let newDocType = doc.meta.type
if (typeof newDocType !== 'string') {
newDocType = 'default'
}
if (newDocType !== docType && window.ipcApi) {
// cache css
docType = newDocType
const meta = await window.ipcApi.readDataDirFile(docType + '.yaml')
const field = meta?.['header-includes']
headerIncludes = typeof field === 'string'
? stripSurroundingStyleTags(field)
: ''
}
return headerIncludes
}

export const getCss = async (doc: Doc): Promise<string> =>
interpolateTemplate(template, doc.meta) + (await getHeaderIncludesCss(doc))

export const stripSurroundingStyleTags = (s: string): string =>
s.startsWith('<style>\n') && s.endsWith('\n</style>') ? s.slice(8, -9) : s

0 comments on commit f55140b

Please sign in to comment.