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
mb21 committed Jun 5, 2021

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent fc19a8a commit f55140b
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
@@ -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.
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.
@@ -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> => {
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;
@@ -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
@@ -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

@@ -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[] = [];
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
@@ -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
@@ -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'
@@ -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;
4 changes: 2 additions & 2 deletions src/renderPreview/renderPreviewImpl.ts
Original file line number Diff line number Diff line change
@@ -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('')
@@ -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
26 changes: 24 additions & 2 deletions src/renderPreview/templates/getCss.ts
Original file line number Diff line number Diff line change
@@ -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.