You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

947 lines
26 KiB

var markdownTitle = "";
let aiSelected = "";
let componentID = "";
const MarkdownIt = window.markdownit;
// 创建 markdown-it 实例
const md = new MarkdownIt();
// // 函数:将AST转换为JSON
function astToJson(tokens) {
const result = [];
let currentSection = null;
tokens.forEach((token) => {
if (token.type === "heading_open") {
const level = parseInt(token.tag.slice(1));
currentSection = {
type: "heading",
level: level,
text: "",
};
result.push(currentSection);
} else if (token.type === "inline" && currentSection) {
currentSection.text += token.content;
} else if (token.type === "paragraph_open") {
currentSection = {
type: "paragraph",
text: "",
};
result.push(currentSection);
} else if (
token.type === "inline" &&
currentSection &&
currentSection.type === "paragraph"
) {
currentSection.text += token.content;
}
});
return result;
}
var CustomHookA = Cherry.createSyntaxHook(
"codeBlock",
Cherry.constants.HOOKS_TYPE_LIST.PAR,
{
makeHtml(str) {
console.warn("custom hook", "hello");
return str;
},
rule(str) {
const regex = {
begin: "",
content: "",
end: "",
};
regex.reg = new RegExp(regex.begin + regex.content + regex.end, "g");
return regex;
},
}
);
/**
* 自定义一个自定义菜单
* 点第一次时,把选中的文字变成同时加粗和斜体
* 保持光标选区不变,点第二次时,把加粗斜体的文字变成普通文本
*/
var customMenuA = Cherry.createMenuHook("加粗斜体", {
iconName: "font",
onClick: function (selection) {
// 获取用户选中的文字,调用getSelection方法后,如果用户没有选中任何文字,会尝试获取光标所在位置的单词或句子
let $selection = this.getSelection(selection) || "同时加粗斜体";
// 如果是单选,并且选中内容的开始结束内没有加粗语法,则扩大选中范围
if (!this.isSelections && !/^\s*(\*\*\*)[\s\S]+(\1)/.test($selection)) {
this.getMoreSelection("***", "***", () => {
const newSelection = this.editor.editor.getSelection();
const isBoldItalic = /^\s*(\*\*\*)[\s\S]+(\1)/.test(newSelection);
if (isBoldItalic) {
$selection = newSelection;
}
return isBoldItalic;
});
}
// 如果选中的文本中已经有加粗语法了,则去掉加粗语法
if (/^\s*(\*\*\*)[\s\S]+(\1)/.test($selection)) {
return $selection.replace(
/(^)(\s*)(\*\*\*)([^\n]+)(\3)(\s*)($)/gm,
"$1$4$7"
);
}
/**
* 注册缩小选区的规则
* 注册后,插入“***TEXT***”,选中状态会变成“***【TEXT】***”
* 如果不注册,插入后效果为:“【***TEXT***】”
*/
this.registerAfterClickCb(() => {
this.setLessSelection("***", "***");
});
return $selection.replace(/(^)([^\n]+)($)/gm, "$1***$2***$3");
},
});
/**
* 定义一个空壳,用于自行规划cherry已有工具栏的层级结构
*/
var customMenuB = Cherry.createMenuHook("实验室", {
icon: {
type: "svg",
content:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M8 14s1.5 2 4 2 4-2 4-2" /><line x1="9" y1="9" x2="9.01" y2="9" /><line x1="15" y1="9" x2="15.01" y2="9" /></svg>',
iconStyle: "width: 15px; height: 15px; vertical-align: middle;",
},
});
var exportDataHook = Cherry.createMenuHook("导出", {
subMenuConfig: [
{
noIcon: true,
name: "思维导图",
onclick: () => {
if (!markdownTitle || markdownTitle == "") {
markdownTitle = window.prompt("请输入文稿标题");
}
const postData = {
title: markdownTitle,
content: cherry.getMarkdown(),
};
window.parent.postMessage({ type: "saveMind", data: postData }, "*");
},
},
{
noIcon: true,
name: "PPTX",
onclick: () => {
if (!markdownTitle || markdownTitle == "") {
markdownTitle = window.prompt("请输入文稿标题");
}
const markdownContent = cherry.getMarkdown();
createPPTDialog(markdownContent);
},
},
{
noIcon: true,
name: "PDF",
onclick: () => {
cherry.export();
},
},
{
noIcon: true,
name: "长图",
onclick: () => {
cherry.export("img");
},
},
{
noIcon: true,
name: "markdown",
onclick: () => {
cherry.export("markdown");
},
},
{
noIcon: true,
name: "word",
onclick: () => {
if (!markdownTitle || markdownTitle == "") {
markdownTitle = window.prompt("请输入文稿标题");
}
const content = cherry.getHtml();
const converted = htmlDocx.asBlob(content);
// 创建一个隐藏的 <a> 元素
const a = document.createElement("a");
a.style.display = "none";
document.body.appendChild(a);
// 设置 <a> 元素的 href 属性为 Blob URL
a.href = URL.createObjectURL(converted);
a.download = `${markdownTitle}.docx`;
// 触发点击事件
a.click();
// 清理
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
},
},
{
noIcon: true,
name: "html",
onclick: () => {
cherry.export("html");
},
},
],
});
// 生成ppt弹窗
function createPPTDialog(markdownContent) {
// 将Markdown内容转换为HTML
const htmlContent = cherry.getHtml();
// 解析Markdown为AST
const tokens = md.parse(markdownContent, {});
// 将AST转换为JSON
const jsonResult = astToJson(tokens);
console.log(JSON.stringify(jsonResult, null, 2));
const dialog = document.createElement("div");
dialog.className = "cherry-pptx-dialog";
dialog.innerHTML = `
<div class="cherry-pptx-header">
<span>导出为PPTX</span>
<button class="close-btn">X</button>
</div>
<div class="cherry-pptx-content">
<div class="cherry-pptx-preview">
<h3>内容预览</h3>
<div id="markdown-preview">${htmlContent}</div>
</div>
<div class="cherry-pptx-options">
<h3>选择模板</h3>
<div class="template-list">
${["基础", "科技", "总结", "产品", "太空人", "党建", "简约紫"]
.map(
(item) =>
`<div class="template-card" data-template="${item}">${item}</div>`
)
.join("")}</div>
</div>
</div>
</div>
<div class="cherry-pptx-footer">
<button id="ppt-btn">一键生成PPT</button>
</div>
`;
document.body.appendChild(dialog);
// 关闭按钮事件
dialog.querySelector(".close-btn").addEventListener("click", () => {
document.body.removeChild(dialog);
});
// 模板卡片点击事件
let selectedTemplate;
const templateCards = dialog.querySelectorAll(".template-card");
templateCards.forEach((card) => {
card.addEventListener("click", () => {
templateCards.forEach((c) => c.classList.remove("selected"));
card.classList.add("selected");
selectedTemplate = card.getAttribute("data-template");
});
});
// 生成PPT按钮事件
dialog.querySelector("#ppt-btn").addEventListener("click", () => {
if (selectedTemplate === undefined) {
showModal({
titleText: "提示",
contentText: "请选择一个模板",
showOk: true,
});
return;
}
const postData = {
title: markdownTitle,
content: markdownContent,
template: selectedTemplate,
};
console.log(postData);
window.parent.postMessage({ type: "savePPT", data: postData }, "*");
document.body.removeChild(dialog);
});
}
class AiDialogClass {
actionArr = {
creation_leader: {
title: "文章选择",
btn: ["aiCancle", "aiOutline"],
},
creation_builder: {
title: "大纲",
btn: ["aiAdd", "aiReplace", "aiCancle", "aiArticle"],
},
article: {
title: "文章",
btn: ["aiAdd", "aiReplace", "aiCancle", "checkOutline"],
},
creation_continuation: {
title: "续写",
btn: ["aiAdd", "aiReplace", "aiCancle"],
},
creation_optimization: {
title: "优化",
btn: ["aiAdd", "aiReplace", "aiCancle"],
},
creation_proofreading: {
title: "纠错",
btn: ["aiAdd", "aiReplace", "aiCancle"],
},
creation_summarize: {
title: "总结",
btn: ["aiAdd", "aiReplace", "aiCancle"],
},
creation_translation: {
title: "翻译",
btn: ["aiAdd", "aiReplace", "aiCancle"],
},
};
buttons = [
{
title: "追加",
action: "aiAdd",
method: this.addAiContent,
},
{
title: "替换",
action: "aiReplace",
method: this.replaceAiContent,
},
{
title: "舍弃",
action: "aiCancle",
method: this.closeDialog,
},
{
title: "生成大纲",
action: "aiOutline",
method: this.createAiOutline,
},
{
title: "生成文章",
action: "aiArticle",
method: this.createAiArticle,
},
{
title: "查看大纲",
action: "checkOutline",
method: this.cheakAiContent,
},
];
outline = {
title: "",
content: "",
category: "",
};
constructor() {
this.container = document.querySelector("#aiDialog");
this.createDialog();
}
// 生成弹窗的基本框架
createDialog() {
const dialog = document.createElement("div");
dialog.innerHTML = `
<div class="ai-markdown-dialog" style="padding: 0px 15px;">
<div class="ai-dialog-title">${this.action}</div>
<div class="ai-content-box">
<div class="ai-mask"><i id="aiLoader"></i></div>
<textarea class="ai-dialog-content" ></textarea>
</div>
<div class="ai-outline-choose hide">
<div>
<label for="">标题:</label>
<input id="aiTitle" type="text" placeholder="请输入标题">
</div>
<div>
<label for="">分类:</label>
<select name="articleClassification" id="aiSelect">
<option value="论文">论文</option>
<option value="合同">合同</option>
<option value="项目">项目</option>
<option value="投标">投标</option>
<option value="招标">招标</option>
<option value="散文">散文</option>
<option value="产品说明">产品说明</option>
<option value="销售计划">销售计划</option>
<option value="年度计划">年度计划</option>
<option value="战略规划">战略规划</option>
</select>
</div>
</div>
<div class="button-box">
<button class="ai-dialog-button" data-type="aiOutline">生成大纲</button>
<button class="ai-dialog-button" id="checkOutline" data-type="checkOutline">修改大纲</button>
<button class="ai-dialog-button" data-type="aiArticle">生成文章</button>
<button class="ai-dialog-button" data-type="aiAdd">追加</button>
<button class="ai-dialog-button" data-type="aiReplace">替换</button>
<button class="ai-dialog-button" data-type="aiCancle">取消</button>
</div>
</div>`;
this.container.appendChild(dialog);
// 绑定事件
const aiOptions = Array.from(
this.container.querySelectorAll(".ai-dialog-button")
);
aiOptions.forEach((item) => {
item.addEventListener("click", () => {
const actionType = item.getAttribute("data-type");
const btn = this.buttons.find((item) => item.action == actionType);
if (btn && btn.method) {
btn.method(this.container, this);
}
});
});
}
// 打开弹窗
openDialog(action) {
this.container.classList.remove("hide");
const title = "AI - " + this.actionArr[action].title;
this.container.querySelector(".ai-dialog-title").innerText = title;
this.addButton(action);
}
// 根据不同选择,展示不同弹窗
addButton(action) {
if (action == "creation_leader") {
this.container
.querySelector(".ai-outline-choose")
.classList.remove("hide");
this.container.querySelector(".ai-content-box").classList.add("hide");
} else {
this.container.querySelector(".ai-outline-choose").classList.add("hide");
this.container.querySelector(".ai-content-box").classList.remove("hide");
this.container.querySelector(".ai-mask").classList.remove("hide");
}
const buttonArr = Array.from(
this.container.querySelectorAll(".ai-dialog-button")
);
buttonArr.forEach((item) => {
const actionType = item.getAttribute("data-type");
if (this.actionArr[action].btn.indexOf(actionType) !== -1) {
item.classList.remove("hide");
} else {
item.classList.add("hide");
}
});
}
// 将内容放入textarea中
putInTextarea(action, content) {
// this.sendRequest(action,content)
this.container.querySelector("textarea").value = content;
this.container.querySelector(".ai-mask").classList.add("hide");
}
// 发送请求
sendRequest(action, data) {
if (action !== "creation_leader") {
window.parent.postMessage(
{
type: "aiCreater",
data,
action,
},
"*"
);
}
this.openDialog(action);
}
// 关闭弹窗
closeDialog(dialog) {
dialog.querySelector("textarea").value = "";
this.outline = {
title: "",
content: "",
category: "",
};
dialog?.classList.add("hide");
}
// 追加
addAiContent(dialog, that) {
const content = dialog.querySelector("textarea").value;
cherry.insert(content, false);
that.closeDialog(dialog);
}
// 替换
replaceAiContent(dialog, that) {
const content = dialog.querySelector("textarea").value;
cherry.setMarkdown(content);
that.closeDialog(dialog);
}
// 生成大纲
createAiOutline(dialog, that) {
that.addButton("creation_leader");
const content = {
title: dialog.querySelector("#aiTitle").value,
category: dialog.querySelector("#aiSelect").value,
};
that.outline.title = content.title;
that.outline.category = content.category;
if (content.title !== "") {
window.parent.postMessage(
{
type: "aiCreater",
data: content,
action: "creation_leader",
},
"*"
);
that.openDialog("creation_builder");
} else {
showModal({
titleText: "提示",
contentText: "请输入标题",
showOk: true,
});
}
}
// 生成文章
createAiArticle(dialog, that) {
const data = {
outline: dialog.querySelector("textarea").value,
title: that.outline.title || "AI - 文章",
};
that.outline.content = data.outline;
window.parent.postMessage(
{
type: "aiCreater",
data,
action: "creation_builder",
},
"*"
);
that.openDialog("article");
}
//查看大纲
cheakAiContent(dialog, that) {
that.openDialog("creation_builder");
dialog.querySelector("textarea").value = that.outline.content;
dialog.querySelector(".ai-mask").classList.add("hide");
}
}
const aiDialog = new AiDialogClass();
const aiTips = (action) => {
if (aiSelected == "") {
showModal({
titleText: "提示",
contentText: "请选择内容",
showOk: true,
});
return;
}
aiDialog.sendRequest(action, aiSelected);
};
var aiEditMenu = Cherry.createMenuHook("AI", {
subMenuConfig: [
{
noIcon: true,
name: "优化",
onclick: () => {
aiTips("creation_optimization");
},
},
{
noIcon: true,
name: "纠错",
onclick: () => {
aiTips("creation_proofreading");
},
},
{
noIcon: true,
name: "续写",
onclick: () => {
aiTips("creation_continuation");
},
},
{
noIcon: true,
name: "翻译",
onclick: () => {
aiTips("creation_translation");
},
},
{
noIcon: true,
name: "总结",
onclick: () => {
aiTips("creation_summarize");
},
},
{
noIcon: true,
name: "大纲",
onclick: () => {
aiDialog.openDialog("creation_leader");
//aiDialog.sendRequest('creation_leader', cherry.getMarkdown())
},
},
],
});
const saveData = () => {
//console.log(markdownTitle)
if (!markdownTitle || markdownTitle == "") {
markdownTitle = window.prompt("请输入文稿标题");
}
const postData = { title: markdownTitle, content: cherry.getMarkdown() };
window.parent.postMessage(
{ type: "exportMd", data: JSON.stringify(postData), componentID },
"*"
);
};
var saveMenu = Cherry.createMenuHook("保存", {
onClick: function () {
//console.log(selection)
saveData();
},
});
/**
* 定义带图表表格的按钮
*/
var customMenuTable = Cherry.createMenuHook("图表", {
iconName: "trendingUp",
subMenuConfig: [
{
noIcon: true,
name: "折线图",
onclick: (event) => {
cherry.insert(
"\n| :line:{x,y} | Header1 | Header2 | Header3 | Header4 |\n| ------ | ------ | ------ | ------ | ------ |\n| Sample1 | 11 | 11 | 4 | 33 |\n| Sample2 | 112 | 111 | 22 | 222 |\n| Sample3 | 333 | 142 | 311 | 11 |\n"
);
},
},
{
noIcon: true,
name: "柱状图",
onclick: (event) => {
cherry.insert(
"\n| :bar:{x,y} | Header1 | Header2 | Header3 | Header4 |\n| ------ | ------ | ------ | ------ | ------ |\n| Sample1 | 11 | 11 | 4 | 33 |\n| Sample2 | 112 | 111 | 22 | 222 |\n| Sample3 | 333 | 142 | 311 | 11 |\n"
);
},
},
],
});
var basicConfig = {
id: "markdown",
externals: {
echarts: window.echarts,
katex: window.katex,
MathJax: window.MathJax,
},
isPreviewOnly: false,
engine: {
global: {
urlProcessor(url, srcType) {
console.log(`url-processor`, url, srcType);
return url;
},
},
syntax: {
image: {
videoWrapper: (link, type, defaultWrapper) => {
console.log(type);
return defaultWrapper;
},
},
autoLink: {
/** 生成的<a>标签追加target属性的默认值 空:在<a>标签里不会追加target属性, _blank:在<a>标签里追加target="_blank"属性 */
target: "",
/** 生成的<a>标签追加rel属性的默认值 空:在<a>标签里不会追加rel属性, nofollow:在<a>标签里追加rel="nofollow:在"属性*/
rel: "",
/** 是否开启短链接 */
enableShortLink: true,
/** 短链接长度 */
shortLinkLength: 20,
},
codeBlock: {
theme: "twilight",
wrap: true, // 超出长度是否换行,false则显示滚动条
lineNumber: true, // 默认显示行号
copyCode: true, // 是否显示“复制”按钮
editCode: true, // 是否显示“编辑”按钮
changeLang: true, // 是否显示“切换语言”按钮
expandCode: false, // 是否展开/收起代码块,当代码块行数大于10行时,会自动收起代码块
selfClosing: true, // 自动闭合,为true时,当md中有奇数个```时,会自动在md末尾追加一个```
},
table: {
enableChart: true,
},
fontEmphasis: {
allowWhitespace: false, // 是否允许首尾空格
},
strikethrough: {
needWhitespace: false, // 是否必须有前后空格
},
mathBlock: {
engine: "katex", // katex或MathJax
src: "/markdown/katex/katex.min.js",
//src: 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js', // 如果使用MathJax plugins,则需要使用该url通过script标签引入
},
inlineMath: {
engine: "katex", // katex或MathJax
},
// emoji: {
// useUnicode: true,
// customResourceURL: 'https://github.githubassets.com/images/icons/emoji/unicode/${code}.png?v8',
// upperCase: false,
// },
// htmlBlock: {
// filterStyle: true,
// }
toc: {
tocStyle: "nested",
},
// 'header': {
// strict: false
// }
},
customSyntax: {
// SyntaxHookClass
CustomHook: {
syntaxClass: CustomHookA,
force: false,
after: "br",
},
},
},
multipleFileSelection: {
video: true,
audio: false,
image: true,
word: false,
pdf: true,
file: true,
},
toolbars: {
toolbar: [
"undo",
"redo",
"|",
// 把字体样式类按钮都放在加粗按钮下面
{
bold: [
"bold",
"italic",
"underline",
"strikethrough",
"sub",
"sup",
"ruby",
"customMenuAName",
],
},
// 'bold',
// 'italic',
// {
// strikethrough: ['strikethrough', 'underline', 'sub', 'sup', 'ruby'],
// },
"size",
"|",
"color",
"header",
{
ol: ["ol", "ul", "checklist", "panel"],
},
"justify",
//'formula',
{
insert: [
"drawIo",
"image",
"audio",
"video",
"link",
"ruby",
"detail",
"hr",
"br",
"code",
"inlineCode",
"formula",
"toc",
"table",
"pdf",
"word",
"file",
],
},
"graph",
"customMenuTable",
//'togglePreview',
//'codeTheme',
"fullScreen",
"search",
"togglePreview",
],
toolbarRight: [
"saveMenu",
"|",
"exportDataHook",
"changeLocale",
"wordCount",
"aiEditMenu",
],
bubble: [
"bold",
"italic",
"underline",
"strikethrough",
"sub",
"sup",
"quote",
"ruby",
"|",
"size",
"color",
], // array or false
//sidebar: ['mobilePreview', 'copy', 'theme', 'publish'],
sidebar: ["mobilePreview", "copy", "shortcutKey", "theme"],
toc: {
updateLocationHash: true, // 要不要更新URL的hash
defaultModel: "pure", // pure: 精简模式/缩略模式,只有一排小点; full: 完整模式,会展示所有标题
},
//toc: false,
customMenu: {
customMenuAName: customMenuA,
customMenuBName: customMenuB,
saveMenu,
customMenuTable,
exportDataHook,
aiEditMenu,
},
shortcutKeySettings: {
/** 是否替换已有的快捷键, true: 替换默认快捷键; false: 会追加到默认快捷键里,相同的shortcutKey会覆盖默认的 */
isReplace: false,
shortcutKeyMap: {
"Control-S": {
hookName: "saveMenu",
aliasName: "保存",
},
"Alt-Digit1": {
hookName: "header",
aliasName: "标题",
},
"Control-Shift-KeyX": {
hookName: "bold",
aliasName: "加粗",
},
},
},
},
drawioIframeUrl: "./drawio.html",
previewer: {
// 自定义markdown预览区域class
// className: 'markdown'
floatWhenClosePreviewer: true,
},
keydown: [],
//extensions: [],
callback: {
changeString2Pinyin: pinyin,
onClickPreview: (event) => {
console.log("onClickPreview", event);
},
},
event: {
selectionChange: ({ selections, lastSelections, info }) => {
aiSelected = lastSelections[0];
//console.log(aiSelected)
},
},
editor: {
id: "cherry-text",
name: "cherry-text",
autoSave2Textarea: true,
//defaultModel: 'editOnly',
defaultModel: "edit&preview",
showFullWidthMark: true, // 是否高亮全角符号 ·|¥|、|:|“|”|【|】|(|)|《|》
showSuggestList: true, // 是否显示联想框
},
// cherry初始化后是否检查 location.hash 尝试滚动到对应位置
autoScrollByHashAfterInit: true,
// locale: 'en_US',
themeSettings: {
mainTheme: "light",
},
};
var config = Object.assign({}, basicConfig, { value: "" });
window.cherry = new Cherry(config);
const debouncedHandleKeyDown = (event) => {
// 确保仅在我们的按钮获得焦点时处理快捷键
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") {
event.stopPropagation(); // 先阻止事件冒泡
event.preventDefault(); // 再阻止默认行为
saveData();
}
};
function isBase64(str) {
if (str === "" || str.trim() === "") {
return false;
}
try {
return btoa(atob(str)) == str;
} catch (err) {
return false;
}
}
function decodeBase64(base64String) {
// 将Base64字符串分成每64个字符一组
const padding =
base64String.length % 4 === 0 ? 0 : 4 - (base64String.length % 4);
base64String += "=".repeat(padding);
// 使用atob()函数解码Base64字符串
const binaryString = atob(base64String);
// 将二进制字符串转换为TypedArray
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// 将TypedArray转换为字符串
return new TextDecoder("utf-8").decode(bytes);
}
const eventHandler = (e) => {
const eventData = e.data;
componentID = eventData.componentID ? eventData.componentID : "";
if (eventData.type === "start") {
markdownTitle = eventData.title || "未命名文稿";
return;
}
// console.log(markdownTitle)
if (eventData.type === "init") {
const data = eventData.data;
markdownTitle = data.title || "未命名文稿";
if (!data) {
return;
}
let content = data.content;
if (isBase64(content)) {
content = decodeBase64(content);
} else if (content instanceof ArrayBuffer) {
content = new TextDecoder("utf-8").decode(content);
}
cherry.setMarkdown(content);
}
if (eventData.type == "aiReciver") {
aiDialog.putInTextarea(eventData.action, eventData.data);
}
};
window.addEventListener("load", () => {
window.parent.postMessage({ type: "initSuccess" }, "*");
window.addEventListener("message", eventHandler);
document.addEventListener("keydown", debouncedHandleKeyDown);
});
window.addEventListener("unload", () => {
window.removeEventListener("message", eventHandler);
document.removeEventListener("keydown", debouncedHandleKeyDown);
});