import Cherry from 'cherry-markdown-core';
import 'cherry-markdown-core/dist/cherry-markdown.min.css';
const vscode = acquireVsCodeApi();
* 在侧边栏增加编辑/预览入口
*/
const customMenuChangeModule = Cherry.createMenuHook('编辑', {
iconName: 'pen',
onClick(selection) {
if (window.isDisableEdit) {
vscode.postMessage({
type: 'tips',
data: "can't edit presently 当前文档已失焦点,编辑后无法保存",
});
return selection;
}
const pen = document.getElementsByClassName('cherry-toolbar-pen')[0];
const markdown = document.getElementById('markdown');
const isPreviewOnly = !/active/.test(pen.className);
if (isPreviewOnly) {
markdown.className = 'markdown-edit-preview';
pen.className = `${pen.className} active`;
pen.innerHTML = '<i class="ch-icon ch-icon-pen-fill"></i>';
} else {
markdown.className = 'markdown-preview-only';
pen.className = pen.className.replace(' active', '');
pen.innerHTML = '<i class="ch-icon ch-icon-pen"></i>';
}
return selection;
},
});
const customMenuFont = Cherry.createMenuHook('字体样式', {
iconName: 'font',
});
const customMenuExport = Cherry.createMenuHook('保存', {
iconName: 'export',
subMenuConfig: [
{
noIcon: true,
name: '保存为 PNG',
onclick: async () => {
const cherrymarkdown = document.querySelector('.cherry-previewer');
if (!cherrymarkdown) {
vscode.postMessage({ type: 'export-png', data: 'export-fail' });
return;
}
try {
const mod = await import( 'html-to-image');
const toPng = mod.toPng || (mod.default && mod.default.toPng);
if (!toPng) throw new Error('html-to-image unavailable');
const dataUrl = await toPng(cherrymarkdown);
vscode.postMessage({ type: 'export-png', data: dataUrl });
} catch (error) {
console.error('toPng error:', error);
vscode.postMessage({ type: 'export-png', data: 'export-fail' });
}
},
},
],
});
const onClickLink = (e, target) => {
const href = target.attributes?.href.value;
const hrefValidation = href ? href : 'href-invalid';
if (isHttpUrl(hrefValidation) || hrefValidation) {
e.preventDefault();
vscode.postMessage({
type: 'open-url',
data: href,
});
return;
}
vscode.postMessage({
type: 'open-url',
data: 'href-invalid',
});
};
const basicConfig = {
id: 'markdown',
externals: {
echarts: window.echarts,
MathJax: window.MathJax,
},
isPreviewOnly: false,
engine: {
global: {
urlProcessor(url, srcType) {
return url;
},
},
syntax: {
codeBlock: {
theme: 'twilight',
},
table: {
enableChart: false,
},
fontEmphasis: {
allowWhitespace: false,
},
strikethrough: {
needWhitespace: false,
},
mathBlock: {
engine: 'MathJax',
},
inlineMath: {
engine: 'MathJax',
},
emoji: {
useUnicode: true,
},
header: {
anchorStyle: 'none',
},
codeBlock: {
mermaid: {
svg2img: false,
},
},
},
},
toolbars: {
toolbar: [
'bold',
{
customMenuFont: ['italic', 'strikethrough', 'underline', 'sub', 'sup', 'ruby'],
},
'size',
'color',
'|',
'header',
'list',
'|',
'panel',
'justify',
'detail',
'|',
{
insert: [
'image',
'link',
'hr',
'br',
'code',
'formula',
'toc',
'table',
],
},
'togglePreview',
],
bubble: ['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'quote', 'ruby', '|', 'size', 'color'],
sidebar: ['customMenuChangeModule', 'mobilePreview', 'copy', 'theme', 'customMenuExport'],
customMenu: {
customMenuChangeModule,
customMenuFont,
customMenuExport,
},
toc: true,
},
editor: {
* @typedef {Object} fileUploadParams
* @property {string=} name 文件名
* @property {string=} poster 封面
* @property {boolean=} isBorder 是否有边框
* @property {boolean} isShadow 是否有阴影
* @property {boolean} isRadius 是否圆角
*/
* @callback fileUploadCallback 回填回调函数
* @param {string} url 回填的url
* @param {fileUploadParams} params 回填的参数
*/
* 文件上传逻辑(涉及到文件上传均会调用此处)
* @param {File} file 具体文件
* @param {fileUploadCallback=} callback
*/
fileUpload: (file, callback) => {
vscode.postMessage({
type: 'upload-file',
data: {
name: file.name,
path: file.path,
size: file.size,
type: file.type,
},
});
window.uploadFileCallback = callback;
},
},
event: {
changeMainTheme: (theme) => {
vscode.postMessage({
type: 'change-theme',
data: theme,
});
},
},
previewer: {
lazyLoadImg: {
},
},
keydown: [],
callback: {
changeString2Pinyin: window.pinyin,
beforeImageMounted(srcProp, srcValue) {
const { _activeTextEditorPath } = window;
if (isHttpUrl(srcValue) || isDataUrl(srcValue)) {
return {
src: srcValue,
};
}
const absolutePath = new URL(srcValue, _activeTextEditorPath).href;
return {
src: absolutePath,
};
},
onClickPreview: (e) => {
const { target } = e;
switch (target?.nodeName) {
case 'SPAN':
if (target?.parentElement?.nodeName === 'A') {
onClickLink(e, target?.parentElement);
}
break;
case 'A':
onClickLink(e, target);
break;
}
},
},
};
function isDataUrl(url) {
return /^data:/.test(url);
}
function isHttpUrl(url) {
return /https?:\/\//.test(url);
}
* [vscode language](https://code.visualstudio.com/docs/getstarted/locales#_available-locales);
* [cherry language](https://github.com/Tencent/cherry-markdown/wiki/%E5%A4%9A%E8%AF%AD%E8%A8%80);
* */
const languageIdentifiers = {
en: 'en_US',
'zh-cn': 'zh_CN',
ru: 'ru_RU',
};
const mdInfo = JSON.parse(document.getElementById('markdown-info').value);
const locale = languageIdentifiers[mdInfo.vscodeLanguage] || 'zh_CN';
const config = Object.assign({}, basicConfig, { value: mdInfo.text, locale });
import( 'mathjax/es5/tex-svg.js').catch(() => {});
const cherry = new Cherry(config);
cherry.previewer.getDom().addEventListener('scroll', () => {
const domContainer = cherry.previewer.getDom();
if (window.disableScrollListener) {
return true;
}
if (domContainer.scrollTop <= 0) {
postScrollMessage(0);
return true;
}
if (domContainer.scrollTop + domContainer.offsetHeight > domContainer.scrollHeight) {
postScrollMessage(-1);
return true;
}
const basePoint = domContainer.getBoundingClientRect();
const watchPoint = {
x: basePoint.left + basePoint.width / 2,
y: basePoint.top + 1,
};
const targetElements = elementsFromPoint(watchPoint.x, watchPoint.y);
let targetElement;
for (let i = 0; i < targetElements.length; i++) {
if (domContainer.contains(targetElements[i])) {
targetElement = targetElements[i];
break;
}
}
if (!targetElement || targetElement === domContainer) {
return;
}
let mdElement = targetElement.closest('[data-sign]');
while (mdElement && mdElement.parentElement && mdElement.parentElement !== domContainer) {
mdElement = mdElement.parentElement.closest('[data-sign]');
}
if (!mdElement) {
return;
}
let lines = 0;
let element = mdElement;
while (element) {
lines += +element.getAttribute('data-lines');
element = element.previousElementSibling;
}
const mdElementStyle = getComputedStyle(mdElement);
const marginTop = parseFloat(mdElementStyle.marginTop);
const marginBottom = parseFloat(mdElementStyle.marginBottom);
const mdRect = mdElement.getBoundingClientRect();
const mdActualHeight = mdRect.height + marginTop + marginBottom;
const mdOffsetTop = mdRect.y - marginTop - basePoint.y;
const lineNum = +mdElement.getAttribute('data-lines');
const percent = Math.abs(mdOffsetTop) / mdActualHeight;
postScrollMessage(lines - lineNum + parseInt(lineNum * percent, 10));
});
function postScrollMessage(line) {
vscode.postMessage({
type: 'preview-scroll',
data: line,
});
}
cherry.onChange((newValue) => {
if (window.disableEditListener) {
return true;
}
vscode.postMessage({
type: 'cherry-change',
data: newValue,
});
});
let scrollTimeOut;
let editTimeOut;
window.addEventListener('message', (e) => {
const { cmd, data } = e.data;
switch (cmd) {
case 'editor-change':
window.disableEditListener = true;
cherry.setValue(data.text);
editTimeOut && clearTimeout(editTimeOut);
editTimeOut = setTimeout(() => {
window.disableEditListener = false;
}, 500);
break;
case 'editor-scroll':
window.disableScrollListener = true;
cherry.previewer.scrollToLineNumWithOffset(data, 0);
scrollTimeOut && clearTimeout(scrollTimeOut);
scrollTimeOut = setTimeout(() => {
window.disableScrollListener = false;
}, 500);
break;
case 'disable-edit':
window.isDisableEdit = true;
const pen = document.getElementsByClassName('cherry-toolbar-pen')[0];
const markdown = document.getElementById('markdown');
markdown.className = 'markdown-preview-only';
pen.className = pen.className.replace(' active', '');
pen.innerHTML = '<i class="ch-icon ch-icon-pen"></i>';
break;
case 'enable-edit':
window.isDisableEdit = false;
break;
case 'upload-file-callback': {
const { url, ...rest } = data;
window.uploadFileCallback(url, rest);
try {
const previewDom = cherry.previewer.getDom();
const imgs = previewDom.querySelectorAll(`img[src="${url}"]`);
imgs.forEach((img) => {
img.classList.remove('ch-image-border', 'ch-image-no-border', 'ch-image-shadow', 'ch-image-radius');
if (rest.isNotBorder) {
img.classList.add('ch-image-no-border');
} else if (rest.isBorder) {
img.classList.add('ch-image-border');
}
if (rest.isShadow) {
img.classList.add('ch-image-shadow');
}
if (rest.isRadius) {
img.classList.add('ch-image-radius');
}
});
} catch (e) {
}
break;
}
}
});
* document.elementsFromPoint polyfill
* ref: https://github.com/JSmith01/elementsfrompoint-polyfill/blob/master/index.js
* @param {number} x
* @param {number} y
*/
function elementsFromPoint(x, y) {
if (typeof document.elementsFromPoint === 'function') {
return document.elementsFromPoint(x, y);
}
if (typeof ( (document).msElementsFromPoint) === 'function') {
const nodeList = (document).msElementsFromPoint(x, y);
return nodeList !== null ? Array.from(nodeList) : nodeList;
}
const elements = [];
const pointerEvents = [];
let ele;
do {
const currentElement = (document.elementFromPoint(x, y));
if (ele !== currentElement) {
ele = currentElement;
elements.push(ele);
pointerEvents.push(ele.style.pointerEvents);
ele.style.pointerEvents = 'none';
} else {
ele = null;
}
} while (ele);
elements.forEach((e, index) => {
e.style.pointerEvents = pointerEvents[index];
});
return elements;
}