mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 06:30:15 +08:00
Export with options (#1511)
* Export with options * Fix function names and add documentation * Narrow scrollbar
This commit is contained in:
parent
e18ad566d5
commit
e5dc8f1540
@ -51,7 +51,7 @@ const rendererConfig = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /(theme\-chalk(?:\/|\\)index|katex|github\-markdown|prism[\-a-z]*|\.theme)\.css$/,
|
test: /(theme\-chalk(?:\/|\\)index|katex|github\-markdown|prism[\-a-z]*|\.theme|headerFooterStyle)\.css$/,
|
||||||
use: [
|
use: [
|
||||||
'to-string-loader',
|
'to-string-loader',
|
||||||
'css-loader'
|
'css-loader'
|
||||||
@ -59,7 +59,7 @@ const rendererConfig = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
exclude: /(theme\-chalk(?:\/|\\)index|katex|github\-markdown|prism[\-a-z]*|\.theme)\.css$/,
|
exclude: /(theme\-chalk(?:\/|\\)index|katex|github\-markdown|prism[\-a-z]*|\.theme|headerFooterStyle)\.css$/,
|
||||||
use: [
|
use: [
|
||||||
proMode ? MiniCssExtractPlugin.loader : 'style-loader',
|
proMode ? MiniCssExtractPlugin.loader : 'style-loader',
|
||||||
{ loader: 'css-loader', options: { importLoaders: 1 } },
|
{ loader: 'css-loader', options: { importLoaders: 1 } },
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs-extra'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { BrowserWindow, dialog, ipcMain, shell } from 'electron'
|
import { BrowserWindow, dialog, ipcMain, shell } from 'electron'
|
||||||
import log from 'electron-log'
|
import log from 'electron-log'
|
||||||
@ -14,32 +14,53 @@ import pandoc from '../../utils/pandoc'
|
|||||||
// the renderer should communicate only with the editor window for file relevant stuff.
|
// the renderer should communicate only with the editor window for file relevant stuff.
|
||||||
// E.g. "mt::save-tabs" --> "mt::window-save-tabs$wid:<windowId>"
|
// E.g. "mt::save-tabs" --> "mt::window-save-tabs$wid:<windowId>"
|
||||||
|
|
||||||
|
const getPdfPageOptions = options => {
|
||||||
|
if (!options) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pageSize, pageSizeWidth, pageSizeHeight, isLandscape } = options
|
||||||
|
if (pageSize === 'custom' && pageSizeWidth && pageSizeHeight) {
|
||||||
|
return {
|
||||||
|
// Note: mm to microns
|
||||||
|
pageSize: { height: pageSizeHeight * 1000, width: pageSizeWidth * 1000 },
|
||||||
|
landscape: !!isLandscape
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return { pageSize, landscape: !!isLandscape }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle the export response from renderer process.
|
// Handle the export response from renderer process.
|
||||||
const handleResponseForExport = async (e, { type, content, pathname, markdown }) => {
|
const handleResponseForExport = async (e, { type, content, pathname, title, pageOptions }) => {
|
||||||
const win = BrowserWindow.fromWebContents(e.sender)
|
const win = BrowserWindow.fromWebContents(e.sender)
|
||||||
const extension = EXTENSION_HASN[type]
|
const extension = EXTENSION_HASN[type]
|
||||||
const dirname = pathname ? path.dirname(pathname) : getPath('documents')
|
const dirname = pathname ? path.dirname(pathname) : getPath('documents')
|
||||||
let nakedFilename = getRecommendTitleFromMarkdownString(markdown)
|
let nakedFilename = title
|
||||||
if (!nakedFilename) {
|
if (!nakedFilename) {
|
||||||
nakedFilename = pathname ? path.basename(pathname, '.md') : 'Untitled'
|
nakedFilename = pathname ? path.basename(pathname, '.md') : 'Untitled'
|
||||||
}
|
}
|
||||||
const defaultPath = path.join(dirname, `${nakedFilename}${extension}`)
|
|
||||||
|
|
||||||
|
const defaultPath = path.join(dirname, `${nakedFilename}${extension}`)
|
||||||
const { filePath, canceled } = await dialog.showSaveDialog(win, {
|
const { filePath, canceled } = await dialog.showSaveDialog(win, {
|
||||||
defaultPath
|
defaultPath
|
||||||
})
|
})
|
||||||
|
|
||||||
if (filePath && !canceled) {
|
if (filePath && !canceled) {
|
||||||
let data = content
|
|
||||||
try {
|
try {
|
||||||
if (!content && type === 'pdf') {
|
if (type === 'pdf') {
|
||||||
data = await win.webContents.printToPDF({ printBackground: true })
|
const options = { printBackground: true }
|
||||||
|
Object.assign(options, getPdfPageOptions(pageOptions))
|
||||||
|
const data = await win.webContents.printToPDF(options)
|
||||||
removePrintServiceFromWindow(win)
|
removePrintServiceFromWindow(win)
|
||||||
|
await writeFile(filePath, data, extension, {})
|
||||||
|
} else {
|
||||||
|
if (!content) {
|
||||||
|
throw new Error('No HTML content found.')
|
||||||
|
}
|
||||||
|
await writeFile(filePath, content, extension, 'utf8')
|
||||||
}
|
}
|
||||||
if (data) {
|
|
||||||
await writeFile(filePath, data, extension)
|
|
||||||
win.webContents.send('AGANI::export-success', { type, filePath })
|
win.webContents.send('AGANI::export-success', { type, filePath })
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(err)
|
log.error(err)
|
||||||
const ERROR_MSG = err.message || `Error happened when export ${filePath}`
|
const ERROR_MSG = err.message || `Error happened when export ${filePath}`
|
||||||
@ -399,7 +420,7 @@ ipcMain.on('AGANI::format-link-click', (e, { data, dirname }) => {
|
|||||||
// --- menu -------------------------------------
|
// --- menu -------------------------------------
|
||||||
|
|
||||||
export const exportFile = (win, type) => {
|
export const exportFile = (win, type) => {
|
||||||
win.webContents.send('AGANI::export', { type })
|
win.webContents.send('mt::show-export-dialog', type)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const importFile = async win => {
|
export const importFile = async win => {
|
||||||
@ -423,7 +444,16 @@ export const importFile = async win => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const print = win => {
|
export const print = win => {
|
||||||
win.webContents.send('AGANI::print')
|
// See GH#749, Electron#16085 and Electron#17523.
|
||||||
|
dialog.showMessageBox(win, {
|
||||||
|
type: 'info',
|
||||||
|
buttons: ['OK'],
|
||||||
|
defaultId: 0,
|
||||||
|
noLink: true,
|
||||||
|
message: 'Printing doesn\'t work',
|
||||||
|
detail: 'Printing is disabled due to an Electron upstream issue. Please export the document as PDF and print the PDF file. We apologize for the inconvenience!'
|
||||||
|
})
|
||||||
|
// win.webContents.send('mt::show-export-dialog', 'print')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const openFile = async win => {
|
export const openFile = async win => {
|
||||||
|
99
src/muya/lib/assets/styles/headerFooterStyle.css
Normal file
99
src/muya/lib/assets/styles/headerFooterStyle.css
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
:root {
|
||||||
|
--footerHeaderBorderColor: #1c1c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.page-container,
|
||||||
|
table.page-container > thead,
|
||||||
|
table.page-container > tfoot,
|
||||||
|
table.page-container > tbody,
|
||||||
|
table.page-container > thead > tr,
|
||||||
|
table.page-container > thead > tr > th,
|
||||||
|
table.page-container > thead > tr > td,
|
||||||
|
table.page-container > tbody > tr,
|
||||||
|
table.page-container > tbody > tr > th,
|
||||||
|
table.page-container > tbody > tr > td,
|
||||||
|
table.page-container > tfoot > tr,
|
||||||
|
table.page-container > tfoot > tr > th,
|
||||||
|
table.page-container > tfoot > tr > td {
|
||||||
|
position: relative !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header .hf-container,
|
||||||
|
.page-footer-fake .hf-container,
|
||||||
|
.page-footer .hf-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.75em;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: table-header-group;
|
||||||
|
}
|
||||||
|
.page-header .hf-container {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.page-header.styled .hf-container {
|
||||||
|
padding-bottom: 1px;
|
||||||
|
border-bottom: 1px solid var(--footerHeaderBorderColor);
|
||||||
|
}
|
||||||
|
.page-header .hf-container > div {
|
||||||
|
flex: 1;
|
||||||
|
max-height: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.page-header .header-content-left {
|
||||||
|
text-align: left;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.page-header .header-content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.page-header .header-content-right {
|
||||||
|
text-align: right;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
.page-header.single .header-content-left,
|
||||||
|
.page-header.single .header-content-right {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-footer-fake {
|
||||||
|
display: table-footer-group;
|
||||||
|
}
|
||||||
|
.page-footer-fake .hf-container {
|
||||||
|
margin-top: 16px;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.page-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.page-footer.styled .hf-container {
|
||||||
|
padding-top: 1px;
|
||||||
|
border-top: 1px solid var(--footerHeaderBorderColor);
|
||||||
|
}
|
||||||
|
.page-footer .hf-container > div {
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.page-footer .footer-content-left {
|
||||||
|
text-align: left;
|
||||||
|
margin-right: 14px;
|
||||||
|
}
|
||||||
|
.page-footer .footer-content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.page-footer .footer-content-right {
|
||||||
|
text-align: right;
|
||||||
|
margin-left: 14px;
|
||||||
|
}
|
||||||
|
.page-footer.single .footer-content-left,
|
||||||
|
.page-footer.single .footer-content-right {
|
||||||
|
display: none;
|
||||||
|
}
|
@ -273,7 +273,6 @@ export const MUYA_DEFAULT_OPTION = {
|
|||||||
// 'mermaid': `graph LR;\nYou-->|Mark Text|Me;`
|
// 'mermaid': `graph LR;\nYou-->|Mark Text|Me;`
|
||||||
// }
|
// }
|
||||||
|
|
||||||
export const isInElectron = window && window.process && window.process.type === 'renderer'
|
|
||||||
export const isOsx = window && window.navigator && /Mac/.test(window.navigator.platform)
|
export const isOsx = window && window.navigator && /Mac/.test(window.navigator.platform)
|
||||||
export const isWin = window && window.navigator.userAgent && /win32|wow32|win64|wow64/i.test(window.navigator.userAgent)
|
export const isWin = window && window.navigator.userAgent && /win32|wow32|win64|wow64/i.test(window.navigator.userAgent)
|
||||||
// http[s] (domain or IPv4 or localhost or IPv6) [port] /not-white-space
|
// http[s] (domain or IPv4 or localhost or IPv6) [port] /not-white-space
|
||||||
|
@ -145,9 +145,9 @@ class Muya {
|
|||||||
return this.contentState.history.clearHistory()
|
return this.contentState.history.clearHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
exportStyledHTML (title = '', printOptimization = false, extraCss = '') {
|
exportStyledHTML (options) {
|
||||||
const { markdown } = this
|
const { markdown } = this
|
||||||
return new ExportHtml(markdown, this).generate(title, printOptimization, extraCss)
|
return new ExportHtml(markdown, this).generate(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
exportHtml () {
|
exportHtml () {
|
||||||
|
@ -5,6 +5,7 @@ import loadRenderer from '../renderers'
|
|||||||
import githubMarkdownCss from 'github-markdown-css/github-markdown.css'
|
import githubMarkdownCss from 'github-markdown-css/github-markdown.css'
|
||||||
import highlightCss from 'prismjs/themes/prism.css'
|
import highlightCss from 'prismjs/themes/prism.css'
|
||||||
import katexCss from 'katex/dist/katex.css'
|
import katexCss from 'katex/dist/katex.css'
|
||||||
|
import footerHeaderCss from '../assets/styles/headerFooterStyle.css'
|
||||||
import { EXPORT_DOMPURIFY_CONFIG } from '../config'
|
import { EXPORT_DOMPURIFY_CONFIG } from '../config'
|
||||||
import { sanitize, unescapeHtml } from '../utils'
|
import { sanitize, unescapeHtml } from '../utils'
|
||||||
import { validEmoji } from '../ui/emojis'
|
import { validEmoji } from '../ui/emojis'
|
||||||
@ -131,16 +132,20 @@ class ExportHtml {
|
|||||||
},
|
},
|
||||||
mathRenderer: this.mathRenderer
|
mathRenderer: this.mathRenderer
|
||||||
})
|
})
|
||||||
|
|
||||||
html = sanitize(html, EXPORT_DOMPURIFY_CONFIG)
|
html = sanitize(html, EXPORT_DOMPURIFY_CONFIG)
|
||||||
|
|
||||||
const exportContainer = this.exportContainer = document.createElement('div')
|
const exportContainer = this.exportContainer = document.createElement('div')
|
||||||
exportContainer.classList.add('ag-render-container')
|
exportContainer.classList.add('ag-render-container')
|
||||||
exportContainer.innerHTML = html
|
exportContainer.innerHTML = html
|
||||||
document.body.appendChild(exportContainer)
|
document.body.appendChild(exportContainer)
|
||||||
|
|
||||||
// render only render the light theme of mermaid and diragram...
|
// render only render the light theme of mermaid and diragram...
|
||||||
await this.renderMermaid()
|
await this.renderMermaid()
|
||||||
await this.renderDiagram()
|
await this.renderDiagram()
|
||||||
let result = exportContainer.innerHTML
|
let result = exportContainer.innerHTML
|
||||||
exportContainer.remove()
|
exportContainer.remove()
|
||||||
|
|
||||||
// hack to add arrow marker to output html
|
// hack to add arrow marker to output html
|
||||||
const pathes = document.querySelectorAll('path[id^=raphael-marker-]')
|
const pathes = document.querySelectorAll('path[id^=raphael-marker-]')
|
||||||
const def = '<defs style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">'
|
const def = '<defs style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">'
|
||||||
@ -151,6 +156,7 @@ class ExportHtml {
|
|||||||
}
|
}
|
||||||
return `${def}${str}`
|
return `${def}${str}`
|
||||||
})
|
})
|
||||||
|
|
||||||
this.exportContainer = null
|
this.exportContainer = null
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@ -158,20 +164,25 @@ class ExportHtml {
|
|||||||
/**
|
/**
|
||||||
* Get HTML with style
|
* Get HTML with style
|
||||||
*
|
*
|
||||||
* @param {*} title Page title
|
* @param {*} options Document options
|
||||||
* @param {*} printOptimization Optimize HTML and CSS for printing
|
|
||||||
*/
|
*/
|
||||||
async generate (title = '', printOptimization = false, extraCss = '') {
|
async generate (options) {
|
||||||
|
const { printOptimization } = options
|
||||||
|
|
||||||
// WORKAROUND: Hide Prism.js style when exporting or printing. Otherwise the background color is white in the dark theme.
|
// WORKAROUND: Hide Prism.js style when exporting or printing. Otherwise the background color is white in the dark theme.
|
||||||
const highlightCssStyle = printOptimization ? `@media print { ${highlightCss} }` : highlightCss
|
const highlightCssStyle = printOptimization ? `@media print { ${highlightCss} }` : highlightCss
|
||||||
const html = await this.renderHtml()
|
const html = this._prepareHtml(await this.renderHtml(), options)
|
||||||
const katexCssStyle = this.mathRendererCalled ? katexCss : ''
|
const katexCssStyle = this.mathRendererCalled ? katexCss : ''
|
||||||
|
this.mathRendererCalled = false
|
||||||
|
|
||||||
|
// `extraCss` may changed in the mean time.
|
||||||
|
const { title, extraCss } = options
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>${title}</title>
|
<title>${sanitize(title, EXPORT_DOMPURIFY_CONFIG)}</title>
|
||||||
<style>
|
<style>
|
||||||
${githubMarkdownCss}
|
${githubMarkdownCss}
|
||||||
</style>
|
</style>
|
||||||
@ -189,6 +200,28 @@ class ExportHtml {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 45px;
|
padding: 45px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media not print {
|
||||||
|
.markdown-body {
|
||||||
|
padding: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.markdown-body {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hf-container {
|
||||||
|
color: #24292e;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .highlight pre,
|
||||||
|
.markdown-body pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
.markdown-body table {
|
.markdown-body table {
|
||||||
display: table;
|
display: table;
|
||||||
}
|
}
|
||||||
@ -202,12 +235,6 @@ class ExportHtml {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@media (max-width: 767px) {
|
|
||||||
.markdown-body {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body ol ol,
|
.markdown-body ol ol,
|
||||||
.markdown-body ul ol {
|
.markdown-body ul ol {
|
||||||
list-style-type: decimal;
|
list-style-type: decimal;
|
||||||
@ -222,12 +249,105 @@ class ExportHtml {
|
|||||||
<style>${extraCss}</style>
|
<style>${extraCss}</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<article class="markdown-body">
|
|
||||||
${html}
|
${html}
|
||||||
</article>
|
|
||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*
|
||||||
|
* @param {string} html The converted HTML text.
|
||||||
|
* @param {*} options The export options.
|
||||||
|
*/
|
||||||
|
_prepareHtml (html, options) {
|
||||||
|
const { header, footer } = options
|
||||||
|
const appendHeaderFooter = !!header || !!footer
|
||||||
|
if (!appendHeaderFooter) {
|
||||||
|
return createMarkdownArticle(html)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.extraCss) {
|
||||||
|
options.extraCss = footerHeaderCss
|
||||||
|
} else {
|
||||||
|
options.extraCss = footerHeaderCss + options.extraCss
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = HF_TABLE_START
|
||||||
|
if (header) {
|
||||||
|
output += createTableHeader(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (footer) {
|
||||||
|
output += HF_TABLE_FOOTER()
|
||||||
|
output = createRealFooter(options) + output
|
||||||
|
}
|
||||||
|
|
||||||
|
output = output + createTableBody(html) + HF_TABLE_END
|
||||||
|
return sanitize(output, EXPORT_DOMPURIFY_CONFIG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variables and function to generate the header and footer.
|
||||||
|
const HF_TABLE_START = '<table class="page-container">'
|
||||||
|
const createTableBody = html => {
|
||||||
|
return `<tbody><tr><td>
|
||||||
|
<div class="main-container">
|
||||||
|
${createMarkdownArticle(html)}
|
||||||
|
</div>
|
||||||
|
</td></tr></tbody>`
|
||||||
|
}
|
||||||
|
const HF_TABLE_END = '</table>'
|
||||||
|
|
||||||
|
/// The header at is shown at the top.
|
||||||
|
const createTableHeader = options => {
|
||||||
|
const { header, headerFooterStyled } = options
|
||||||
|
const { type, left, center, right } = header
|
||||||
|
let headerClass = type === 1 ? 'single' : ''
|
||||||
|
headerClass += getHeaderFooterStyledClass(headerFooterStyled)
|
||||||
|
return `<thead class="page-header ${headerClass}"><tr><th>
|
||||||
|
<div class="hf-container">
|
||||||
|
<div class="header-content-left">${left}</div>
|
||||||
|
<div class="header-content">${center}</div>
|
||||||
|
<div class="header-content-right">${right}</div>
|
||||||
|
</div>
|
||||||
|
</th></tr></thead>`
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fake footer to reserve space.
|
||||||
|
const HF_TABLE_FOOTER = `<tfoot class="page-footer-fake"><tr><td>
|
||||||
|
<div class="hf-container">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</td></tr></tfoot>`
|
||||||
|
|
||||||
|
/// The real footer at is shown at the bottom.
|
||||||
|
const createRealFooter = options => {
|
||||||
|
const { footer, headerFooterStyled } = options
|
||||||
|
const { type, left, center, right } = footer
|
||||||
|
let footerClass = type === 1 ? 'single' : ''
|
||||||
|
footerClass += getHeaderFooterStyledClass(headerFooterStyled)
|
||||||
|
return `<div class="page-footer ${footerClass}">
|
||||||
|
<div class="hf-container">
|
||||||
|
<div class="footer-content-left">${left}</div>
|
||||||
|
<div class="footer-content">${center}</div>
|
||||||
|
<div class="footer-content-right">${right}</div>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate the mardown article HTML.
|
||||||
|
const createMarkdownArticle = html => {
|
||||||
|
return `<article class="markdown-body">${html}</article>`
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the class whether a header/footer should be styled.
|
||||||
|
const getHeaderFooterStyledClass = value => {
|
||||||
|
if (value === undefined) {
|
||||||
|
// Prefer theme settings.
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return !value ? ' simple' : ' styled'
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ExportHtml
|
export default ExportHtml
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// DOTO: Don't use Node API in editor folder, remove `path` @jocs
|
|
||||||
import createDOMPurify from 'dompurify'
|
import createDOMPurify from 'dompurify'
|
||||||
import { isInElectron, URL_REG } from '../config'
|
import { URL_REG } from '../config'
|
||||||
|
|
||||||
|
const { sanitize: runSanitize } = createDOMPurify(window)
|
||||||
|
|
||||||
const ID_PREFIX = 'ag-'
|
const ID_PREFIX = 'ag-'
|
||||||
let id = 0
|
let id = 0
|
||||||
@ -263,7 +264,7 @@ export const getImageInfo = (src, baseUrl = window.DIRNAME) => {
|
|||||||
if (imageExtension) {
|
if (imageExtension) {
|
||||||
const isAbsoluteLocal = /^(?:\/|\\\\|[a-zA-Z]:\\).+/.test(src)
|
const isAbsoluteLocal = /^(?:\/|\\\\|[a-zA-Z]:\\).+/.test(src)
|
||||||
if (isUrl || (!isAbsoluteLocal && !baseUrl)) {
|
if (isUrl || (!isAbsoluteLocal && !baseUrl)) {
|
||||||
if (!isUrl && !baseUrl && isInElectron) {
|
if (!isUrl && !baseUrl) {
|
||||||
console.warn('"baseUrl" is not defined!')
|
console.warn('"baseUrl" is not defined!')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,14 +272,13 @@ export const getImageInfo = (src, baseUrl = window.DIRNAME) => {
|
|||||||
isUnknownType: false,
|
isUnknownType: false,
|
||||||
src
|
src
|
||||||
}
|
}
|
||||||
} else if (isInElectron) {
|
} else {
|
||||||
// Correct relative path on desktop. If we resolve a absolute path "path.resolve" doesn't do anything.
|
// Correct relative path on desktop. If we resolve a absolute path "path.resolve" doesn't do anything.
|
||||||
return {
|
return {
|
||||||
isUnknownType: false,
|
isUnknownType: false,
|
||||||
src: 'file://' + require('path').resolve(baseUrl, src)
|
src: 'file://' + require('path').resolve(baseUrl, src)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// else: Forbid the request due absolute or relative path in browser
|
|
||||||
} else if (isUrl && !imageExtension) {
|
} else if (isUrl && !imageExtension) {
|
||||||
// Assume it's a valid image and make a http request later
|
// Assume it's a valid image and make a http request later
|
||||||
return {
|
return {
|
||||||
@ -367,8 +367,7 @@ export const mixins = (constructor, ...object) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const sanitize = (html, options) => {
|
export const sanitize = (html, options) => {
|
||||||
const DOMPurify = createDOMPurify(window)
|
return runSanitize(escapeInBlockHtml(html), options)
|
||||||
return DOMPurify.sanitize(escapeInBlockHtml(html), options)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getParagraphReference = (ele, id) => {
|
export const getParagraphReference = (ele, id) => {
|
||||||
|
@ -26,7 +26,8 @@ body article.print-container {
|
|||||||
height: auto !important;
|
height: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body > div {
|
body > div,
|
||||||
|
body > svg {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
97
src/renderer/assets/themes/export/academic.theme.css
Normal file
97
src/renderer/assets/themes/export/academic.theme.css
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
/** Academic **/
|
||||||
|
|
||||||
|
/* DejaVu Serif --> 11pt */
|
||||||
|
/* Don't use "Noto Color Emoji" because it will result in PDF files with multiple MB and weird looking emojis. */
|
||||||
|
.hf-container,
|
||||||
|
.markdown-body {
|
||||||
|
font-family: Georgia,Palatino,"Palatino Linotype","Times New Roman","DejaVu Serif",serif,"Apple Color Emoji","Segoe UI Emoji";
|
||||||
|
font-size: 12pt;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: #212121;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--footerHeaderBorderColor: #1c1c1c;
|
||||||
|
}
|
||||||
|
.hf-container {
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
|
.page-header:not(.simple) .hf-container {
|
||||||
|
padding-bottom: 2px;
|
||||||
|
border-bottom: 1px solid var(--footerHeaderBorderColor);
|
||||||
|
}
|
||||||
|
.page-footer:not(.simple) .hf-container {
|
||||||
|
padding-top: 2px;
|
||||||
|
border-top: 1px solid var(--footerHeaderBorderColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1,
|
||||||
|
.markdown-body h2,
|
||||||
|
.markdown-body h3,
|
||||||
|
.markdown-body h4,
|
||||||
|
.markdown-body h5,
|
||||||
|
.markdown-body h6 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #282828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1 {
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
.markdown-body h2 {
|
||||||
|
font-size: 1.6em;
|
||||||
|
}
|
||||||
|
.markdown-body h3 {
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
.markdown-body h4,
|
||||||
|
.markdown-body h5 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
.markdown-body h6 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1,
|
||||||
|
.markdown-body h2 {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body a {
|
||||||
|
color: #212121;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body hr {
|
||||||
|
background-color: #b0b0b0;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body blockquote {
|
||||||
|
border: none;
|
||||||
|
color: #282828;
|
||||||
|
padding: 0 1em;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .highlight pre,
|
||||||
|
.markdown-body pre {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table td,
|
||||||
|
.markdown-body table th {
|
||||||
|
border: 1px solid #1a1a1a;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
.markdown-body table tr {
|
||||||
|
background: none;
|
||||||
|
border-top: 1px solid #1a1a1a;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.markdown-body table tr:nth-child(2n) {
|
||||||
|
background: none;
|
||||||
|
}
|
66
src/renderer/assets/themes/export/liber.theme.css
Normal file
66
src/renderer/assets/themes/export/liber.theme.css
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/** Liber **/
|
||||||
|
|
||||||
|
/* Don't use "Noto Color Emoji" because it will result in PDF files with multiple MB and weird looking emojis. */
|
||||||
|
.hf-container,
|
||||||
|
.markdown-body {
|
||||||
|
font-family: "Source Sans Pro","Segoe UI","Helvetica Neue","Open Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji";
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: #212121;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--footerHeaderBorderColor: #1c1c1c;
|
||||||
|
}
|
||||||
|
.hf-container {
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
|
.page-header:not(.simple) .hf-container {
|
||||||
|
padding-bottom: 2px;
|
||||||
|
border-bottom: 1px solid var(--footerHeaderBorderColor);
|
||||||
|
}
|
||||||
|
.page-footer:not(.simple) .hf-container {
|
||||||
|
padding-top: 2px;
|
||||||
|
border-top: 1px solid var(--footerHeaderBorderColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1,
|
||||||
|
.markdown-body h2,
|
||||||
|
.markdown-body h3,
|
||||||
|
.markdown-body h4,
|
||||||
|
.markdown-body h5,
|
||||||
|
.markdown-body h6 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #282828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1 {
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
.markdown-body h2 {
|
||||||
|
font-size: 1.6em;
|
||||||
|
}
|
||||||
|
.markdown-body h3 {
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
.markdown-body h4,
|
||||||
|
.markdown-body h5 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
.markdown-body h6 {
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1,
|
||||||
|
.markdown-body h2 {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .highlight pre,
|
||||||
|
.markdown-body pre {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
@ -100,6 +100,7 @@ import { offsetToWordCursor, validateLineCursor, SpellChecker } from '@/spellche
|
|||||||
import { isOsx, animatedScrollTo } from '@/util'
|
import { isOsx, animatedScrollTo } from '@/util'
|
||||||
import { moveImageToFolder, uploadImage } from '@/util/fileSystem'
|
import { moveImageToFolder, uploadImage } from '@/util/fileSystem'
|
||||||
import { guessClipboardFilePath } from '@/util/clipboard'
|
import { guessClipboardFilePath } from '@/util/clipboard'
|
||||||
|
import { getCssForOptions } from '@/util/pdf'
|
||||||
import { addCommonStyle, setEditorWidth } from '@/util/theme'
|
import { addCommonStyle, setEditorWidth } from '@/util/theme'
|
||||||
|
|
||||||
import 'muya/themes/default.css'
|
import 'muya/themes/default.css'
|
||||||
@ -114,9 +115,6 @@ export default {
|
|||||||
Search
|
Search
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
filename: {
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
markdown: String,
|
markdown: String,
|
||||||
cursor: Object,
|
cursor: Object,
|
||||||
textDirection: {
|
textDirection: {
|
||||||
@ -534,7 +532,6 @@ export default {
|
|||||||
bus.$on('deleteParagraph', this.handleParagraph)
|
bus.$on('deleteParagraph', this.handleParagraph)
|
||||||
bus.$on('insertParagraph', this.handleInsertParagraph)
|
bus.$on('insertParagraph', this.handleInsertParagraph)
|
||||||
bus.$on('scroll-to-header', this.scrollToHeader)
|
bus.$on('scroll-to-header', this.scrollToHeader)
|
||||||
bus.$on('print', this.handlePrint)
|
|
||||||
bus.$on('screenshot-captured', this.handleScreenShot)
|
bus.$on('screenshot-captured', this.handleScreenShot)
|
||||||
bus.$on('switch-spellchecker-language', this.switchSpellcheckLanguage)
|
bus.$on('switch-spellchecker-language', this.switchSpellcheckLanguage)
|
||||||
|
|
||||||
@ -878,27 +875,60 @@ export default {
|
|||||||
this.scrollToHighlight()
|
this.scrollToHighlight()
|
||||||
},
|
},
|
||||||
|
|
||||||
async handlePrint () {
|
async handleExport (options) {
|
||||||
// generate styled HTML with empty title and optimized for printing
|
const {
|
||||||
const html = await this.editor.exportStyledHTML('', true)
|
type,
|
||||||
this.printer.renderMarkdown(html, true)
|
header,
|
||||||
this.$store.dispatch('PRINT_RESPONSE')
|
footer,
|
||||||
},
|
headerFooterStyled,
|
||||||
|
htmlTitle
|
||||||
async handleExport (type) {
|
} = options
|
||||||
const markdown = this.editor.getMarkdown()
|
if (!/^pdf|print|styledHtml$/.test(type)) {
|
||||||
switch (type) {
|
throw new Error(`Invalid type to export: "${type}".`)
|
||||||
case 'styledHtml': {
|
|
||||||
const content = await this.editor.exportStyledHTML(this.filename)
|
|
||||||
this.$store.dispatch('EXPORT', { type, content, markdown })
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extraCss = getCssForOptions(options)
|
||||||
|
switch (type) {
|
||||||
|
case 'styledHtml': {
|
||||||
|
const content = await this.editor.exportStyledHTML({
|
||||||
|
title: htmlTitle || '',
|
||||||
|
printOptimization: false,
|
||||||
|
extraCss
|
||||||
|
})
|
||||||
|
this.$store.dispatch('EXPORT', { type, content })
|
||||||
|
break
|
||||||
|
}
|
||||||
case 'pdf': {
|
case 'pdf': {
|
||||||
// generate styled HTML with empty title and optimized for printing
|
// NOTE: We need to set page size via Electron.
|
||||||
const html = await this.editor.exportStyledHTML('', true)
|
const { pageSize, pageSizeWidth, pageSizeHeight, isLandscape } = options
|
||||||
|
const pageOptions = {
|
||||||
|
pageSize, pageSizeWidth, pageSizeHeight, isLandscape
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = await this.editor.exportStyledHTML({
|
||||||
|
title: '',
|
||||||
|
printOptimization: true,
|
||||||
|
extraCss,
|
||||||
|
header,
|
||||||
|
footer,
|
||||||
|
headerFooterStyled
|
||||||
|
})
|
||||||
this.printer.renderMarkdown(html, true)
|
this.printer.renderMarkdown(html, true)
|
||||||
this.$store.dispatch('EXPORT', { type, markdown })
|
this.$store.dispatch('EXPORT', { type, pageOptions })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'print': {
|
||||||
|
// NOTE: Print doesn't support page size or orientation.
|
||||||
|
const html = await this.editor.exportStyledHTML({
|
||||||
|
title: '',
|
||||||
|
printOptimization: true,
|
||||||
|
extraCss,
|
||||||
|
header,
|
||||||
|
footer,
|
||||||
|
headerFooterStyled
|
||||||
|
})
|
||||||
|
this.printer.renderMarkdown(html, true)
|
||||||
|
this.$store.dispatch('PRINT_RESPONSE')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1021,7 +1051,6 @@ export default {
|
|||||||
bus.$off('deleteParagraph', this.handleParagraph)
|
bus.$off('deleteParagraph', this.handleParagraph)
|
||||||
bus.$off('insertParagraph', this.handleInsertParagraph)
|
bus.$off('insertParagraph', this.handleInsertParagraph)
|
||||||
bus.$off('scroll-to-header', this.scrollToHeader)
|
bus.$off('scroll-to-header', this.scrollToHeader)
|
||||||
bus.$off('print', this.handlePrint)
|
|
||||||
bus.$off('screenshot-captured', this.handleScreenShot)
|
bus.$off('screenshot-captured', this.handleScreenShot)
|
||||||
bus.$off('switch-spellchecker-language', this.switchSpellcheckLanguage)
|
bus.$off('switch-spellchecker-language', this.switchSpellcheckLanguage)
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
<tabs v-show="showTabBar"></tabs>
|
<tabs v-show="showTabBar"></tabs>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<editor
|
<editor
|
||||||
:fileanme="filename"
|
|
||||||
:markdown="markdown"
|
:markdown="markdown"
|
||||||
:cursor="cursor"
|
:cursor="cursor"
|
||||||
:text-direction="textDirection"
|
:text-direction="textDirection"
|
||||||
@ -30,9 +29,6 @@ import TabNotifications from './notifications.vue'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
filename: {
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
markdown: {
|
markdown: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
61
src/renderer/components/exportSettings/exportOptions.js
Normal file
61
src/renderer/components/exportSettings/exportOptions.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
export const pageSizeList = [
|
||||||
|
{
|
||||||
|
label: 'A3 (297mm x 420mm)',
|
||||||
|
value: 'A3'
|
||||||
|
}, {
|
||||||
|
label: 'A4 (210mm x 297mm)',
|
||||||
|
value: 'A4'
|
||||||
|
}, {
|
||||||
|
label: 'A5 (148mm x 210mm)',
|
||||||
|
value: 'A5'
|
||||||
|
}, {
|
||||||
|
label: 'US Legal (8.5" x 13")',
|
||||||
|
value: 'Legal'
|
||||||
|
}, {
|
||||||
|
label: 'US Letter (8.5" x 11")',
|
||||||
|
value: 'Letter'
|
||||||
|
}, {
|
||||||
|
label: 'Tabloid (17" x 11")',
|
||||||
|
value: 'Tabloid'
|
||||||
|
}, {
|
||||||
|
label: 'Custom',
|
||||||
|
value: 'custom'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const headerFooterTypes = [
|
||||||
|
{
|
||||||
|
label: 'None',
|
||||||
|
value: 0
|
||||||
|
}, {
|
||||||
|
label: 'Single cell',
|
||||||
|
value: 1
|
||||||
|
}, {
|
||||||
|
label: 'Three cells',
|
||||||
|
value: 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const headerFooterStyles = [
|
||||||
|
{
|
||||||
|
label: 'Default',
|
||||||
|
value: 0
|
||||||
|
}, {
|
||||||
|
label: 'Simple',
|
||||||
|
value: 1
|
||||||
|
}, {
|
||||||
|
label: 'Styled',
|
||||||
|
value: 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const exportThemeList = [{
|
||||||
|
label: 'Academic',
|
||||||
|
value: 'academic'
|
||||||
|
}, {
|
||||||
|
label: 'GitHub (Default)',
|
||||||
|
value: 'default'
|
||||||
|
}, {
|
||||||
|
label: 'Liber',
|
||||||
|
value: 'liber'
|
||||||
|
}]
|
474
src/renderer/components/exportSettings/index.vue
Normal file
474
src/renderer/components/exportSettings/index.vue
Normal file
@ -0,0 +1,474 @@
|
|||||||
|
<template>
|
||||||
|
<div class="print-settings-dialog">
|
||||||
|
<el-dialog
|
||||||
|
:visible.sync="showExportSettingsDialog"
|
||||||
|
:show-close="false"
|
||||||
|
:modal="true"
|
||||||
|
custom-class="ag-dialog-table"
|
||||||
|
width="500px"
|
||||||
|
>
|
||||||
|
<h3>Export Options</h3>
|
||||||
|
<el-tabs v-model="activeName">
|
||||||
|
<el-tab-pane label="Info" name="info">
|
||||||
|
<span class="text">Please customize the page appearance and click on "export" to continue.</span>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="Page" name="page">
|
||||||
|
<!-- HTML -->
|
||||||
|
<div v-if="!isPrintable">
|
||||||
|
<text-box
|
||||||
|
description="The page title:"
|
||||||
|
:input="htmlTitle"
|
||||||
|
:emitTime="0"
|
||||||
|
:onChange="value => onSelectChange('htmlTitle', value)"
|
||||||
|
></text-box>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PDF/Print -->
|
||||||
|
<div v-if="isPrintable">
|
||||||
|
<div v-if="exportType === 'pdf'">
|
||||||
|
<cur-select
|
||||||
|
class="page-size-select"
|
||||||
|
description="Page size:"
|
||||||
|
:value="pageSize"
|
||||||
|
:options="pageSizeList"
|
||||||
|
:onChange="value => onSelectChange('pageSize', value)"
|
||||||
|
></cur-select>
|
||||||
|
<div v-if="pageSize === 'custom'" class="row">
|
||||||
|
<div>Width/Height in mm:</div>
|
||||||
|
<el-input-number v-model="pageSizeWidth" size="mini" controls-position="right" :min="100"></el-input-number>
|
||||||
|
<el-input-number v-model="pageSizeHeight" size="mini" controls-position="right" :min="100"></el-input-number>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<bool
|
||||||
|
description="Landscape orientation:"
|
||||||
|
:bool="isLandscape"
|
||||||
|
:onChange="value => onSelectChange('isLandscape', value)"
|
||||||
|
></bool>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="description">Page margin in mm:</div>
|
||||||
|
<div>
|
||||||
|
<div style="margin-bottom:2px;">Top/Bottom:</div>
|
||||||
|
<el-input-number v-model="pageMarginTop" size="mini" controls-position="right" :min="0" :max="100"></el-input-number>
|
||||||
|
<el-input-number v-model="pageMarginBottom" size="mini" controls-position="right" :min="0" :max="100"></el-input-number>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="margin-bottom:2px;">Left/Right:</div>
|
||||||
|
<el-input-number v-model="pageMarginLeft" size="mini" controls-position="right" :min="0" :max="100"></el-input-number>
|
||||||
|
<el-input-number v-model="pageMarginRight" size="mini" controls-position="right" :min="0" :max="100"></el-input-number>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="Style" name="style">
|
||||||
|
<bool
|
||||||
|
description="Overwrite theme font settings:"
|
||||||
|
:bool="fontSettingsOverwrite"
|
||||||
|
:onChange="value => onSelectChange('fontSettingsOverwrite', value)"
|
||||||
|
></bool>
|
||||||
|
<div v-if="fontSettingsOverwrite">
|
||||||
|
<font-text-box
|
||||||
|
description="Font family:"
|
||||||
|
:value="fontFamily"
|
||||||
|
:onChange="value => onSelectChange('fontFamily', value)"
|
||||||
|
></font-text-box>
|
||||||
|
<range
|
||||||
|
description="Font size"
|
||||||
|
:value="fontSize"
|
||||||
|
:min="8"
|
||||||
|
:max="32"
|
||||||
|
unit="px"
|
||||||
|
:step="1"
|
||||||
|
:onChange="value => onSelectChange('fontSize', value)"
|
||||||
|
></range>
|
||||||
|
<range
|
||||||
|
description="Line height"
|
||||||
|
:value="lineHeight"
|
||||||
|
:min="1.0"
|
||||||
|
:max="2.0"
|
||||||
|
:step="0.1"
|
||||||
|
:onChange="value => onSelectChange('lineHeight', value)"
|
||||||
|
></range>
|
||||||
|
</div>
|
||||||
|
<bool
|
||||||
|
description="Auto numbering headings:"
|
||||||
|
:bool="autoNumberingHeadings"
|
||||||
|
:onChange="value => onSelectChange('autoNumberingHeadings', value)"
|
||||||
|
></bool>
|
||||||
|
<bool
|
||||||
|
description="Show front matter:"
|
||||||
|
:bool="showFrontMatter"
|
||||||
|
:onChange="value => onSelectChange('showFrontMatter', value)"
|
||||||
|
></bool>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="Theme" name="theme">
|
||||||
|
<div class="text">You can change the document appearance by choosing a theme or create a handcrafted one.</div>
|
||||||
|
<!-- TODO(theme): Create "more" link to PDF theme documentation -->
|
||||||
|
<cur-select
|
||||||
|
description="Theme:"
|
||||||
|
:value="theme"
|
||||||
|
:options="themeList"
|
||||||
|
:onChange="value => onSelectChange('theme', value)"
|
||||||
|
></cur-select>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane v-if="isPrintable" label="Header & Footer" name="header">
|
||||||
|
<div class="text">The text appear on all pages if header and/or footer is defined.</div>
|
||||||
|
<cur-select
|
||||||
|
description="Header type:"
|
||||||
|
:value="headerType"
|
||||||
|
:options="headerFooterTypes"
|
||||||
|
:onChange="value => onSelectChange('headerType', value)"
|
||||||
|
></cur-select>
|
||||||
|
<text-box
|
||||||
|
v-if="headerType === 2"
|
||||||
|
description="The left header text:"
|
||||||
|
:input="headerTextLeft"
|
||||||
|
:emitTime="0"
|
||||||
|
:onChange="value => onSelectChange('headerTextLeft', value)"
|
||||||
|
></text-box>
|
||||||
|
<text-box
|
||||||
|
v-if="headerType !== 0"
|
||||||
|
description="The main header text:"
|
||||||
|
:input="headerTextCenter"
|
||||||
|
:emitTime="0"
|
||||||
|
:onChange="value => onSelectChange('headerTextCenter', value)"
|
||||||
|
></text-box>
|
||||||
|
<text-box
|
||||||
|
v-if="headerType === 2"
|
||||||
|
description="The right header text:"
|
||||||
|
:input="headerTextRight"
|
||||||
|
:emitTime="0"
|
||||||
|
:onChange="value => onSelectChange('headerTextRight', value)"
|
||||||
|
></text-box>
|
||||||
|
|
||||||
|
<cur-select
|
||||||
|
description="Footer type:"
|
||||||
|
:value="footerType"
|
||||||
|
:options="headerFooterTypes"
|
||||||
|
:onChange="value => onSelectChange('footerType', value)"
|
||||||
|
></cur-select>
|
||||||
|
<text-box
|
||||||
|
v-if="footerType === 2"
|
||||||
|
description="The left footer text:"
|
||||||
|
:input="footerTextLeft"
|
||||||
|
:emitTime="0"
|
||||||
|
:onChange="value => onSelectChange('footerTextLeft', value)"
|
||||||
|
></text-box>
|
||||||
|
<text-box
|
||||||
|
v-if="footerType !== 0"
|
||||||
|
description="The main footer text:"
|
||||||
|
:input="footerTextCenter"
|
||||||
|
:emitTime="0"
|
||||||
|
:onChange="value => onSelectChange('footerTextCenter', value)"
|
||||||
|
></text-box>
|
||||||
|
<text-box
|
||||||
|
v-if="footerType === 2"
|
||||||
|
description="The right footer text:"
|
||||||
|
:input="footerTextRight"
|
||||||
|
:emitTime="0"
|
||||||
|
:onChange="value => onSelectChange('footerTextRight', value)"
|
||||||
|
></text-box>
|
||||||
|
|
||||||
|
<bool
|
||||||
|
description="Customize style:"
|
||||||
|
:bool="headerFooterCustomize"
|
||||||
|
:onChange="value => onSelectChange('headerFooterCustomize', value)"
|
||||||
|
></bool>
|
||||||
|
|
||||||
|
<div v-if="headerFooterCustomize">
|
||||||
|
<bool
|
||||||
|
description="Allow styled header and footer:"
|
||||||
|
:bool="headerFooterStyled"
|
||||||
|
:onChange="value => onSelectChange('headerFooterStyled', value)"
|
||||||
|
></bool>
|
||||||
|
<range
|
||||||
|
description="Header and footer font size"
|
||||||
|
:value="headerFooterFontSize"
|
||||||
|
:min="8"
|
||||||
|
:max="20"
|
||||||
|
unit="px"
|
||||||
|
:step="1"
|
||||||
|
:onChange="value => onSelectChange('headerFooterFontSize', value)"
|
||||||
|
></range>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
<div class="button-controlls">
|
||||||
|
<button class="button-primary" @click="handleClicked">
|
||||||
|
Export...
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import fs from 'fs-extra'
|
||||||
|
import path from 'path'
|
||||||
|
import bus from '../../bus'
|
||||||
|
import Bool from '@/prefComponents/common/bool'
|
||||||
|
import CurSelect from '@/prefComponents/common/select'
|
||||||
|
import FontTextBox from '@/prefComponents/common/fontTextBox'
|
||||||
|
import Range from '@/prefComponents/common/range'
|
||||||
|
import TextBox from '@/prefComponents/common/textBox'
|
||||||
|
import {
|
||||||
|
pageSizeList,
|
||||||
|
headerFooterTypes,
|
||||||
|
headerFooterStyles,
|
||||||
|
exportThemeList
|
||||||
|
} from './exportOptions'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Bool,
|
||||||
|
CurSelect,
|
||||||
|
FontTextBox,
|
||||||
|
Range,
|
||||||
|
TextBox
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
this.exportType = ''
|
||||||
|
this.themesLoaded = false
|
||||||
|
this.pageSizeList = pageSizeList
|
||||||
|
this.headerFooterTypes = headerFooterTypes
|
||||||
|
this.headerFooterStyles = headerFooterStyles
|
||||||
|
return {
|
||||||
|
isPrintable: true,
|
||||||
|
showExportSettingsDialog: false,
|
||||||
|
activeName: 'info',
|
||||||
|
htmlTitle: '',
|
||||||
|
pageSize: 'A4',
|
||||||
|
pageSizeWidth: 210,
|
||||||
|
pageSizeHeight: 297,
|
||||||
|
isLandscape: false,
|
||||||
|
pageMarginTop: 20,
|
||||||
|
pageMarginRight: 15,
|
||||||
|
pageMarginBottom: 20,
|
||||||
|
pageMarginLeft: 15,
|
||||||
|
fontSettingsOverwrite: false,
|
||||||
|
fontFamily: 'Default',
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
autoNumberingHeadings: false,
|
||||||
|
showFrontMatter: false,
|
||||||
|
theme: 'default',
|
||||||
|
themeList: exportThemeList,
|
||||||
|
headerType: 0,
|
||||||
|
headerTextLeft: '',
|
||||||
|
headerTextCenter: '',
|
||||||
|
headerTextRight: '',
|
||||||
|
footerType: 0,
|
||||||
|
footerTextLeft: '',
|
||||||
|
footerTextCenter: '',
|
||||||
|
footerTextRight: '',
|
||||||
|
headerFooterCustomize: false,
|
||||||
|
headerFooterStyled: true,
|
||||||
|
headerFooterFontSize: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
})
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
bus.$on('showExportDialog', this.showDialog)
|
||||||
|
},
|
||||||
|
beforeDestroy () {
|
||||||
|
bus.$off('showExportDialog', this.showDialog)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showDialog (type) {
|
||||||
|
this.exportType = type
|
||||||
|
this.isPrintable = type !== 'styledHtml'
|
||||||
|
if (!this.isPrintable && (this.activeName === 'header' || this.activeName === 'page')) {
|
||||||
|
this.activeName = 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showExportSettingsDialog = true
|
||||||
|
bus.$emit('editor-blur')
|
||||||
|
|
||||||
|
if (!this.themesLoaded) {
|
||||||
|
this.themesLoaded = true
|
||||||
|
this.loadThemesFromDisk()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleClicked () {
|
||||||
|
const {
|
||||||
|
exportType,
|
||||||
|
isPrintable,
|
||||||
|
htmlTitle,
|
||||||
|
pageSize,
|
||||||
|
pageSizeWidth,
|
||||||
|
pageSizeHeight,
|
||||||
|
isLandscape,
|
||||||
|
pageMarginTop,
|
||||||
|
pageMarginRight,
|
||||||
|
pageMarginBottom,
|
||||||
|
pageMarginLeft,
|
||||||
|
fontSettingsOverwrite,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
lineHeight,
|
||||||
|
autoNumberingHeadings,
|
||||||
|
showFrontMatter,
|
||||||
|
theme,
|
||||||
|
headerType,
|
||||||
|
headerTextLeft,
|
||||||
|
headerTextCenter,
|
||||||
|
headerTextRight,
|
||||||
|
footerType,
|
||||||
|
footerTextLeft,
|
||||||
|
footerTextCenter,
|
||||||
|
footerTextRight,
|
||||||
|
headerFooterCustomize,
|
||||||
|
headerFooterStyled,
|
||||||
|
headerFooterFontSize
|
||||||
|
} = this
|
||||||
|
const options = {
|
||||||
|
type: exportType,
|
||||||
|
pageSize,
|
||||||
|
pageSizeWidth,
|
||||||
|
pageSizeHeight,
|
||||||
|
isLandscape,
|
||||||
|
pageMarginTop,
|
||||||
|
pageMarginRight,
|
||||||
|
pageMarginBottom,
|
||||||
|
pageMarginLeft,
|
||||||
|
autoNumberingHeadings,
|
||||||
|
showFrontMatter,
|
||||||
|
theme: theme === 'default' ? null : theme
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPrintable) {
|
||||||
|
options.htmlTitle = htmlTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fontSettingsOverwrite) {
|
||||||
|
Object.assign(options, {
|
||||||
|
fontSize,
|
||||||
|
lineHeight,
|
||||||
|
fontFamily: fontFamily === 'Default' ? null : fontFamily
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headerType !== 0) {
|
||||||
|
Object.assign(options, {
|
||||||
|
header: {
|
||||||
|
type: headerType,
|
||||||
|
left: headerTextLeft,
|
||||||
|
center: headerTextCenter,
|
||||||
|
right: headerTextRight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (footerType !== 0) {
|
||||||
|
Object.assign(options, {
|
||||||
|
footer: {
|
||||||
|
type: footerType,
|
||||||
|
left: footerTextLeft,
|
||||||
|
center: footerTextCenter,
|
||||||
|
right: footerTextRight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headerFooterCustomize) {
|
||||||
|
Object.assign(options, {
|
||||||
|
headerFooterStyled,
|
||||||
|
headerFooterFontSize
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showExportSettingsDialog = false
|
||||||
|
bus.$emit('export', options)
|
||||||
|
},
|
||||||
|
onSelectChange (key, value) {
|
||||||
|
this[key] = value
|
||||||
|
},
|
||||||
|
loadThemesFromDisk () {
|
||||||
|
const { userDataPath } = global.marktext.paths
|
||||||
|
const themeDir = path.join(userDataPath, 'themes/export')
|
||||||
|
|
||||||
|
// Search for dictionaries on filesystem.
|
||||||
|
if (fs.existsSync(themeDir) && fs.lstatSync(themeDir).isDirectory()) {
|
||||||
|
fs.readdirSync(themeDir).forEach(async filename => {
|
||||||
|
const fullname = path.join(themeDir, filename)
|
||||||
|
if (/.+\.css$/i.test(filename) && fs.lstatSync(fullname).isFile()) {
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(fullname, 'utf8')
|
||||||
|
|
||||||
|
// Match comment with theme name in first line only.
|
||||||
|
const match = content.match(/^(?:\/\*+[ \t]*([A-z0-9 -]+)[ \t]*(?:\*+\/|[\n\r])?)/)
|
||||||
|
|
||||||
|
let label
|
||||||
|
if (match && match[1]) {
|
||||||
|
label = match[1]
|
||||||
|
} else {
|
||||||
|
label = filename
|
||||||
|
}
|
||||||
|
|
||||||
|
this.themeList.push({
|
||||||
|
value: filename,
|
||||||
|
label
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error('loadThemesFromDisk failed:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.print-settings-dialog {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-controlls {
|
||||||
|
margin-top: 8px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tab-pane section:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
.print-settings-dialog #pane-header .pref-text-box-item .el-input {
|
||||||
|
width: 90% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-settings-dialog .el-dialog__body {
|
||||||
|
padding: 0 20px 20px 20px;
|
||||||
|
}
|
||||||
|
.print-settings-dialog .pref-select-item .el-select {
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
.print-settings-dialog .el-tabs__content {
|
||||||
|
max-height: 350px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.print-settings-dialog .el-tabs__content::-webkit-scrollbar:vertical {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -20,7 +20,6 @@
|
|||||||
<editor-with-tabs
|
<editor-with-tabs
|
||||||
v-if="hasCurrentFile && init"
|
v-if="hasCurrentFile && init"
|
||||||
:markdown="markdown"
|
:markdown="markdown"
|
||||||
:filename="filename"
|
|
||||||
:cursor="cursor"
|
:cursor="cursor"
|
||||||
:source-code="sourceCode"
|
:source-code="sourceCode"
|
||||||
:show-tab-bar="showTabBar"
|
:show-tab-bar="showTabBar"
|
||||||
@ -29,6 +28,7 @@
|
|||||||
></editor-with-tabs>
|
></editor-with-tabs>
|
||||||
<aidou></aidou>
|
<aidou></aidou>
|
||||||
<about-dialog></about-dialog>
|
<about-dialog></about-dialog>
|
||||||
|
<export-setting-dialog></export-setting-dialog>
|
||||||
<rename></rename>
|
<rename></rename>
|
||||||
<tweet></tweet>
|
<tweet></tweet>
|
||||||
<import-modal></import-modal>
|
<import-modal></import-modal>
|
||||||
@ -44,6 +44,7 @@ import TitleBar from '@/components/titleBar'
|
|||||||
import SideBar from '@/components/sideBar'
|
import SideBar from '@/components/sideBar'
|
||||||
import Aidou from '@/components/aidou/aidou'
|
import Aidou from '@/components/aidou/aidou'
|
||||||
import AboutDialog from '@/components/about'
|
import AboutDialog from '@/components/about'
|
||||||
|
import ExportSettingDialog from '@/components/exportSettings'
|
||||||
import Rename from '@/components/rename'
|
import Rename from '@/components/rename'
|
||||||
import Tweet from '@/components/tweet'
|
import Tweet from '@/components/tweet'
|
||||||
import ImportModal from '@/components/import'
|
import ImportModal from '@/components/import'
|
||||||
@ -61,6 +62,7 @@ export default {
|
|||||||
TitleBar,
|
TitleBar,
|
||||||
SideBar,
|
SideBar,
|
||||||
AboutDialog,
|
AboutDialog,
|
||||||
|
ExportSettingDialog,
|
||||||
Rename,
|
Rename,
|
||||||
Tweet,
|
Tweet,
|
||||||
ImportModal
|
ImportModal
|
||||||
@ -118,7 +120,7 @@ export default {
|
|||||||
// module: listenForMain
|
// module: listenForMain
|
||||||
dispatch('LISTEN_FOR_EDIT')
|
dispatch('LISTEN_FOR_EDIT')
|
||||||
dispatch('LISTEN_FOR_VIEW')
|
dispatch('LISTEN_FOR_VIEW')
|
||||||
dispatch('LISTEN_FOR_ABOUT_DIALOG')
|
dispatch('LISTEN_FOR_SHOW_DIALOG')
|
||||||
dispatch('LISTEN_FOR_PARAGRAPH_INLINE_STYLE')
|
dispatch('LISTEN_FOR_PARAGRAPH_INLINE_STYLE')
|
||||||
// module: project
|
// module: project
|
||||||
dispatch('LISTEN_FOR_UPDATE_PROJECT')
|
dispatch('LISTEN_FOR_UPDATE_PROJECT')
|
||||||
@ -136,7 +138,6 @@ export default {
|
|||||||
dispatch('LISTEN_FOR_SET_PATHNAME')
|
dispatch('LISTEN_FOR_SET_PATHNAME')
|
||||||
dispatch('LISTEN_FOR_BOOTSTRAP_WINDOW')
|
dispatch('LISTEN_FOR_BOOTSTRAP_WINDOW')
|
||||||
dispatch('LISTEN_FOR_SAVE_CLOSE')
|
dispatch('LISTEN_FOR_SAVE_CLOSE')
|
||||||
dispatch('LISTEN_FOR_EXPORT_PRINT')
|
|
||||||
dispatch('LISTEN_FOR_RENAME')
|
dispatch('LISTEN_FOR_RENAME')
|
||||||
dispatch('LINTEN_FOR_SET_LINE_ENDING')
|
dispatch('LINTEN_FOR_SET_LINE_ENDING')
|
||||||
dispatch('LISTEN_FOR_NEW_TAB')
|
dispatch('LISTEN_FOR_NEW_TAB')
|
||||||
|
@ -43,9 +43,15 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
emitTime: {
|
||||||
|
type: Number,
|
||||||
|
default: 800
|
||||||
|
},
|
||||||
regexValidator: {
|
regexValidator: {
|
||||||
type: RegExp,
|
type: RegExp,
|
||||||
default: /(.*?)/
|
default () {
|
||||||
|
return /(.*?)/
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -71,11 +77,17 @@ export default {
|
|||||||
clearTimeout(this.inputTimer)
|
clearTimeout(this.inputTimer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { emitTime } = this
|
||||||
|
if (emitTime === 0) {
|
||||||
|
this.onChange(value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Setting delay a little bit higher to prevent continuously file writes when typing.
|
// Setting delay a little bit higher to prevent continuously file writes when typing.
|
||||||
this.inputTimer = setTimeout(() => {
|
this.inputTimer = setTimeout(() => {
|
||||||
this.inputTimer = null
|
this.inputTimer = null
|
||||||
this.onChange(value)
|
this.onChange(value)
|
||||||
}, 800)
|
}, emitTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,7 @@
|
|||||||
<separator></separator>
|
<separator></separator>
|
||||||
<text-box
|
<text-box
|
||||||
description="Defines the maximum editor area width. An empty string or suffixes of ch (characters), px (pixels) or % (percentage) are allowed."
|
description="Defines the maximum editor area width. An empty string or suffixes of ch (characters), px (pixels) or % (percentage) are allowed."
|
||||||
:bool="editorLineWidth"
|
:input="editorLineWidth"
|
||||||
:regexValidator="/^(?:$|[0-9]+(?:ch|px|%)$)/"
|
:regexValidator="/^(?:$|[0-9]+(?:ch|px|%)$)/"
|
||||||
defaultValue="Default value from current theme"
|
defaultValue="Default value from current theme"
|
||||||
:onChange="value => onSelectChange('editorLineWidth', value)"
|
:onChange="value => onSelectChange('editorLineWidth', value)"
|
||||||
|
@ -11,7 +11,6 @@ class MarkdownPrint {
|
|||||||
this.clearup()
|
this.clearup()
|
||||||
const printContainer = document.createElement('article')
|
const printContainer = document.createElement('article')
|
||||||
printContainer.classList.add('print-container')
|
printContainer.classList.add('print-container')
|
||||||
printContainer.classList.add('markdown-body')
|
|
||||||
this.container = printContainer
|
this.container = printContainer
|
||||||
printContainer.innerHTML = html
|
printContainer.innerHTML = html
|
||||||
|
|
||||||
|
@ -947,20 +947,39 @@ const actions = {
|
|||||||
ipcRenderer.send('mt::update-format-menu', windowId, formats)
|
ipcRenderer.send('mt::update-format-menu', windowId, formats)
|
||||||
},
|
},
|
||||||
|
|
||||||
// listen for export from main process
|
EXPORT ({ state }, { type, content, pageOptions }) {
|
||||||
LISTEN_FOR_EXPORT_PRINT ({ commit, state }) {
|
|
||||||
ipcRenderer.on('AGANI::export', (e, { type }) => {
|
|
||||||
bus.$emit('export', type)
|
|
||||||
})
|
|
||||||
ipcRenderer.on('AGANI::print', e => {
|
|
||||||
bus.$emit('print')
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
EXPORT ({ commit, state }, { type, content, markdown }) {
|
|
||||||
if (!hasKeys(state.currentFile)) return
|
if (!hasKeys(state.currentFile)) return
|
||||||
|
|
||||||
|
// Extract title from TOC buffer.
|
||||||
|
let title = ''
|
||||||
|
const { listToc } = state
|
||||||
|
if (listToc && listToc.length > 0) {
|
||||||
|
let headerRef = listToc[0]
|
||||||
|
|
||||||
|
// The main title should be at the beginning of the document.
|
||||||
|
const len = Math.min(listToc.length, 6)
|
||||||
|
for (let i = 1; i < len; ++i) {
|
||||||
|
if (headerRef.lvl === 1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = listToc[i]
|
||||||
|
if (headerRef.lvl > header.lvl) {
|
||||||
|
headerRef = header
|
||||||
|
}
|
||||||
|
}
|
||||||
|
title = headerRef.content
|
||||||
|
}
|
||||||
|
|
||||||
const { filename, pathname } = state.currentFile
|
const { filename, pathname } = state.currentFile
|
||||||
ipcRenderer.send('AGANI::response-export', { type, content, filename, pathname, markdown })
|
ipcRenderer.send('AGANI::response-export', {
|
||||||
|
type,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
filename,
|
||||||
|
pathname,
|
||||||
|
pageOptions
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
LINTEN_FOR_EXPORT_SUCCESS ({ commit }) {
|
LINTEN_FOR_EXPORT_SUCCESS ({ commit }) {
|
||||||
|
@ -21,10 +21,13 @@ const actions = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
LISTEN_FOR_ABOUT_DIALOG ({ commit }) {
|
LISTEN_FOR_SHOW_DIALOG ({ commit }) {
|
||||||
ipcRenderer.on('AGANI::about-dialog', e => {
|
ipcRenderer.on('AGANI::about-dialog', e => {
|
||||||
bus.$emit('aboutDialog')
|
bus.$emit('aboutDialog')
|
||||||
})
|
})
|
||||||
|
ipcRenderer.on('mt::show-export-dialog', (e, type) => {
|
||||||
|
bus.$emit('showExportDialog', type)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
LISTEN_FOR_PARAGRAPH_INLINE_STYLE ({ commit }) {
|
LISTEN_FOR_PARAGRAPH_INLINE_STYLE ({ commit }) {
|
||||||
|
116
src/renderer/util/pdf.js
Normal file
116
src/renderer/util/pdf.js
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import fs from 'fs-extra'
|
||||||
|
import path from 'path'
|
||||||
|
import createDOMPurify from 'dompurify'
|
||||||
|
import { isFile } from 'common/filesystem'
|
||||||
|
import academicTheme from '@/assets/themes/export/academic.theme.css'
|
||||||
|
import liberTheme from '@/assets/themes/export/liber.theme.css'
|
||||||
|
|
||||||
|
const { sanitize } = createDOMPurify(window)
|
||||||
|
|
||||||
|
export const getCssForOptions = options => {
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
pageMarginTop,
|
||||||
|
pageMarginRight,
|
||||||
|
pageMarginBottom,
|
||||||
|
pageMarginLeft,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
lineHeight,
|
||||||
|
autoNumberingHeadings,
|
||||||
|
showFrontMatter,
|
||||||
|
theme,
|
||||||
|
headerFooterFontSize
|
||||||
|
} = options
|
||||||
|
const isPrintable = type !== 'styledHtml'
|
||||||
|
|
||||||
|
let output = ''
|
||||||
|
if (isPrintable) {
|
||||||
|
output += `@media print{@page{
|
||||||
|
margin: ${pageMarginTop}mm ${pageMarginRight}mm ${pageMarginBottom}mm ${pageMarginLeft}mm;}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Font options
|
||||||
|
output += '.markdown-body{'
|
||||||
|
if (fontFamily) {
|
||||||
|
output += `font-family:"${fontFamily}",${FALLBACK_FONT_FAMILIES};`
|
||||||
|
output = `.hf-container{font-family:"${fontFamily}",${FALLBACK_FONT_FAMILIES};}${output}`
|
||||||
|
}
|
||||||
|
if (fontSize) {
|
||||||
|
output += `font-size:${fontSize}px;`
|
||||||
|
}
|
||||||
|
if (lineHeight) {
|
||||||
|
output += `line-height:${lineHeight};`
|
||||||
|
}
|
||||||
|
output += '}'
|
||||||
|
|
||||||
|
// Auto numbering headings via CSS
|
||||||
|
if (autoNumberingHeadings) {
|
||||||
|
output += autoNumberingHeadingsCss
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide front matter
|
||||||
|
if (!showFrontMatter) {
|
||||||
|
output += 'pre.front-matter{display:none!important;}'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (theme) {
|
||||||
|
if (theme === 'academic') {
|
||||||
|
output += academicTheme
|
||||||
|
} else if (theme === 'liber') {
|
||||||
|
output += liberTheme
|
||||||
|
} else {
|
||||||
|
// Read theme from disk
|
||||||
|
const { userDataPath } = global.marktext.paths
|
||||||
|
const themePath = path.join(userDataPath, 'themes/export', theme)
|
||||||
|
if (isFile(themePath)) {
|
||||||
|
try {
|
||||||
|
const themeCSS = fs.readFileSync(themePath, 'utf8')
|
||||||
|
output += themeCSS
|
||||||
|
} catch (_) {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headerFooterFontSize) {
|
||||||
|
output += `.page-header .hf-container,
|
||||||
|
.page-footer-fake .hf-container,
|
||||||
|
.page-footer .hf-container {
|
||||||
|
font-size: ${headerFooterFontSize}px;
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPrintable) {
|
||||||
|
// Close @page
|
||||||
|
output += '}'
|
||||||
|
}
|
||||||
|
return sanitize(output, EXPORT_DOMPURIFY_CONFIG)
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXPORT_DOMPURIFY_CONFIG = {
|
||||||
|
FORBID_ATTR: ['contenteditable'],
|
||||||
|
ALLOW_DATA_ATTR: false,
|
||||||
|
USE_PROFILES: {
|
||||||
|
html: true,
|
||||||
|
svg: true,
|
||||||
|
svgFilters: true,
|
||||||
|
mathMl: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't use "Noto Color Emoji" because it will result in PDF files with multiple MB and weird looking emojis.
|
||||||
|
const FALLBACK_FONT_FAMILIES = '"Open Sans","Segoe UI","Helvetica Neue",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"'
|
||||||
|
|
||||||
|
const autoNumberingHeadingsCss = `body {counter-reset: h2}
|
||||||
|
h2 {counter-reset: h3}
|
||||||
|
h3 {counter-reset: h4}
|
||||||
|
h4 {counter-reset: h5}
|
||||||
|
h5 {counter-reset: h6}
|
||||||
|
h2:before {counter-increment: h2; content: counter(h2) ". "}
|
||||||
|
h3:before {counter-increment: h3; content: counter(h2) "." counter(h3) ". "}
|
||||||
|
h4:before {counter-increment: h4; content: counter(h2) "." counter(h3) "." counter(h4) ". "}
|
||||||
|
h5:before {counter-increment: h5; content: counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) ". "}
|
||||||
|
h6:before {counter-increment: h6; content: counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) "." counter(h6) ". "}
|
||||||
|
h2.nocount:before, h3.nocount:before, h4.nocount:before, h5.nocount:before, h6.nocount:before { content: ""; counter-increment: none }`
|
Loading…
Reference in New Issue
Block a user