unterrainerinformatik_safeL.../main.js
2025-06-01 23:19:27 +02:00

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;