/** * 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();