| 
1 |  | -const { Plugin } = require('obsidian');  | 
 | 1 | +const { Plugin } = require("obsidian");  | 
2 | 2 | 
 
  | 
3 | 3 | module.exports = class SafeLearnPlugin extends Plugin {  | 
4 | 4 |   async onload() {  | 
5 | 5 |     console.log("✅ SafeLearn Plugin loaded");  | 
6 | 6 | 
 
  | 
7 |  | -    // Wenn du styles.css verwendest, kannst du das hier weglassen  | 
8 |  | -    // this.addStyles();  | 
9 |  | - | 
10 |  | -    this.registerEvent(this.app.workspace.on("layout-change", () => this.processAll()));  | 
 | 7 | +    this.observeEditors();  | 
11 | 8 |   }  | 
12 | 9 | 
 
  | 
13 | 10 |   onunload() {  | 
14 | 11 |     console.log("❎ SafeLearn Plugin unloaded");  | 
15 |  | -    document.querySelectorAll(".fragment-highlight, .permission-block, .side-by-side").forEach(el => {  | 
16 |  | -      el.classList.remove("fragment-highlight", "permission-block", "side-by-side");  | 
17 |  | -    });  | 
 | 12 | +    // nichts weiter nötig – DOM wird neu aufgebaut bei Wechsel  | 
18 | 13 |   }  | 
19 | 14 | 
 
  | 
20 |  | -  processAll() {  | 
21 |  | -    const leaves = this.app.workspace.getLeavesOfType("markdown");  | 
22 |  | -    for (const leaf of leaves) {  | 
23 |  | -      const view = leaf.view;  | 
24 |  | -      if (view.previewMode?.renderer) {  | 
25 |  | -        const container = view.previewMode.containerEl;  | 
26 |  | -        this.highlightFragments(container);  | 
27 |  | -        this.markPermissionBlocks(container);  | 
28 |  | -        this.convertSideBySide(container);  | 
 | 15 | +  observeEditors() {  | 
 | 16 | +    const observer = new MutationObserver((mutations) => {  | 
 | 17 | +      for (const mutation of mutations) {  | 
 | 18 | +        if (mutation.type === "childList") {  | 
 | 19 | +          for (const node of mutation.addedNodes) {  | 
 | 20 | +            if (node.nodeType === 1) {  | 
 | 21 | +              this.processNode(node);  | 
 | 22 | +            }  | 
 | 23 | +          }  | 
 | 24 | +        }  | 
29 | 25 |       }  | 
30 |  | -    }  | 
 | 26 | +    });  | 
 | 27 | + | 
 | 28 | +    this.registerEvent(  | 
 | 29 | +      this.app.workspace.on("layout-change", () => {  | 
 | 30 | +        const editors = document.querySelectorAll(".cm-content");  | 
 | 31 | +        editors.forEach((el) => {  | 
 | 32 | +          this.processNode(el);  | 
 | 33 | +          observer.observe(el, { childList: true, subtree: true });  | 
 | 34 | +        });  | 
 | 35 | +      })  | 
 | 36 | +    );  | 
 | 37 | +  }  | 
 | 38 | + | 
 | 39 | +  processNode(container) {  | 
 | 40 | +    if (!(container instanceof HTMLElement)) return;  | 
 | 41 | + | 
 | 42 | +    this.highlightFragments(container);  | 
 | 43 | +    this.markPermissionBlocks(container);  | 
 | 44 | +    this.convertSideBySide(container);  | 
31 | 45 |   }  | 
32 | 46 | 
 
  | 
33 | 47 |   highlightFragments(container) {  | 
34 | 48 |     const walker = document.createTreeWalker(  | 
35 | 49 |       container,  | 
36 | 50 |       NodeFilter.SHOW_TEXT,  | 
37 | 51 |       {  | 
38 |  | -        acceptNode: (node) => {  | 
39 |  | -          return node.nodeValue?.match(/\s*##fragment\s*/)  | 
40 |  | -            ? NodeFilter.FILTER_ACCEPT  | 
41 |  | -            : NodeFilter.FILTER_SKIP;  | 
42 |  | -        },  | 
 | 52 | +        acceptNode: (node) => node.nodeValue?.includes("##fragment") ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP,  | 
43 | 53 |       },  | 
44 | 54 |       false  | 
45 | 55 |     );  | 
46 | 56 | 
 
  | 
47 |  | -    const toHighlight = [];  | 
48 |  | -    while (walker.nextNode()) {  | 
49 |  | -      toHighlight.push(walker.currentNode);  | 
50 |  | -    }  | 
 | 57 | +    const nodes = [];  | 
 | 58 | +    while (walker.nextNode()) nodes.push(walker.currentNode);  | 
 | 59 | + | 
 | 60 | +    for (const node of nodes) {  | 
 | 61 | +      const parent = node.parentNode;  | 
 | 62 | +      if (!parent) continue;  | 
51 | 63 | 
 
  | 
52 |  | -    for (const node of toHighlight) {  | 
53 |  | -      const span = document.createElement("span");  | 
54 |  | -      span.className = "fragment-highlight";  | 
55 |  | -      span.textContent = node.nodeValue;  | 
56 |  | -      node.parentNode?.replaceChild(span, node);  | 
 | 64 | +      const parts = node.nodeValue.split("##fragment");  | 
 | 65 | +      parent.removeChild(node);  | 
 | 66 | + | 
 | 67 | +      for (let i = 0; i < parts.length; i++) {  | 
 | 68 | +        parent.appendChild(document.createTextNode(parts[i]));  | 
 | 69 | +        if (i < parts.length - 1) {  | 
 | 70 | +          const span = document.createElement("span");  | 
 | 71 | +          span.className = "fragment-highlight";  | 
 | 72 | +          span.textContent = "##fragment";  | 
 | 73 | +          parent.appendChild(span);  | 
 | 74 | +        }  | 
 | 75 | +      }  | 
57 | 76 |     }  | 
58 | 77 |   }  | 
59 | 78 | 
 
  | 
60 | 79 |   markPermissionBlocks(container) {  | 
61 |  | -    const blocks = container.innerHTML.match(/@@@[^@\n]+[\s\S]*?@@@/g);  | 
62 |  | -    if (!blocks) return;  | 
63 |  | -    for (const block of blocks) {  | 
64 |  | -      const match = block.match(/^@@@([^\n]+)\n([\s\S]*?)\n@@@$/);  | 
65 |  | -      if (!match) continue;  | 
66 |  | -      const role = match[1].trim();  | 
67 |  | -      const content = match[2].trim();  | 
68 |  | -      const div = document.createElement("div");  | 
69 |  | -      div.className = "permission-block";  | 
70 |  | -      div.innerHTML = `<div class="perm-label">@@@ ${role}</div><div>${content}</div><div class="perm-end">@@@</div>`;  | 
71 |  | -      container.innerHTML = container.innerHTML.replace(block, div.outerHTML);  | 
72 |  | -    }  | 
 | 80 | +    const nodes = container.querySelectorAll("div");  | 
 | 81 | + | 
 | 82 | +    nodes.forEach((block) => {  | 
 | 83 | +      const text = block.innerText;  | 
 | 84 | + | 
 | 85 | +      const match = text.match(/^@@@([^\n]+)\n([\s\S]*?)\n@@@$/);  | 
 | 86 | +      if (match) {  | 
 | 87 | +        const role = match[1].trim();  | 
 | 88 | +        const content = match[2].trim();  | 
 | 89 | + | 
 | 90 | +        const permDiv = document.createElement("div");  | 
 | 91 | +        permDiv.className = "permission-block";  | 
 | 92 | +        permDiv.innerHTML = `<div class="perm-label">@@@ ${role}</div><div>${content}</div><div class="perm-end">@@@</div>`;  | 
 | 93 | +        block.replaceWith(permDiv);  | 
 | 94 | +      }  | 
 | 95 | +    });  | 
73 | 96 |   }  | 
74 | 97 | 
 
  | 
75 | 98 |   convertSideBySide(container) {  | 
76 |  | -    const pattern = /##side-by-side-start([\s\S]*?)##side-by-side-end/g;  | 
77 |  | -    container.innerHTML = container.innerHTML.replace(pattern, (match, content) => {  | 
78 |  | -      const parts = content.split(/##separator/g);  | 
79 |  | -      const columns = parts.map(col => `<div>${col.trim()}</div>`).join('');  | 
80 |  | -      return `<div class="side-by-side">${columns}</div>`;  | 
 | 99 | +    const blocks = container.querySelectorAll("div");  | 
 | 100 | + | 
 | 101 | +    blocks.forEach((block) => {  | 
 | 102 | +      const text = block.innerText;  | 
 | 103 | +      const match = text.match(/##side-by-side-start([\s\S]*?)##side-by-side-end/);  | 
 | 104 | +      if (match) {  | 
 | 105 | +        const content = match[1];  | 
 | 106 | +        const parts = content.split(/##separator/g);  | 
 | 107 | +        const wrapper = document.createElement("div");  | 
 | 108 | +        wrapper.className = "side-by-side";  | 
 | 109 | +        parts.forEach((part) => {  | 
 | 110 | +          const col = document.createElement("div");  | 
 | 111 | +          col.innerText = part.trim();  | 
 | 112 | +          wrapper.appendChild(col);  | 
 | 113 | +        });  | 
 | 114 | +        block.replaceWith(wrapper);  | 
 | 115 | +      }  | 
81 | 116 |     });  | 
82 | 117 |   }  | 
83 | 118 | };  | 
0 commit comments