Stackedit/src/services/exportSvc.js
2023-06-29 22:24:07 +08:00

180 lines
6.1 KiB
JavaScript

import md5 from 'js-md5';
import FileSaver from 'file-saver';
import TemplateWorker from 'worker-loader!./templateWorker.js'; // eslint-disable-line
import localDbSvc from './localDbSvc';
import markdownConversionSvc from './markdownConversionSvc';
import extensionSvc from './extensionSvc';
import utils from './utils';
import store from '../store';
import htmlSanitizer from '../libs/htmlSanitizer';
function groupHeadings(headings, level = 1) {
const result = [];
let currentItem;
function pushCurrentItem() {
if (currentItem) {
if (currentItem.children.length > 0) {
currentItem.children = groupHeadings(currentItem.children, level + 1);
}
result.push(currentItem);
}
}
headings.forEach((heading) => {
if (heading.level !== level) {
currentItem = currentItem || {
children: [],
};
currentItem.children.push(heading);
} else {
pushCurrentItem();
currentItem = heading;
}
});
pushCurrentItem();
return result;
}
const getImgBase64 = async (uri) => {
if (uri.indexOf('http://') !== 0 && uri.indexOf('https://') !== 0) {
const currDirNode = store.getters['explorer/selectedNodeFolder'];
const absoluteImgPath = utils.getAbsoluteFilePath(currDirNode, uri);
const md5Id = md5(absoluteImgPath);
const imgItem = await localDbSvc.getImgItem(md5Id);
if (imgItem) {
const potIdx = uri.lastIndexOf('.');
const suffix = potIdx > -1 ? uri.substring(potIdx + 1) : 'png';
const mime = `image/${suffix}`;
return `data:${mime};base64,${imgItem.content}`;
}
return '';
}
return uri;
};
const containerElt = document.createElement('div');
containerElt.className = 'hidden-rendering-container';
document.body.appendChild(containerElt);
export default {
/**
* Apply the template to the file content
*/
async applyTemplate(fileId, template = {
value: '{{{files.0.content.text}}}',
helpers: '',
}, pdf = false) {
const file = store.state.file.itemsById[fileId];
const content = await localDbSvc.loadItem(`${fileId}/content`);
const properties = utils.computeProperties(content.properties);
const options = extensionSvc.getOptions(properties);
const converter = markdownConversionSvc.createConverter(options, true);
const parsingCtx = markdownConversionSvc.parseSections(converter, content.text);
const conversionCtx = markdownConversionSvc.convert(parsingCtx);
const html = conversionCtx.htmlSectionList.map(htmlSanitizer.sanitizeHtml).join('');
const colorThemeClass = `app--${store.getters['data/computedSettings'].colorTheme}`;
const themeClass = `preview-theme--${store.state.theme.currPreviewTheme}`;
let themeStyleContent = '';
const themeStyleEle = document.getElementById(`preview-theme-${store.state.theme.currPreviewTheme}`);
if (themeStyleEle) {
themeStyleContent = themeStyleEle.innerText;
}
containerElt.innerHTML = html;
extensionSvc.sectionPreview(containerElt, options);
// Unwrap tables
containerElt.querySelectorAll('.table-wrapper').cl_each((wrapperElt) => {
while (wrapperElt.firstChild) {
wrapperElt.parentNode.insertBefore(wrapperElt.firstChild, wrapperElt.nextSibling);
}
wrapperElt.parentNode.removeChild(wrapperElt);
});
// 替换相对路径图片为blob图片
const imgs = Array.prototype.slice.call(containerElt.getElementsByTagName('img')).map((imgElt) => {
let uri = imgElt.attributes && imgElt.attributes.href && imgElt.attributes.href.nodeValue;
if (uri && uri.indexOf('http://') !== 0 && uri.indexOf('https://') !== 0) {
uri = decodeURIComponent(uri);
imgElt.removeAttribute('src');
return { imgElt, uri };
}
return { imgElt };
});
const loadedPromises = imgs.map(it => new Promise((resolve, reject) => {
if (!it.imgElt.src && it.uri) {
getImgBase64(it.uri).then((newUrl) => {
it.imgElt.src = newUrl;
resolve();
}, () => reject(new Error('加载当前空间图片出错')));
return;
}
resolve();
}));
await Promise.all(loadedPromises);
// Make TOC
const allHeaders = containerElt.querySelectorAll('h1,h2,h3,h4,h5,h6');
Array.prototype.slice.call(allHeaders).forEach((headingElt) => {
headingElt.innerHTML = `<span class="prefix"></span><span class="content">${headingElt.innerHTML}</span><span class="suffix"></span>`;
});
const headings = allHeaders.cl_map(headingElt => ({
title: headingElt.textContent,
anchor: headingElt.id,
level: parseInt(headingElt.tagName.slice(1), 10),
children: [],
}));
const toc = groupHeadings(headings);
const view = {
pdf,
files: [{
name: file.name,
content: {
text: content.text,
properties,
yamlProperties: content.properties,
html: containerElt.innerHTML,
toc,
colorThemeClass,
themeClass,
themeStyleContent,
},
}],
};
containerElt.innerHTML = '';
// Run template conversion in a Worker to prevent attacks from helpers
const worker = new TemplateWorker();
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
worker.terminate();
reject(new Error('Template generation timeout.'));
}, 10000);
worker.addEventListener('message', (e) => {
clearTimeout(timeoutId);
worker.terminate();
// e.data can contain unsafe data if helpers attempts to call postMessage
const [err, result] = e.data;
if (err) {
reject(new Error(`${err}`));
} else {
resolve(`${result}`);
}
});
worker.postMessage([template.value, view, template.helpers]);
});
},
/**
* Export a file to disk.
*/
async exportToDisk(fileId, type, template) {
const file = store.state.file.itemsById[fileId];
const html = await this.applyTemplate(fileId, template);
const blob = new Blob([html], {
type: 'text/plain;charset=utf-8',
});
FileSaver.saveAs(blob, `${file.name}.${type}`);
},
};