mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 18:01:45 +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: [
|
||||
'to-string-loader',
|
||||
'css-loader'
|
||||
@ -59,7 +59,7 @@ const rendererConfig = {
|
||||
},
|
||||
{
|
||||
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: [
|
||||
proMode ? MiniCssExtractPlugin.loader : 'style-loader',
|
||||
{ loader: 'css-loader', options: { importLoaders: 1 } },
|
||||
|
@ -1,4 +1,4 @@
|
||||
import fs from 'fs'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import { BrowserWindow, dialog, ipcMain, shell } from 'electron'
|
||||
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.
|
||||
// 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.
|
||||
const handleResponseForExport = async (e, { type, content, pathname, markdown }) => {
|
||||
const handleResponseForExport = async (e, { type, content, pathname, title, pageOptions }) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
const extension = EXTENSION_HASN[type]
|
||||
const dirname = pathname ? path.dirname(pathname) : getPath('documents')
|
||||
let nakedFilename = getRecommendTitleFromMarkdownString(markdown)
|
||||
let nakedFilename = title
|
||||
if (!nakedFilename) {
|
||||
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, {
|
||||
defaultPath
|
||||
})
|
||||
|
||||
if (filePath && !canceled) {
|
||||
let data = content
|
||||
try {
|
||||
if (!content && type === 'pdf') {
|
||||
data = await win.webContents.printToPDF({ printBackground: true })
|
||||
if (type === 'pdf') {
|
||||
const options = { printBackground: true }
|
||||
Object.assign(options, getPdfPageOptions(pageOptions))
|
||||
const data = await win.webContents.printToPDF(options)
|
||||
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) {
|
||||
log.error(err)
|
||||
const ERROR_MSG = err.message || `Error happened when export ${filePath}`
|
||||
@ -399,7 +420,7 @@ ipcMain.on('AGANI::format-link-click', (e, { data, dirname }) => {
|
||||
// --- menu -------------------------------------
|
||||
|
||||
export const exportFile = (win, type) => {
|
||||
win.webContents.send('AGANI::export', { type })
|
||||
win.webContents.send('mt::show-export-dialog', type)
|
||||
}
|
||||
|
||||
export const importFile = async win => {
|
||||
@ -423,7 +444,16 @@ export const importFile = async 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 => {
|
||||
|
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;`
|
||||
// }
|
||||
|
||||
export const isInElectron = window && window.process && window.process.type === 'renderer'
|
||||
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)
|
||||
// http[s] (domain or IPv4 or localhost or IPv6) [port] /not-white-space
|
||||
|
@ -145,9 +145,9 @@ class Muya {
|
||||
return this.contentState.history.clearHistory()
|
||||
}
|
||||
|
||||
exportStyledHTML (title = '', printOptimization = false, extraCss = '') {
|
||||
exportStyledHTML (options) {
|
||||
const { markdown } = this
|
||||
return new ExportHtml(markdown, this).generate(title, printOptimization, extraCss)
|
||||
return new ExportHtml(markdown, this).generate(options)
|
||||
}
|
||||
|
||||
exportHtml () {
|
||||
|
@ -5,6 +5,7 @@ import loadRenderer from '../renderers'
|
||||
import githubMarkdownCss from 'github-markdown-css/github-markdown.css'
|
||||
import highlightCss from 'prismjs/themes/prism.css'
|
||||
import katexCss from 'katex/dist/katex.css'
|
||||
import footerHeaderCss from '../assets/styles/headerFooterStyle.css'
|
||||
import { EXPORT_DOMPURIFY_CONFIG } from '../config'
|
||||
import { sanitize, unescapeHtml } from '../utils'
|
||||
import { validEmoji } from '../ui/emojis'
|
||||
@ -131,16 +132,20 @@ class ExportHtml {
|
||||
},
|
||||
mathRenderer: this.mathRenderer
|
||||
})
|
||||
|
||||
html = sanitize(html, EXPORT_DOMPURIFY_CONFIG)
|
||||
|
||||
const exportContainer = this.exportContainer = document.createElement('div')
|
||||
exportContainer.classList.add('ag-render-container')
|
||||
exportContainer.innerHTML = html
|
||||
document.body.appendChild(exportContainer)
|
||||
|
||||
// render only render the light theme of mermaid and diragram...
|
||||
await this.renderMermaid()
|
||||
await this.renderDiagram()
|
||||
let result = exportContainer.innerHTML
|
||||
exportContainer.remove()
|
||||
|
||||
// hack to add arrow marker to output html
|
||||
const pathes = document.querySelectorAll('path[id^=raphael-marker-]')
|
||||
const def = '<defs style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">'
|
||||
@ -151,6 +156,7 @@ class ExportHtml {
|
||||
}
|
||||
return `${def}${str}`
|
||||
})
|
||||
|
||||
this.exportContainer = null
|
||||
return result
|
||||
}
|
||||
@ -158,20 +164,25 @@ class ExportHtml {
|
||||
/**
|
||||
* Get HTML with style
|
||||
*
|
||||
* @param {*} title Page title
|
||||
* @param {*} printOptimization Optimize HTML and CSS for printing
|
||||
* @param {*} options Document options
|
||||
*/
|
||||
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.
|
||||
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 : ''
|
||||
this.mathRendererCalled = false
|
||||
|
||||
// `extraCss` may changed in the mean time.
|
||||
const { title, extraCss } = options
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>${title}</title>
|
||||
<title>${sanitize(title, EXPORT_DOMPURIFY_CONFIG)}</title>
|
||||
<style>
|
||||
${githubMarkdownCss}
|
||||
</style>
|
||||
@ -189,6 +200,28 @@ class ExportHtml {
|
||||
margin: 0 auto;
|
||||
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 {
|
||||
display: table;
|
||||
}
|
||||
@ -202,12 +235,6 @@ class ExportHtml {
|
||||
margin-top: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.markdown-body {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body ol ol,
|
||||
.markdown-body ul ol {
|
||||
list-style-type: decimal;
|
||||
@ -222,12 +249,105 @@ class ExportHtml {
|
||||
<style>${extraCss}</style>
|
||||
</head>
|
||||
<body>
|
||||
<article class="markdown-body">
|
||||
${html}
|
||||
</article>
|
||||
</body>
|
||||
</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
|
||||
|
@ -1,6 +1,7 @@
|
||||
// DOTO: Don't use Node API in editor folder, remove `path` @jocs
|
||||
import createDOMPurify from 'dompurify'
|
||||
import { isInElectron, URL_REG } from '../config'
|
||||
import { URL_REG } from '../config'
|
||||
|
||||
const { sanitize: runSanitize } = createDOMPurify(window)
|
||||
|
||||
const ID_PREFIX = 'ag-'
|
||||
let id = 0
|
||||
@ -263,7 +264,7 @@ export const getImageInfo = (src, baseUrl = window.DIRNAME) => {
|
||||
if (imageExtension) {
|
||||
const isAbsoluteLocal = /^(?:\/|\\\\|[a-zA-Z]:\\).+/.test(src)
|
||||
if (isUrl || (!isAbsoluteLocal && !baseUrl)) {
|
||||
if (!isUrl && !baseUrl && isInElectron) {
|
||||
if (!isUrl && !baseUrl) {
|
||||
console.warn('"baseUrl" is not defined!')
|
||||
}
|
||||
|
||||
@ -271,14 +272,13 @@ export const getImageInfo = (src, baseUrl = window.DIRNAME) => {
|
||||
isUnknownType: false,
|
||||
src
|
||||
}
|
||||
} else if (isInElectron) {
|
||||
} else {
|
||||
// Correct relative path on desktop. If we resolve a absolute path "path.resolve" doesn't do anything.
|
||||
return {
|
||||
isUnknownType: false,
|
||||
src: 'file://' + require('path').resolve(baseUrl, src)
|
||||
}
|
||||
}
|
||||
// else: Forbid the request due absolute or relative path in browser
|
||||
} else if (isUrl && !imageExtension) {
|
||||
// Assume it's a valid image and make a http request later
|
||||
return {
|
||||
@ -367,8 +367,7 @@ export const mixins = (constructor, ...object) => {
|
||||
}
|
||||
|
||||
export const sanitize = (html, options) => {
|
||||
const DOMPurify = createDOMPurify(window)
|
||||
return DOMPurify.sanitize(escapeInBlockHtml(html), options)
|
||||
return runSanitize(escapeInBlockHtml(html), options)
|
||||
}
|
||||
|
||||
export const getParagraphReference = (ele, id) => {
|
||||
|
@ -26,7 +26,8 @@ body article.print-container {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
body > div {
|
||||
body > div,
|
||||
body > svg {
|
||||
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 { moveImageToFolder, uploadImage } from '@/util/fileSystem'
|
||||
import { guessClipboardFilePath } from '@/util/clipboard'
|
||||
import { getCssForOptions } from '@/util/pdf'
|
||||
import { addCommonStyle, setEditorWidth } from '@/util/theme'
|
||||
|
||||
import 'muya/themes/default.css'
|
||||
@ -114,9 +115,6 @@ export default {
|
||||
Search
|
||||
},
|
||||
props: {
|
||||
filename: {
|
||||
type: String
|
||||
},
|
||||
markdown: String,
|
||||
cursor: Object,
|
||||
textDirection: {
|
||||
@ -534,7 +532,6 @@ export default {
|
||||
bus.$on('deleteParagraph', this.handleParagraph)
|
||||
bus.$on('insertParagraph', this.handleInsertParagraph)
|
||||
bus.$on('scroll-to-header', this.scrollToHeader)
|
||||
bus.$on('print', this.handlePrint)
|
||||
bus.$on('screenshot-captured', this.handleScreenShot)
|
||||
bus.$on('switch-spellchecker-language', this.switchSpellcheckLanguage)
|
||||
|
||||
@ -878,27 +875,60 @@ export default {
|
||||
this.scrollToHighlight()
|
||||
},
|
||||
|
||||
async handlePrint () {
|
||||
// generate styled HTML with empty title and optimized for printing
|
||||
const html = await this.editor.exportStyledHTML('', true)
|
||||
this.printer.renderMarkdown(html, true)
|
||||
this.$store.dispatch('PRINT_RESPONSE')
|
||||
},
|
||||
async handleExport (options) {
|
||||
const {
|
||||
type,
|
||||
header,
|
||||
footer,
|
||||
headerFooterStyled,
|
||||
htmlTitle
|
||||
} = options
|
||||
if (!/^pdf|print|styledHtml$/.test(type)) {
|
||||
throw new Error(`Invalid type to export: "${type}".`)
|
||||
}
|
||||
|
||||
async handleExport (type) {
|
||||
const markdown = this.editor.getMarkdown()
|
||||
const extraCss = getCssForOptions(options)
|
||||
switch (type) {
|
||||
case 'styledHtml': {
|
||||
const content = await this.editor.exportStyledHTML(this.filename)
|
||||
this.$store.dispatch('EXPORT', { type, content, markdown })
|
||||
const content = await this.editor.exportStyledHTML({
|
||||
title: htmlTitle || '',
|
||||
printOptimization: false,
|
||||
extraCss
|
||||
})
|
||||
this.$store.dispatch('EXPORT', { type, content })
|
||||
break
|
||||
}
|
||||
|
||||
case 'pdf': {
|
||||
// generate styled HTML with empty title and optimized for printing
|
||||
const html = await this.editor.exportStyledHTML('', true)
|
||||
// NOTE: We need to set page size via Electron.
|
||||
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.$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
|
||||
}
|
||||
}
|
||||
@ -1021,7 +1051,6 @@ export default {
|
||||
bus.$off('deleteParagraph', this.handleParagraph)
|
||||
bus.$off('insertParagraph', this.handleInsertParagraph)
|
||||
bus.$off('scroll-to-header', this.scrollToHeader)
|
||||
bus.$off('print', this.handlePrint)
|
||||
bus.$off('screenshot-captured', this.handleScreenShot)
|
||||
bus.$off('switch-spellchecker-language', this.switchSpellcheckLanguage)
|
||||
|
||||
|
@ -5,7 +5,6 @@
|
||||
<tabs v-show="showTabBar"></tabs>
|
||||
<div class="container">
|
||||
<editor
|
||||
:fileanme="filename"
|
||||
:markdown="markdown"
|
||||
:cursor="cursor"
|
||||
:text-direction="textDirection"
|
||||
@ -30,9 +29,6 @@ import TabNotifications from './notifications.vue'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
filename: {
|
||||
type: String
|
||||
},
|
||||
markdown: {
|
||||
type: String,
|
||||
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
|
||||
v-if="hasCurrentFile && init"
|
||||
:markdown="markdown"
|
||||
:filename="filename"
|
||||
:cursor="cursor"
|
||||
:source-code="sourceCode"
|
||||
:show-tab-bar="showTabBar"
|
||||
@ -29,6 +28,7 @@
|
||||
></editor-with-tabs>
|
||||
<aidou></aidou>
|
||||
<about-dialog></about-dialog>
|
||||
<export-setting-dialog></export-setting-dialog>
|
||||
<rename></rename>
|
||||
<tweet></tweet>
|
||||
<import-modal></import-modal>
|
||||
@ -44,6 +44,7 @@ import TitleBar from '@/components/titleBar'
|
||||
import SideBar from '@/components/sideBar'
|
||||
import Aidou from '@/components/aidou/aidou'
|
||||
import AboutDialog from '@/components/about'
|
||||
import ExportSettingDialog from '@/components/exportSettings'
|
||||
import Rename from '@/components/rename'
|
||||
import Tweet from '@/components/tweet'
|
||||
import ImportModal from '@/components/import'
|
||||
@ -61,6 +62,7 @@ export default {
|
||||
TitleBar,
|
||||
SideBar,
|
||||
AboutDialog,
|
||||
ExportSettingDialog,
|
||||
Rename,
|
||||
Tweet,
|
||||
ImportModal
|
||||
@ -118,7 +120,7 @@ export default {
|
||||
// module: listenForMain
|
||||
dispatch('LISTEN_FOR_EDIT')
|
||||
dispatch('LISTEN_FOR_VIEW')
|
||||
dispatch('LISTEN_FOR_ABOUT_DIALOG')
|
||||
dispatch('LISTEN_FOR_SHOW_DIALOG')
|
||||
dispatch('LISTEN_FOR_PARAGRAPH_INLINE_STYLE')
|
||||
// module: project
|
||||
dispatch('LISTEN_FOR_UPDATE_PROJECT')
|
||||
@ -136,7 +138,6 @@ export default {
|
||||
dispatch('LISTEN_FOR_SET_PATHNAME')
|
||||
dispatch('LISTEN_FOR_BOOTSTRAP_WINDOW')
|
||||
dispatch('LISTEN_FOR_SAVE_CLOSE')
|
||||
dispatch('LISTEN_FOR_EXPORT_PRINT')
|
||||
dispatch('LISTEN_FOR_RENAME')
|
||||
dispatch('LINTEN_FOR_SET_LINE_ENDING')
|
||||
dispatch('LISTEN_FOR_NEW_TAB')
|
||||
|
@ -43,9 +43,15 @@ export default {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
emitTime: {
|
||||
type: Number,
|
||||
default: 800
|
||||
},
|
||||
regexValidator: {
|
||||
type: RegExp,
|
||||
default: /(.*?)/
|
||||
default () {
|
||||
return /(.*?)/
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -71,11 +77,17 @@ export default {
|
||||
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.
|
||||
this.inputTimer = setTimeout(() => {
|
||||
this.inputTimer = null
|
||||
this.onChange(value)
|
||||
}, 800)
|
||||
}, emitTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -99,7 +99,7 @@
|
||||
<separator></separator>
|
||||
<text-box
|
||||
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|%)$)/"
|
||||
defaultValue="Default value from current theme"
|
||||
:onChange="value => onSelectChange('editorLineWidth', value)"
|
||||
|
@ -11,7 +11,6 @@ class MarkdownPrint {
|
||||
this.clearup()
|
||||
const printContainer = document.createElement('article')
|
||||
printContainer.classList.add('print-container')
|
||||
printContainer.classList.add('markdown-body')
|
||||
this.container = printContainer
|
||||
printContainer.innerHTML = html
|
||||
|
||||
|
@ -947,20 +947,39 @@ const actions = {
|
||||
ipcRenderer.send('mt::update-format-menu', windowId, formats)
|
||||
},
|
||||
|
||||
// listen for export from main process
|
||||
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 }) {
|
||||
EXPORT ({ state }, { type, content, pageOptions }) {
|
||||
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
|
||||
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 }) {
|
||||
|
@ -21,10 +21,13 @@ const actions = {
|
||||
})
|
||||
},
|
||||
|
||||
LISTEN_FOR_ABOUT_DIALOG ({ commit }) {
|
||||
LISTEN_FOR_SHOW_DIALOG ({ commit }) {
|
||||
ipcRenderer.on('AGANI::about-dialog', e => {
|
||||
bus.$emit('aboutDialog')
|
||||
})
|
||||
ipcRenderer.on('mt::show-export-dialog', (e, type) => {
|
||||
bus.$emit('showExportDialog', type)
|
||||
})
|
||||
},
|
||||
|
||||
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