您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a markdown editor to Canvas
当前为
// ==UserScript== // @name Canvas Markdown // @namespace https://theusaf.org // @version 2.1.2 // @description Adds a markdown editor to Canvas // @author theusaf // @supportURL https://github.com/theusaf/canvas-markdown/issues // @copyright (c) 2023-2024 theusaf // @homepage https://github.com/theusaf/canvas-markdown // @license MIT // @match https://*/* // @grant none // ==/UserScript== let highlight, languages; try { if (new URL(document.querySelector("#global_nav_help_link") ?.href ?? "")?.hostname === "help.instructure.com") { console.log("[Canvas Markdown] Detected Canvas page, loading..."); (async () => { console.log("[Canvas Markdown] Importing dependencies..."); await import("https://cdn.jsdelivr.net/gh/theusaf/canvas-markdown@5216c569489b9aa2caa6aee49ef8aadabb1f1794/lib/codemirror/codemirror.js"); await import("https://cdn.jsdelivr.net/gh/theusaf/canvas-markdown@5216c569489b9aa2caa6aee49ef8aadabb1f1794/lib/codemirror/mode/markdown/markdown.js"); highlight = (await import("https://cdn.jsdelivr.net/gh/theusaf/canvas-markdown@5216c569489b9aa2caa6aee49ef8aadabb1f1794/lib/highlight/es/core.min.js")).default; languages = (await import("https://cdn.jsdelivr.net/gh/theusaf/canvas-markdown@5216c569489b9aa2caa6aee49ef8aadabb1f1794/lib/highlight/languages.js")).default; const s = document.createElement("script"); s.src = "https://cdn.jsdelivr.net/npm/[email protected]/dist/showdown.min.js"; document.head.append(s); const codemirrorCSS = document.createElement("link"); codemirrorCSS.rel = "stylesheet"; codemirrorCSS.href = "https://cdn.jsdelivr.net/gh/theusaf/canvas-markdown@5216c569489b9aa2caa6aee49ef8aadabb1f1794/lib/codemirror/codemirror.css"; const highlightCSS = document.createElement("link"); highlightCSS.rel = "stylesheet"; highlightCSS.href = "https://cdn.jsdelivr.net/gh/theusaf/canvas-markdown@5216c569489b9aa2caa6aee49ef8aadabb1f1794/lib/highlight/styles/github-dark.min.css"; document.head.append(highlightCSS); document.head.append(codemirrorCSS); console.log("[Canvas Markdown] Setting up..."); setupWatcher(); console.log("[Canvas Markdown] Done."); })(); } else { console.log("[Canvas Markdown] Not a Canvas page, skipping..."); } } catch (e) { /* ignore */ } function getEditorElements() { return [ ...document.querySelectorAll(".ic-RichContentEditor:not([md-id=canvas-container])"), ]; } function setupWatcher() { setInterval(() => { const potentialEditorElements = getEditorElements(); if (potentialEditorElements.length) { for (const editorElement of potentialEditorElements) { const markdownEditor = new MarkdownEditor(editorElement); markdownEditor.setup(); } } }, 1e3); } var MarkdownEditorMode; (function (MarkdownEditorMode) { MarkdownEditorMode[MarkdownEditorMode["RAW"] = 0] = "RAW"; MarkdownEditorMode[MarkdownEditorMode["PRETTY"] = 1] = "PRETTY"; })(MarkdownEditorMode || (MarkdownEditorMode = {})); // https://developer.mozilla.org/en-US/docs/Web/API/btoa#unicode_strings function toBinary(str) { const codeUnits = Uint16Array.from({ length: str.length }, (_, index) => str.charCodeAt(index)), charCodes = new Uint8Array(codeUnits.buffer); let result = ""; charCodes.forEach((char) => { result += String.fromCharCode(char); }); return result; } function fromBinary(binary) { const bytes = Uint8Array.from({ length: binary.length }, (element, index) => binary.charCodeAt(index)), charCodes = new Uint16Array(bytes.buffer); let result = ""; charCodes.forEach((char) => { result += String.fromCharCode(char); }); return result; } class MarkdownEditor { editorContainer; canvasTextArea; canvasResizeHandle; canvasSwitchEditorButton; canvasFullScreenButton; markdownTextContainer; markdownPrettyContainer; markdownTextArea; markdownEditor; markdownSettingsButton; markdownSwitchButton; markdownSwitchTypeButton; markdownSettingsExistingContainer; showdownConverter; active = false; mode = MarkdownEditorMode.PRETTY; activating = false; constructor(editor) { this.editorContainer = editor; } setup() { this.editorContainer.setAttribute("md-id", "canvas-container"); if (this.isReady()) { this.canvasTextArea = this.getCanvasTextArea(); this.canvasResizeHandle = this.getCanvasResizeHandle(); this.canvasSwitchEditorButton = this.getCanvasSwitchEditorButton(); this.canvasFullScreenButton = this.getCanvasFullScreenButton(); this.injectMarkdownEditor(); this.setupShowdown(); this.injectMarkdownSettingsButton(); this.injectMarkdownUI(); this.applyEventListeners(); } else { setTimeout(() => this.setup(), 1e3); } } isReady() { return !!(this.getCanvasTextArea() && this.getCanvasResizeHandle() && this.getCanvasSwitchEditorButton() && this.getCanvasFullScreenButton()); } getCanvasFullScreenButton() { return this.editorContainer.querySelector("[data-btn-id=rce-fullscreen-btn]"); } setupShowdown() { showdown.setFlavor("github"); this.showdownConverter = new showdown.Converter({ ghMentions: false, }); } getCanvasResizeHandle() { return this.editorContainer.querySelector("[data-btn-id=rce-resize-handle]"); } getCanvasTextArea() { return this.editorContainer.querySelector("textarea[data-rich_text=true]"); } getCanvasSwitchEditorButton() { return this.editorContainer.querySelector("[data-btn-id=rce-edit-btn]"); } isCanvasInTextMode() { return /rich text/i.test(this.canvasSwitchEditorButton.title); } getCanvasSwitchTypeButton() { return this.editorContainer.querySelector("[data-btn-id=rce-editormessage-btn]"); } isCanvasInPlainTextMode() { return /pretty html/i.test(this.getCanvasSwitchTypeButton().textContent); } insertAfter(newNode, referenceNode) { referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); } injectMarkdownEditor() { const editorContent = document.createElement("template"); editorContent.innerHTML = ` <div md-id="markdown-editor-text-container" style="display: none;"> <textarea md-id="markdown-editor" style="height: 400px; resize: none;"></textarea> </div> <div md-id="markdown-editor-pretty-container"> <div class="RceHtmlEditor"> <div> <label style="display: block"> <span></span> <div class="react-codemirror2" md-id="markdown-editor-codemirror-container"> <!-- Insert CodeMirror editor here --> </div> </label> </div> </div> </div> `; this.editorContainer .querySelector(".rce-wrapper") .prepend(editorContent.content.cloneNode(true)); this.markdownTextContainer = this.editorContainer.querySelector("[md-id=markdown-editor-text-container]"); this.markdownTextArea = this.editorContainer.querySelector("[md-id=markdown-editor]"); this.markdownPrettyContainer = this.editorContainer.querySelector("[md-id=markdown-editor-pretty-container]"); this.markdownEditor = CodeMirror(this.markdownPrettyContainer.querySelector("[md-id=markdown-editor-codemirror-container]"), { mode: "markdown", lineNumbers: true, lineWrapping: true, }); const codeMirrorEditor = this.markdownEditor.getWrapperElement(); codeMirrorEditor.style.height = "400px"; codeMirrorEditor.setAttribute("md-id", "markdown-editor-codemirror"); // Hide the markdown editor. By doing it here, it also allows CodeMirror to // properly render when the editor is shown. this.markdownPrettyContainer.style.display = "none"; } displaySettings() { const settingsUI = document.createElement("template"); settingsUI.innerHTML = ` <div md-id="settings-container"> <style> [md-id=settings-container] { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgb(0, 0, 0, 0.5); z-index: 999; display: flex; } [md-id=settings-container] > div { width: 90%; height: 90%; margin: auto; overflow-y: auto; background-color: white; padding: 1rem; position: relative; border-radius: 0.5rem; } [md-id=settings-container] h2 { margin-top: 1rem; } [md-id=close-button] { position: fixed; top: 2%; right: 4%; padding: 0.5rem; cursor: pointer; width: 1rem; height: 1rem; color: black; font-size: 1.5rem; text-align: center; margin: 0.5rem; text-shadow: black 0 0 0.2rem; } [md-id=settings-form-container] [md-id=settings-existing-container] { display: flex; flex-direction: column; margin-top: 1rem; } [md-id=settings-form-label-container] { display: flex; flex-direction: row; margin-bottom: 0.5rem; } [md-id=settings-form-label-container] > * { font-weight: bold; flex: 1; padding: 0.5rem; font-size: 1.2rem; } [md-id=settings-existing-input-container] { margin-top: 1rem; } [md-id=settings-existing-container] { margin-top: 1rem; border-top: 0.15rem solid #ccc; } [md-id=settings-form-input-container], [md-id=settings-existing-input-container] { display: flex; flex-direction: row; } [md-id=settings-form-input-container] > *, [md-id=settings-existing-input-container] > * { flex: 1; padding: 0.5rem; display: flex; } [md-id=settings-form-input-container] > * > input, [md-id=settings-form-input-container] > * > textarea, [md-id=settings-existing-input-container] > * > input, [md-id=settings-existing-input-container] > * > textarea { flex: 1; } [md-id=settings-form-label-container] > :nth-child(2n + 1), [md-id=settings-form-input-container] > :nth-child(2n + 1), [md-id=settings-existing-input-container] > :nth-child(2n + 1) { background-color: #eee; } [md-id="settings-download-button-container"] > * { padding: 0.5rem; background-color: #eee; border: 0.15rem solid #ccc; border-radius: 0.5rem; margin: 0.5rem; cursor: pointer; } [md-id="settings-form-save-button"] { height: 2rem; } [md-id="settings-form-save-tooltip"] { position: absolute; top: -4.25rem; left: -25%; width: 4rem; height: 4rem; pointer-events: none; background-color: black; text-align: center; border-radius: 0.5rem; align-items: center; justify-content: center; display: flex; color: white; opacity: 0; transition: opacity 0.2s ease-in-out; } </style> <div> <span md-id="close-button">X</span> <h2>Canvas Markdown Settings</h2> <h3>Custom Styles</h3> <p> You can use these settings to customize the default styles of HTML elements in the output. In the form below, input a tag or CSS selector to target in the first section. In the second section, input the CSS properties you want to apply to the element as you would in a style attribute. </p> <div md-id="settings-form-container"> <!-- Insert form here --> <div md-id="settings-form-label-container"> <label for="cm-settings-selector">Selector</label> <label for="cm-settings-style">Style</label> <span>Style Preview</span> </div> <div md-id="settings-form-input-container"> <!-- Insert form inputs here --> </div> </div> <div md-id="settings-existing-container" style=""> <!-- Insert existing settings here --> </div> <h3>Import/Export Settings</h3> <div md-id="settings-download-container"> <!-- Insert download/load settings here --> <div md-id="settings-download-button-container"> <label md-id="settings-download-button">Download Settings</label> <label for="cm-settings-upload-input" md-id="settings-upload-button"> Upload Settings </label> <input type="file" accept=".json" id="cm-settings-upload-input" md-id="settings-upload-input" style="display: none;" /> </div> </div> </div> </div> `; document.body.append(settingsUI.content.cloneNode(true)); const settingsContainer = document.querySelector("[md-id=settings-container]"), closeButton = document.querySelector("[md-id=close-button]"), downloadButton = document.querySelector("[md-id=settings-download-button]"), uploadButton = document.querySelector("[md-id=settings-upload-button]"); closeButton.addEventListener("click", () => { settingsContainer.remove(); }); downloadButton.addEventListener("click", () => { const settings = this.loadSettings(); const blob = new Blob([JSON.stringify(settings)], { type: "application/json", }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "canvas-markdown-settings.json"; a.click(); URL.revokeObjectURL(url); }); uploadButton.addEventListener("click", () => { const input = document.querySelector("[md-id=settings-upload-input]"); input.onchange = () => { const file = input.files[0]; if (file.type !== "application/json") { alert("Invalid file type"); return; } const reader = new FileReader(); reader.onload = () => { try { const settings = JSON.parse(reader.result); this.saveSettings(settings); settingsContainer.remove(); this.displaySettings(); } catch (e) { alert("Invalid file"); } }; reader.readAsText(file); }; }); this.markdownSettingsExistingContainer = document.querySelector("[md-id=settings-existing-container]"); const settings = this.loadSettings(); for (const setting of settings.customStyles) { const container = this.createExistingSettingsContainer(); this.markdownSettingsExistingContainer.append(container); this.addSettingsForm(container, setting, true); } this.addSettingsForm(document.querySelector("[md-id=settings-form-input-container]"), null, false); } addSettingsForm(formContainer, setting = null, isExisting = true) { const formInputTemplate = document.createElement("template"); formInputTemplate.innerHTML = ` <span> <input type="text" id="cm-settings-selector" md-id="settings-form-selector" placeholder="e.g. h1, .header, #header" /> </span> <span> <textarea id="cm-settings-style" md-id="settings-form-style" placeholder="e.g. color: red; font-weight: bold;"></textarea> </span> <span style="justify-content: space-between; display: flex;"> <div> <span md-id="settings-form-style-preview">Hello World</span> </div> <div> <span style="position: relative"> <span md-id="settings-form-save-tooltip"></span> <button md-id="settings-form-save-button">Save</button> </span> <button md-id="settings-form-delete-button" style="margin-left: 0.5rem">Delete</button> </div> </span> `; formContainer.append(formInputTemplate.content.cloneNode(true)); const saveButton = formContainer.querySelector("[md-id=settings-form-save-button]"), deleteButton = formContainer.querySelector("[md-id=settings-form-delete-button]"), stylePreview = formContainer.querySelector("[md-id=settings-form-style-preview]"), saveTooltip = formContainer.querySelector("[md-id=settings-form-save-tooltip]"), selectorInput = formContainer.querySelector("[md-id=settings-form-selector]"), styleInput = formContainer.querySelector("[md-id=settings-form-style]"); if (!isExisting) { deleteButton.style.display = "none"; } if (setting) { selectorInput.value = setting.target; styleInput.value = setting.style; stylePreview.style.cssText = setting.style; } // Add event listeners deleteButton.addEventListener("click", () => { formContainer.remove(); this.saveSettingsFromForm(); }); saveButton.addEventListener("click", () => { const isValid = this.isSettingsValid({ target: selectorInput.value, style: styleInput.value, }); if (isValid !== true) { saveTooltip.style.opacity = "1"; saveTooltip.textContent = isValid; saveTooltip.style.backgroundColor = "red"; setTimeout(() => { saveTooltip.style.opacity = "0"; }, 500); } else { if (!isExisting) { const container = this.createExistingSettingsContainer(); this.markdownSettingsExistingContainer.append(container); this.addSettingsForm(container, { target: selectorInput.value, style: styleInput.value, }, true); selectorInput.value = ""; styleInput.value = ""; stylePreview.style.cssText = ""; } this.saveSettingsFromForm(); saveTooltip.style.opacity = "1"; saveTooltip.textContent = "Saved!"; saveTooltip.style.backgroundColor = "green"; setTimeout(() => { saveTooltip.style.opacity = "0"; }, 500); } }); styleInput.addEventListener("input", () => { stylePreview.style.cssText = styleInput.value; }); } createExistingSettingsContainer() { const existingSettingsContainer = document.createElement("div"); existingSettingsContainer.setAttribute("md-id", "settings-existing-input-container"); return existingSettingsContainer; } isSettingsValid(settings) { const { target, style } = settings; if (!target.trim() || !style.trim()) return "Empty inputs"; try { document.querySelector(target); } catch (e) { return "Invalid selector"; } return true; } saveSettingsFromForm() { const settings = this.getSettingsFromForm(); this.saveSettings({ customStyles: settings, }); } getSettingsFromForm() { const formContainers = [ ...this.markdownSettingsExistingContainer.querySelectorAll("[md-id=settings-existing-input-container]"), ], settings = []; for (const formContainer of formContainers) { const selectorInput = formContainer.querySelector("[md-id=settings-form-selector]"), styleInput = formContainer.querySelector("[md-id=settings-form-style]"), setting = { target: selectorInput.value, style: styleInput.value, }; if (this.isSettingsValid(setting) === true) settings.push(setting); } return settings; } loadSettings() { const defaultSettings = { customStyles: [], }; const settings = JSON.parse(window.localStorage.getItem("canvas-markdown-settings") ?? "{}"); return { ...defaultSettings, ...settings, }; } saveSettings(settings) { const existingSettings = this.loadSettings(); window.localStorage.setItem("canvas-markdown-settings", JSON.stringify({ ...existingSettings, ...settings, })); } applyEventListeners() { let updateTimeout; const updateData = () => { clearTimeout(updateTimeout); updateTimeout = setTimeout(() => { this.updateCanvasData(); }, 500); }; this.markdownTextArea.addEventListener("input", () => updateData()); this.markdownEditor.on("change", () => { this.markdownTextArea.value = this.markdownEditor.getValue(); updateData(); }); const switchButton = this.canvasSwitchEditorButton; switchButton.onclick = () => { if (this.activating) return; if (this.active) this.deactivate(); }; this.markdownSwitchButton.addEventListener("click", () => { if (this.active) { this.deactivate(); switchButton.click(); } else { this.activate(); } }); this.markdownSettingsButton.addEventListener("click", () => { this.displaySettings(); }); this.canvasFullScreenButton.onclick = () => { setTimeout(() => { this.applyCanvasResizeHandleEventListeners(); this.updateEditorHeight(); }, 500); }; this.applyCanvasResizeHandleEventListeners(); } applyCanvasResizeHandleEventListeners() { if (!this.getCanvasResizeHandle()) return; this.canvasResizeHandle = this.getCanvasResizeHandle(); this.canvasResizeHandle.onmousemove = () => this.updateEditorHeight(); this.canvasResizeHandle.onkeydown = () => this.updateEditorHeight(); } updateEditorHeight() { const height = this.canvasTextArea.style.height; this.markdownTextArea.style.height = height; this.markdownEditor.getWrapperElement().style.height = height; } activate() { this.active = true; this.activating = true; this.markdownTextContainer.style.display = "none"; this.markdownPrettyContainer.style.display = "block"; if (!this.isCanvasInTextMode()) { this.canvasSwitchEditorButton.click(); } this.injectMarkdownSwitchTypeButton(); this.mode = MarkdownEditorMode.PRETTY; if (this.markdownSwitchTypeButton) { this.markdownSwitchTypeButton.style.display = "block"; this.markdownSwitchTypeButton.textContent = "Switch to Raw Markdown editor"; } this.getCanvasSwitchTypeButton().style.display = "none"; if (!this.isCanvasInPlainTextMode()) { this.getCanvasSwitchTypeButton().click(); } const markdownCode = this.extractMarkdown(this.canvasTextArea.value); this.markdownTextArea.value = markdownCode; this.markdownEditor.setValue(markdownCode); this.canvasTextArea.parentElement.style.display = "none"; this.markdownEditor.focus(); this.activating = false; } deactivate() { this.active = false; this.markdownTextContainer.style.display = "none"; this.markdownPrettyContainer.style.display = "none"; if (this.markdownSwitchTypeButton) { this.markdownSwitchTypeButton.style.display = "none"; } if (this.getCanvasSwitchTypeButton()) { this.getCanvasSwitchTypeButton().style.display = "block"; } this.canvasTextArea.parentElement.style.display = "block"; } async updateCanvasData() { const markdownCode = this.markdownTextArea.value, output = await this.generateOutput(markdownCode); this.canvasTextArea.value = output; this.activateCanvasCallbacks(); } activateCanvasCallbacks() { const customEvent = new Event("input"); customEvent.keyCode = 13; customEvent.which = 13; customEvent.location = 0; customEvent.code = "Enter"; customEvent.key = "Enter"; this.canvasTextArea.dispatchEvent(customEvent); } injectMarkdownUI() { const markdownSwitchButton = document.createElement("button"), switchButton = this.canvasSwitchEditorButton; markdownSwitchButton.setAttribute("type", "button"); markdownSwitchButton.setAttribute("title", "Switch to Markdown editor"); markdownSwitchButton.className = switchButton.className; markdownSwitchButton.setAttribute("style", switchButton.style.cssText); const markdownSwitchButtonContent = document.createElement("template"); markdownSwitchButtonContent.innerHTML = ` <span class="${switchButton.firstElementChild.className}"> <span class="${switchButton.firstElementChild.firstElementChild.className}" style="${switchButton.firstElementChild.firstElementChild.style .cssText} direction="row" wrap="no-wrap"> <span class="${switchButton.firstElementChild.firstElementChild.firstElementChild .className}"> <span>M🠗</span> </span> </span> </span> `; markdownSwitchButton.append(markdownSwitchButtonContent.content.cloneNode(true)); this.markdownSwitchButton = markdownSwitchButton; this.insertAfter(markdownSwitchButton, switchButton); } injectMarkdownSettingsButton() { const settingsButton = document.createElement("button"), settingsButtonContent = document.createElement("template"), switchButton = this.canvasSwitchEditorButton; settingsButton.setAttribute("type", "button"); settingsButton.setAttribute("title", "Markdown settings"); settingsButton.className = switchButton.className; settingsButton.setAttribute("style", switchButton.style.cssText); settingsButtonContent.innerHTML = ` <span class="${switchButton.firstElementChild.className}"> <span class="${switchButton.firstElementChild.firstElementChild.className}" style="${switchButton.firstElementChild.firstElementChild.style .cssText} direction="row" wrap="no-wrap"> <span class="${switchButton.firstElementChild.firstElementChild.firstElementChild .className}"> <span>M⚙</span> </span> </span> </span> `; settingsButton.append(settingsButtonContent.content.cloneNode(true)); this.markdownSettingsButton = settingsButton; this.insertAfter(settingsButton, switchButton); } injectMarkdownSwitchTypeButton() { if (this.markdownSwitchTypeButton?.isConnected) return; const button = document.createElement("button"), switchButton = this.getCanvasSwitchTypeButton(); button.setAttribute("type", "button"); button.className = switchButton.className; button.setAttribute("style", switchButton.style.cssText); const buttonContent = document.createElement("template"); buttonContent.innerHTML = ` <span class="${switchButton.firstElementChild.className}"> <span class="${switchButton.firstElementChild.firstElementChild.className}" md-id="md-switch-type-button"> Switch to raw Markdown editor </span> </span> `; button.append(buttonContent.content.cloneNode(true)); this.markdownSwitchTypeButton = button; this.insertAfter(button, switchButton); this.markdownSwitchTypeButton.addEventListener("click", () => { if (!this.active) return; if (this.mode === MarkdownEditorMode.PRETTY) { this.mode = MarkdownEditorMode.RAW; this.markdownPrettyContainer.style.display = "none"; this.markdownTextContainer.style.display = "block"; this.markdownSwitchTypeButton.textContent = "Switch to Pretty Markdown editor"; } else { this.mode = MarkdownEditorMode.PRETTY; this.markdownPrettyContainer.style.display = "block"; this.markdownTextContainer.style.display = "none"; this.markdownSwitchTypeButton.textContent = "Switch to Raw Markdown editor"; } }); } /** * Extracts the markdown code from the html comment. */ extractMarkdown(html) { const match = html.match(/<span class="canvas-markdown-code"[^\n]*?>\s*([\w+./=]*)\s*<\/span>/)?.[1]; if (!match) return ""; const decoded = atob(match); if (/\u0000/.test(decoded)) return fromBinary(decoded); else return decoded; } async generateOutput(markdown) { const initialHTML = this.showdownConverter.makeHtml(markdown), outputHTML = await this.highlightCode(initialHTML); let encoded; try { encoded = btoa(markdown); } catch (e) { encoded = btoa(toBinary(markdown)); } return `${outputHTML} <span class="canvas-markdown-code" style="display: none;">${encoded}</span>`; } async highlightCode(html) { const template = document.createElement("template"); template.innerHTML = html; const codeBlocks = [ ...template.content.querySelectorAll("pre code"), ]; await this.extractLanguages(codeBlocks); for (const codeBlock of codeBlocks) { highlight.highlightElement(codeBlock); } // Extract styles from custom settings const settings = this.loadSettings(); for (const setting of settings.customStyles) { const { target, style } = setting; const targetElements = template.content.querySelectorAll(target); for (const targetElement of targetElements) { targetElement.style.cssText += style; } } return this.extractStyles(template); } extractStyles(template) { const tempDiv = document.createElement("pre"), tempCode = document.createElement("code"); tempCode.className = "hljs"; tempDiv.append(tempCode); tempDiv.style.display = "none"; document.body.append(tempDiv); const hljsElements = [ ...template.content.querySelectorAll("pre [class*=hljs]"), ]; for (const element of hljsElements) { let hasOnErrorAttribute = false, onErrorValue = null; if (element.hasAttribute("onerror")) { hasOnErrorAttribute = true; onErrorValue = element.getAttribute("onerror"); element.removeAttribute("onerror"); } const testElement = tempCode.appendChild(element.cloneNode(false)); if (hasOnErrorAttribute) { testElement.setAttribute("onerror", onErrorValue); } if (element.tagName === "CODE") { tempDiv.append(testElement); element.parentElement.style.backgroundColor = getComputedStyle(testElement).backgroundColor; element.style.textShadow = "none"; element.style.display = "block"; element.style.overflowX = "auto"; element.style.padding = "1em"; } const computedStyle = getComputedStyle(testElement), specialClasses = { "hljs-deletion": "background-color", "hljs-addition": "background-color", "hljs-emphasis": "font-style", "hljs-strong": "font-weight", "hljs-section": "font-weight", }; element.style.color = computedStyle.color; for (const [className, style] of Object.entries(specialClasses)) { if (testElement.classList.contains(className)) { element.style.setProperty(style, computedStyle.getPropertyValue(style)); } } testElement.remove(); } const output = template.innerHTML; tempDiv.remove(); return output; } async extractLanguages(codeBlocks) { for (const block of codeBlocks) { const language = block.className.match(/language-([^\s]*)/)?.[1]; if (language && !highlight.getLanguage(language) && languages[language]) { const languageData = (await import(`https://cdn.jsdelivr.net/gh/theusaf/canvas-markdown@5216c569489b9aa2caa6aee49ef8aadabb1f1794/lib/highlight/es/languages/${languages[language]}.min.js`).catch(() => ({}))).default; if (languageData) { highlight.registerLanguage(language, languageData); } } } } }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址