// ==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);
}
}
}
}
}