mirror of
https://github.com/unterrainerinformatik/safeLearn-Obsidian-plugin.git
synced 2026-07-22 13:10:29 +00:00
170 lines
5.9 KiB
JavaScript
170 lines
5.9 KiB
JavaScript
'use strict';
|
|
|
|
var obsidian = require('obsidian');
|
|
var view = require('@codemirror/view');
|
|
var state = require('@codemirror/state');
|
|
|
|
class SafeLearnPlugin extends obsidian.Plugin {
|
|
async onload() {
|
|
console.log("✅ SafeLearn Plugin geladen");
|
|
this.registerEditorExtension(safelearnHighlighter);
|
|
this.registerMarkdownPostProcessor((el) => {
|
|
cleanPreview(el);
|
|
});
|
|
}
|
|
}
|
|
function cleanPreview(el) {
|
|
el.querySelectorAll("*").forEach((node) => {
|
|
if (!(node instanceof HTMLElement))
|
|
return;
|
|
const text = node.textContent?.trim();
|
|
if (!text)
|
|
return;
|
|
if (/^ *@{3} *$/.test(text) ||
|
|
/^ *@{3} +([^\n\s,]+([ ,]+[^\n\s,]+)*)$/.test(text) ||
|
|
/^##fragment$/.test(text) ||
|
|
/^##(side-by-side-(start|end)|separator)$/.test(text)) {
|
|
node.addClass("safelearn-hidden");
|
|
return;
|
|
}
|
|
node.childNodes.forEach(child => {
|
|
if (child.nodeType === Node.TEXT_NODE) {
|
|
let changed = false;
|
|
let newText = child.textContent ?? "";
|
|
const replacePatterns = [
|
|
/@@@(?: +[^\s,]+([ ,]+[^\s,]+)*)?/g,
|
|
/##fragment(?![\w-])/gi,
|
|
/##(side-by-side-(start|end)|separator)/gi
|
|
];
|
|
for (const pattern of replacePatterns) {
|
|
if (pattern.test(newText)) {
|
|
newText = newText.replace(pattern, "");
|
|
changed = true;
|
|
}
|
|
}
|
|
if (changed) {
|
|
const span = document.createElement("span");
|
|
span.textContent = newText;
|
|
child.replaceWith(span);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
const safelearnHighlighter = view.ViewPlugin.define((view) => {
|
|
let decorations = buildDecorations(view);
|
|
const observer = new MutationObserver(() => {
|
|
decorations = buildDecorations(view);
|
|
view.dispatch({ effects: [] });
|
|
});
|
|
observer.observe(view.dom, { attributes: true, attributeFilter: ["class"] });
|
|
return {
|
|
update(update) {
|
|
if (update.docChanged ||
|
|
update.viewportChanged ||
|
|
update.startState !== update.state) {
|
|
decorations = buildDecorations(update.view);
|
|
}
|
|
},
|
|
destroy() {
|
|
observer.disconnect();
|
|
},
|
|
decorations
|
|
};
|
|
}, {
|
|
decorations: v => v.decorations
|
|
});
|
|
function buildDecorations(view$1) {
|
|
if (view$1.dom.classList.contains("cm-preview")) {
|
|
return view.Decoration.none;
|
|
}
|
|
const doc = view$1.state.doc;
|
|
const lineCount = doc.lines;
|
|
const decorations = [];
|
|
let permissionBlockStart = -1;
|
|
let sideBlockStart = -1;
|
|
for (let i = 1; i <= lineCount; i++) {
|
|
const line = doc.line(i);
|
|
const text = line.text.trim();
|
|
// === Global @@@ Directive in first line ===
|
|
if (i === 1 && /^ *@{3} +[^\s,]+([ ,]+[^\s,]+)* *$/.test(text)) {
|
|
const matchIndex = line.text.indexOf("@@@");
|
|
const from = line.from + matchIndex;
|
|
const to = line.to;
|
|
decorations.push({
|
|
from,
|
|
to,
|
|
deco: view.Decoration.mark({ class: "permission-global-directive" }),
|
|
});
|
|
continue;
|
|
}
|
|
// === ##fragment ===
|
|
let fragIndex = line.text.indexOf("##fragment ");
|
|
if (fragIndex === -1) {
|
|
fragIndex = line.text.indexOf("##fragment");
|
|
// only if the line ends with this "##fragment" without any trailing text
|
|
if (fragIndex !== -1 && line.text.slice(fragIndex + "##fragment".length).trim() !== "") {
|
|
fragIndex = -1;
|
|
}
|
|
}
|
|
if (fragIndex !== -1) {
|
|
const from = line.from + fragIndex;
|
|
const to = from + "##fragment ".length;
|
|
decorations.push({
|
|
from,
|
|
to,
|
|
deco: view.Decoration.mark({ class: "fragment-highlight" }),
|
|
});
|
|
}
|
|
// === @@@ Block ===
|
|
if (permissionBlockStart === -1 && text.startsWith("@@@")) {
|
|
permissionBlockStart = i;
|
|
continue;
|
|
}
|
|
if (permissionBlockStart !== -1 && text === "@@@") {
|
|
for (let j = permissionBlockStart; j <= i; j++) {
|
|
const l = doc.line(j);
|
|
decorations.push({
|
|
from: l.from,
|
|
to: l.from,
|
|
deco: view.Decoration.mark({ class: "permission-block" }),
|
|
});
|
|
}
|
|
permissionBlockStart = -1;
|
|
}
|
|
// === ##side-by-side blocks ===
|
|
if (sideBlockStart === -1 && text === "##side-by-side-start") {
|
|
sideBlockStart = i;
|
|
continue;
|
|
}
|
|
if (sideBlockStart !== -1 && text === "##side-by-side-end") {
|
|
for (let j = sideBlockStart; j <= i; j++) {
|
|
const l = doc.line(j);
|
|
const cl = j === sideBlockStart ? "side-by-side-start" : j == i ? "side-by-side-end" : "side-by-side-block";
|
|
decorations.push({
|
|
from: l.from,
|
|
to: l.from,
|
|
deco: view.Decoration.mark({ class: cl }),
|
|
});
|
|
}
|
|
sideBlockStart = -1;
|
|
continue;
|
|
}
|
|
// === ##separator within Side-by-Side ===
|
|
if (sideBlockStart !== -1 && text === "##separator") {
|
|
decorations.push({
|
|
from: line.from,
|
|
to: line.from,
|
|
deco: view.Decoration.mark({ class: "side-by-side-separator" }),
|
|
});
|
|
}
|
|
}
|
|
decorations.sort((a, b) => a.from - b.from);
|
|
const builder = new state.RangeSetBuilder();
|
|
for (const { from, to, deco } of decorations) {
|
|
builder.add(from, to, deco);
|
|
}
|
|
return builder.finish();
|
|
}
|
|
|
|
module.exports = SafeLearnPlugin;
|