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: '', 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); // 创建一个隐藏的 元素 const a = document.createElement("a"); a.style.display = "none"; document.body.appendChild(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 = ` 导出为PPTX X 内容预览 ${htmlContent} 选择模板 ${["基础", "科技", "总结", "产品", "太空人", "党建", "简约紫"] .map( (item) => `${item}` ) .join("")} `; 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 = ` ${this.action} 标题: 分类: 论文 合同 项目 投标 招标 散文 产品说明 销售计划 年度计划 战略规划 生成大纲 修改大纲 生成文章 追加 替换 取消 `; 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: { /** 生成的标签追加target属性的默认值 空:在标签里不会追加target属性, _blank:在标签里追加target="_blank"属性 */ target: "", /** 生成的标签追加rel属性的默认值 空:在标签里不会追加rel属性, nofollow:在标签里追加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); });