// HyperMD, copyright (c) by laobubu // Distributed under an MIT license: http://laobubu.net/HyperMD/LICENSE // // DESCRIPTION: Turn code blocks into flow charts / playground sandboxes etc. // // ============================================= // **START AN ADDON** Check List // ============================================= // 1. Replace "FoldCode" to your addon's name (note the first letter is upper-case) // 2. Edit #region CodeMirror Extension // - If don't need this, delete the whole region // - Otherwise, delete hmdRollAndDice and add your own functions // 3. Edit #region Addon Class // - You might want to reading CONTRIBUTING.md // 4. Edit #region Addon Options // - It's highly suggested to finish the docs, see //TODO: write doc // - Note the defaultOption shall be the status when this addon is disabled! // - As for `FlipFlop` and `ff_*`, you might want to reading CONTRIBUTING.md // 5. Remove this check-list // 6. Modify `DESCRIPTION: ` above // 6. Build, Publish, Pull Request etc. // (function (mod){ //[HyperMD] UMD patched! /*commonjs*/ ("object"==typeof exports&&"undefined"!=typeof module) ? mod(null, exports, require("codemirror"), require("../core"), require("./fold")) : /*amd*/ ("function"==typeof define&&define.amd) ? define(["require","exports","codemirror","../core","./fold"], mod) : /*plain env*/ mod(null, (this.HyperMD.FoldCode = this.HyperMD.FoldCode || {}), CodeMirror, HyperMD, HyperMD.Fold); })(function (require, exports, CodeMirror, core_1, fold_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.rendererRegistry = {}; /** * Add a CodeRenderer to the System CodeRenderer Registry * * @param lang * @param folder * @param suggested enable this folder in suggestedEditorConfig * @param force if a folder with same name is already exists, overwrite it. (dangerous) */ function registerRenderer(info, force) { if (!info || !info.name || !info.renderer) return; var name = info.name; var pattern = info.pattern; var registry = exports.rendererRegistry; if (name in registry) { if (!force) { throw new Error("CodeRenderer " + name + " already exists"); } } if (typeof pattern === 'string') { var t_1 = pattern.toLowerCase(); pattern = function (lang) { return (lang.toLowerCase() === t_1); }; } else if (pattern instanceof RegExp) { pattern = function (lang) { return info.pattern.test(lang); }; } var newInfo = { name: name, suggested: !!info.suggested, pattern: pattern, renderer: info.renderer, }; registry[name] = newInfo; exports.defaultOption[name] = false; exports.suggestedOption[name] = newInfo.suggested; } exports.registerRenderer = registerRenderer; //#endregion //#region FolderFunc for Addon/Fold ----------------------------------------------------- /** the FolderFunc for Addon/Fold */ exports.CodeFolder = function (stream, token) { if (token.start !== 0 || !token.type || token.type.indexOf('HyperMD-codeblock-begin') === -1 || !/[-\w]+\s*$/.test(token.string)) { return null; } return exports.getAddon(stream.cm).fold(stream, token); }; fold_1.registerFolder("code", exports.CodeFolder, true); exports.defaultOption = { /* will be populated by registerRenderer() */ }; exports.suggestedOption = { /* will be populated by registerRenderer() */ }; core_1.suggestedEditorConfig.hmdFoldCode = exports.suggestedOption; CodeMirror.defineOption("hmdFoldCode", exports.defaultOption, function (cm, newVal) { ///// convert newVal's type to `Record`, if it is not. if (!newVal || typeof newVal === "boolean") { newVal = newVal ? exports.suggestedOption : exports.defaultOption; } ///// apply config var inst = exports.getAddon(cm); for (var type in exports.rendererRegistry) { inst.setStatus(type, newVal[type]); } // then, folding task will be queued by setStatus() }); var FoldCode = /** @class */ (function () { function FoldCode(cm) { this.cm = cm; /** * stores renderer status for current editor * @private To enable/disable renderer, use `setStatus()` */ this._enabled = {}; /** renderers' output goes here */ this.folded = {}; } /** enable/disable one kind of renderer, in current editor */ FoldCode.prototype.setStatus = function (type, enabled) { if (!(type in exports.rendererRegistry)) return; if (!this._enabled[type] !== !enabled) { this._enabled[type] = !!enabled; if (enabled) fold_1.getAddon(this.cm).startFold(); else this.clear(type); } }; /** Clear one type of rendered TextMarkers */ FoldCode.prototype.clear = function (type) { var folded = this.folded[type]; if (!folded || !folded.length) return; var info; while (info = folded.pop()) info.marker.clear(); }; FoldCode.prototype.fold = function (stream, token) { var _this = this; if (token.start !== 0 || !token.type || token.type.indexOf('HyperMD-codeblock-begin') === -1) return null; var tmp = /([-\w]+)\s*$/.exec(token.string); var lang = tmp && tmp[1].toLowerCase(); if (!lang) return null; var renderer; var type; var cm = this.cm, registry = exports.rendererRegistry, _enabled = this._enabled; for (var type_i in registry) { var r = registry[type_i]; if (!_enabled[type_i]) continue; if (!r.pattern(lang)) continue; type = type_i; renderer = r.renderer; break; } if (!type) return null; var from = { line: stream.lineNo, ch: 0 }; var to = null; // find the end of code block var lastLineCM = cm.lastLine(); var endLine = stream.lineNo + 1; do { var s = cm.getTokenAt({ line: endLine, ch: 1 }); if (s && s.type && s.type.indexOf('HyperMD-codeblock-end') !== -1) { //FOUND END! to = { line: endLine, ch: s.end }; break; } } while (++endLine < lastLineCM); if (!to) return null; // request the range var rngReq = stream.requestRange(from, to); if (rngReq !== fold_1.RequestRangeResult.OK) return null; // now we can call renderer var code = cm.getRange({ line: from.line + 1, ch: 0 }, { line: to.line, ch: 0 }); var info = { editor: cm, lang: lang, marker: null, lineWidget: null, el: null, break: undefined_function, changed: undefined_function, }; var el = info.el = renderer(code, info); if (!el) { info.marker.clear(); return null; } //----------------------------- var $wrapper = document.createElement('div'); $wrapper.className = contentClass + type; $wrapper.style.minHeight = '1em'; $wrapper.appendChild(el); var lineWidget = info.lineWidget = cm.addLineWidget(to.line, $wrapper, { above: false, coverGutter: false, noHScroll: false, showIfHidden: false, }); //----------------------------- var $stub = document.createElement('span'); $stub.className = stubClass + type; // PATCHED: empty span - icon is rendered via CSS mask on // .hmd-fold-code-mermaid so it shares the copy button's color // pipeline (same background-color + opacity yields same shade). var marker = info.marker = cm.markText(from, to, { replacedWith: $stub, }); //----------------------------- var highlightON = function () { return $stub.className = stubClassHighlight + type; }; var highlightOFF = function () { return $stub.className = stubClass + type; }; $wrapper.addEventListener("mouseenter", highlightON, false); $wrapper.addEventListener("mouseleave", highlightOFF, false); info.changed = function () { lineWidget.changed(); }; info.break = function () { fold_1.breakMark(cm, marker); }; // PATCHED: clicking the fold-code stub used to scroll the viewport // up to the ```mermaid source line, which is disorienting because // the user was looking at the diagram. Snapshot the scroll position // before the break and restore it after — setCursor's {scroll:false} // alone isn't enough (clearing the marker, refocusing, and line- // widget removal each can trigger their own scroll adjustments). $stub.addEventListener('click', function () { var scrollInfo = cm.getScrollInfo(); var savedTop = scrollInfo.top; var savedLeft = scrollInfo.left; // Tell Fold's cursorActivity handler to skip its immediate // viewport refold - it would otherwise create markers during // this flow and cause scroll jumps that defeat our restore. cm._hmdSkipViewportFold = true; cm.operation(function () { var pos = marker.find(); if (pos) cm.setCursor(pos.from, null, { scroll: false }); cm.focus(); marker.clear(); }); // Run AFTER the operation finishes and any line-widget removal // settles. requestAnimationFrame fires before the next paint. requestAnimationFrame(function () { cm.scrollTo(savedLeft, savedTop); cm._hmdSkipViewportFold = false; }); }, false); // PATCHED: helps with instant mermaid rendering var updateSelected = function () { var range = marker.find(); if (!range) return; var selected = cm.listSelections().some(function (s) { var a = s.anchor, h = s.head; if (a.line === h.line && a.ch === h.ch) return false; var sFrom = (a.line < h.line || (a.line === h.line && a.ch < h.ch)) ? a : h; var sTo = sFrom === a ? h : a; if (sTo.line < range.from.line) return false; if (sFrom.line > range.to.line) return false; if (sTo.line === range.from.line && sTo.ch <= range.from.ch) return false; if (sFrom.line === range.to.line && sFrom.ch >= range.to.ch) return false; return true; }); $wrapper.classList.toggle('hmd-fold-code-selected', selected); }; cm.on('cursorActivity', updateSelected); marker.on("clear", function () { cm.off('cursorActivity', updateSelected); var markers = _this.folded[type]; var idx; if (markers && (idx = markers.indexOf(info)) !== -1) markers.splice(idx, 1); if (typeof info.onRemove === 'function') info.onRemove(info); lineWidget.clear(); }); if (!(type in this.folded)) this.folded[type] = [info]; else this.folded[type].push(info); return marker; }; return FoldCode; }()); exports.FoldCode = FoldCode; //#endregion var contentClass = "hmd-fold-code-content hmd-fold-code-"; // + renderer_type var stubClass = "hmd-fold-code-stub hmd-fold-code-"; // + renderer_type var stubClassHighlight = "hmd-fold-code-stub highlight hmd-fold-code-"; // + renderer_type var undefined_function = function () { }; /** ADDON GETTER (Singleton Pattern): a editor can have only one FoldCode instance */ exports.getAddon = core_1.Addon.Getter("FoldCode", FoldCode, exports.defaultOption /** if has options */); });