From f55140b48deeec71941b306d7750a00fc033df31 Mon Sep 17 00:00:00 2001 From: mb21 Date: Tue, 1 Jun 2021 21:31:53 +0200 Subject: [PATCH] read out header-includes from PanWriterUserData/{document-type}.yaml --- README.md | 11 +++++++ electron/dataDir.ts | 28 ++++++++++++++++++ electron/ipc.ts | 6 ++++ electron/pandoc/export.ts | 37 ++++-------------------- electron/preload.ts | 6 +++- src/components/MetaEditor/MetaEditor.tsx | 4 +-- src/renderPreview/renderPreviewImpl.ts | 4 +-- src/renderPreview/templates/getCss.ts | 26 +++++++++++++++-- 8 files changed, 84 insertions(+), 38 deletions(-) create mode 100644 electron/dataDir.ts diff --git a/README.md b/README.md index d9753c7..71933b3 100644 --- a/README.md +++ b/README.md @@ -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: |- + +``` + ### 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. diff --git a/electron/dataDir.ts b/electron/dataDir.ts new file mode 100644 index 0000000..3022a59 --- /dev/null +++ b/electron/dataDir.ts @@ -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 ] + } +} diff --git a/electron/ipc.ts b/electron/ipc.ts index 85b9e71..daeebfb 100644 --- a/electron/ipc.ts +++ b/electron/ipc.ts @@ -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 => { diff --git a/electron/pandoc/export.ts b/electron/pandoc/export.ts index 6048748..321c32c 100644 --- a/electron/pandoc/export.ts +++ b/electron/pandoc/export.ts @@ -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[] = []; diff --git a/electron/preload.ts b/electron/preload.ts index 752fe30..c1732f3 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -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 => + 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) diff --git a/src/components/MetaEditor/MetaEditor.tsx b/src/components/MetaEditor/MetaEditor.tsx index a33ce61..c0245e5 100644 --- a/src/components/MetaEditor/MetaEditor.tsx +++ b/src/components/MetaEditor/MetaEditor.tsx @@ -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('') ? s.slice(8, -9) : s +, onLoad: stripSurroundingStyleTags , onDone: s => `` , placeholder: `blockquote { font-style: italic; diff --git a/src/renderPreview/renderPreviewImpl.ts b/src/renderPreview/renderPreviewImpl.ts index ac9e54a..040ac1e 100644 --- a/src/renderPreview/renderPreviewImpl.ts +++ b/src/renderPreview/renderPreviewImpl.ts @@ -114,7 +114,7 @@ const renderAndSwap = async ( export const renderPlain = async (doc: Doc, previewDiv: HTMLDivElement): Promise => { const { contentWindow } = await setupSingleFrame(previewDiv); const content = [ - '' + '' , doc.meta['header-includes'] , doc.html ].join('') @@ -156,7 +156,7 @@ const pagedjsStyleEl = createStyleEl(` export const renderPaged = async (doc: Doc, previewDiv: HTMLDivElement): Promise => { 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 diff --git a/src/renderPreview/templates/getCss.ts b/src/renderPreview/templates/getCss.ts index 320307a..81052f6 100644 --- a/src/renderPreview/templates/getCss.ts +++ b/src/renderPreview/templates/getCss.ts @@ -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 => { + 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 => + interpolateTemplate(template, doc.meta) + (await getHeaderIncludesCss(doc)) + +export const stripSurroundingStyleTags = (s: string): string => + s.startsWith('') ? s.slice(8, -9) : s