/**
* This widget allows you to preview any markdown code note in real time.
*/
const TPL = `<div style="padding: 10px; border-left: 0px solid var(--main-border-color); height: 100%;">
<style></style>
<div id="markdown-preview"></div>
</div>`;
/* global highlightminjs:false markedminjs:false */
// A couple of temporary hoists
let noteId = "";
const imageCache = {};
/* api.showMessage('md pretest'); */
/** @type {import("highlight.js").default} */
let hljs;
try {
hljs = highlightminjs;
hljs.configure({cssSelector: "#markdown-preview pre code"});
}
catch {
// eslint-disable-next-line no-console
console.info("highlight.min.js not found, no syntax highlighting in markdown preview.");
}
/** @type {import("marked").Marked} */
const markedjs = markedminjs;
const slugify = text => text.toLowerCase().replace(/[^\w]/g, "-");
const renderer = {
heading(text, level) {
const escapedText = slugify(text);
return `<h${level}>
<a href="${escapedText}" id="${escapedText}" class="anchor">
<span class="header-link"></span>
</a>
${text}
</h${level}>`;
},
image(href, title, text) {
const found = imageCache[noteId] && imageCache[noteId][href];
if (found) href = `api/images/${found}/${href}`;
return `<img src="${href}" title="${title}" alt="${text}">`;
},
};
markedjs.use({renderer});
class MarkdownPreviewWidget extends api.RightPanelWidget {
get widgetTitle() {return "Markdown Preview";}
get parentWidget() {return "right-pane";}
isEnabled() {
return super.isEnabled()
&& this.note.type === "code"
&& this.note.mime
&& this.note.mime === "text/x-markdown";
/** && this.note.hasLabel("markdownPreview"); */
}
async doRenderBody() {
this.$body.html(TPL);
this.$preview = this.$body.find("#markdown-preview");
this.$body.on("click", "a", this.jumpToLink.bind(this));
await this.updateCss();
return this.$body;
}
async refreshWithNote(note) {
const {content} = await note.getNoteComplement();
this.$preview.html(markedjs.parse(content));
const highlightLabel = api.startNote.getLabel("syntaxHighlighting");
if (highlightLabel?.value !== "false") hljs?.highlightAll?.();
}
async entitiesReloadedEvent({loadResults}) {
if (loadResults.isNoteContentReloaded(this.noteId)) {
this.refresh();
}
}
async noteSwitched() {
if (!this.note) return;
noteId = this.note.noteId;
await this.getImages();
await this.updateCss();
await this.refresh();
if (!this.isEnabled()) return;
const scrollSyncStatus = this.note.getLabelValue("markdownScrollSync");
const disableBoth = scrollSyncStatus === "none";
const onlyLeft = scrollSyncStatus === "left";
const onlyRight = scrollSyncStatus === "right";
$("#center-pane .CodeMirror-scroll, #center-pane .component.scrolling-container").off(".mdpreview");
$("#right-pane").off(".mdpreview");
if (disableBoth) return;
const isFullHeight = document.querySelector(".note-detail.full-height");
const centerSelector = isFullHeight ? "#center-pane .CodeMirror-scroll" : "#center-pane .component.scrolling-container";
const center = document.querySelector(centerSelector);
const right = document.querySelector("#right-pane");
if (!onlyRight) this.addSyncListener(center, right, !onlyLeft);
if (!onlyLeft) this.addSyncListener(right, center, !onlyRight);
}
async getImages() {
const children = await this.note.getChildNotes();
const images = children.filter(n => n.type === "image");
imageCache[this.note.noteId] = {};
for (const image of images) imageCache[this.note.noteId][image.title] = image.noteId;
}
jumpToLink(ev) {
const link = ev.target;
const href = link.getAttribute("href");
const isAnchorLink = href.startsWith("#");
if (!isAnchorLink) return;
const heading = this.$body.find(href);
if (!heading.length) return;
heading[0].scrollIntoView({behavior: "instant", block: "start"});
}
addSyncListener(target, other, shouldRestart = true) {
const jTarget = $(target);
jTarget.on("scroll.mdpreview", (ev) => {
$(other).off("scroll.mdpreview");
this.syncedScroll(ev.target, other);
clearTimeout($.data(jTarget, "scrollTimer"));
$.data(jTarget, "scrollTimer", setTimeout(() => {
if (shouldRestart) this.addSyncListener(other, target, shouldRestart);
}, 250));
});
}
syncedScroll(target, other) {
const max = target.scrollHeight - target.clientHeight;
const current = target.scrollTop;
const percent = current / max;
const maxPane = other.scrollHeight - other.clientHeight;
const newScroll = (percent * maxPane);
other.scrollTop = newScroll;
}
async updateCss() {
let accumulator = "";
const globalChildren = await api.startNote.getChildNotes();
const globalStyles = globalChildren.filter(n => n.mime == "text/css");
for (const style of globalStyles) accumulator += await style.getContent();
if (this.note && this.isEnabled()) {
const localChildren = await this.note.getChildNotes();
const localStyles = localChildren.filter(n => n.mime == "text/css");
for (const style of localStyles) accumulator += await style.getContent();
}
this.$body.find("style").html(accumulator);
}
}
module.exports = new MarkdownPreviewWidget();