Files
zakirullin--files.md/web/lib/fold-code.js
T
2026-07-13 12:30:20 +08:00

294 lines
13 KiB
JavaScript
Vendored

// 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<string, boolean>`, 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 */);
});