diff --git a/web/script_text_input.js b/web/script_text_input.js
index b48a366..cc312a7 100644
--- a/web/script_text_input.js
+++ b/web/script_text_input.js
@@ -1,425 +1,125 @@
import { app } from "/scripts/app.js";
-const STYLE_ID = "mrmth-script-line-numbers-unified";
+const STYLE_ID = "mrmth-script-line-numbers-v2";
+const BRACKET_PAIRS = { "(": ")", "[": "]", "{": "}" };
+const OPENING_BRACKETS = new Set(["(", "[", "{"]);
-if (!window.__mrmthScriptInputUnifiedInit) {
- window.__mrmthScriptInputUnifiedInit = true;
+function ensureStyles() {
+ if (document.getElementById(STYLE_ID)) return;
+ const style = document.createElement("style");
+ style.id = STYLE_ID;
+ style.textContent = `
+ .mrmth-v2-editor { display:flex; width:100%; min-height:160px; border:1px solid rgba(255,255,255,.12); border-radius:8px; overflow:hidden; }
+ .mrmth-v2-gutter { padding:8px 10px; min-width:40px; text-align:right; user-select:none; color:#9aa0a6; background:rgba(0,0,0,.35); border-right:1px solid rgba(255,255,255,.12); font-family:monospace; font-size:12px; line-height:1.45; white-space:pre; box-sizing:border-box; }
+ .mrmth-v2-textarea { flex:1; min-height:160px !important; border:0 !important; outline:none !important; resize:vertical !important; font-family:monospace !important; font-size:12px !important; line-height:1.45 !important; padding:8px 10px !important; box-sizing:border-box !important; }
+ `;
+ document.head.appendChild(style);
+}
- function ensureStyles() {
- if (document.getElementById(STYLE_ID)) return;
+function hasNearbyScriptLabel(el) {
+ const container = el.closest("div, section, article");
+ if (!container) return false;
- const style = document.createElement("style");
- style.id = STYLE_ID;
- style.textContent = `
- .mrmth-editor {
- display: flex;
- align-items: stretch;
- width: 100%;
- min-height: 170px;
- border: 1px solid rgba(255,255,255,0.14);
- border-radius: 10px;
- overflow: hidden;
- background: rgba(0,0,0,0.18);
- }
-
- .mrmth-gutter {
- padding: 8px 10px;
- min-width: 44px;
- text-align: right;
- user-select: none;
- color: #9aa0a6;
- background: rgba(0,0,0,0.35);
- border-right: 1px solid rgba(255,255,255,0.12);
- font-family: monospace;
- font-size: 12px;
- line-height: 1.45;
- white-space: pre;
- box-sizing: border-box;
- overflow: hidden;
- }
-
- .mrmth-gutter-content {
- white-space: pre;
- will-change: transform;
- }
-
- .mrmth-editor-container {
- position: relative;
- flex: 1;
- min-width: 0;
- min-height: 170px;
- overflow: hidden;
- }
-
- .mrmth-script-textarea {
- display: block;
- width: 100% !important;
- height: 100% !important;
- min-height: 170px !important;
- margin: 0 !important;
- border: 0 !important;
- outline: none !important;
- resize: vertical !important;
- box-sizing: border-box !important;
-
- font-family: monospace !important;
- font-size: 12px !important;
- line-height: 1.45 !important;
- padding: 8px 10px !important;
-
- white-space: pre-wrap !important;
- overflow-wrap: anywhere !important;
- word-break: break-word !important;
-
- z-index: 2;
- color: transparent !important;
- -webkit-text-fill-color: transparent;
- caret-color: #ffffff !important;
- background: transparent !important;
- mix-blend-mode: lighten;
- }
-
- .mrmth-measure {
- position: absolute;
- visibility: hidden;
- pointer-events: none;
- inset: 0 auto auto 0;
- white-space: pre-wrap;
- overflow-wrap: anywhere;
- word-break: break-word;
- box-sizing: border-box;
- height: auto;
- }
-
- .mrmth-syntax-layer {
- position: absolute;
- inset: 0;
- margin: 0;
- pointer-events: none;
- white-space: pre-wrap;
- overflow-wrap: anywhere;
- word-break: break-word;
- box-sizing: border-box;
- z-index: 1;
- mix-blend-mode: normal;
- opacity: 1;
- background: transparent;
- overflow: hidden;
- }
-
- .mrmth-token-comment { color: #7f8c8d; }
- .mrmth-token-number { color: #f39c12; }
- .mrmth-token-constant { color: #f1c40f; }
- .mrmth-token-keyword { color: #e74c3c; }
- .mrmth-token-function { color: #8e44ad; }
- .mrmth-token-variable { color: #ecf0f1; }
- .mrmth-token-operator { color: #95a5a6; }
- .mrmth-token-string { color: #4ea658; }
- .mrmth-token-bracket { font-weight: 700; }
- .mrmth-bracket-0 { color: #ffd700; }
- .mrmth-bracket-1 { color: #da70d6; }
- .mrmth-bracket-2 { color: #87cefa; }
- .mrmth-bracket-3 { color: #98fb98; }
- .mrmth-bracket-4 { color: #ff6347; }
- .mrmth-bracket-5 { color: #ffa500; }
- `;
- document.head.appendChild(style);
+ // text labels around the textarea
+ const candidates = container.querySelectorAll("label, .label, .input-label, .property_name, span, div");
+ for (const n of candidates) {
+ const t = (n.textContent || "").trim().toLowerCase();
+ if (t === "script") return true;
}
+ return false;
+}
- function escapeHtml(value) {
- return value
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
- }
+function isLikelyScriptTextarea(el) {
+ if (!(el instanceof HTMLTextAreaElement)) return false;
+ if (el.dataset.mrmthV2Script === "1") return false;
- function hasNearbyScriptLabel(el) {
- const root = el.closest("div, section, article") ?? el.parentElement;
- if (!root) return false;
- const candidates = root.querySelectorAll("label, .label, .input-label, .property_name, .v-label, span, div");
- for (const n of candidates) {
- const t = (n.textContent || "").trim().toLowerCase();
- if (t === "script") return true;
- }
- return false;
- }
+ const attrs = [
+ el.name,
+ el.id,
+ el.getAttribute("data-name"),
+ el.getAttribute("aria-label"),
+ el.getAttribute("placeholder"),
+ ]
+ .filter(Boolean)
+ .join(" ")
+ .toLowerCase();
- function isLikelyScriptTextarea(el) {
- if (!(el instanceof HTMLTextAreaElement)) return false;
- if (el.dataset.mrmthEnhanced === "1") return false;
+ // accept either direct attribute match OR nearby label
+ return attrs.includes("script") || hasNearbyScriptLabel(el);
+}
- const attrs = [
- el.name,
- el.id,
- el.getAttribute("data-name"),
- el.getAttribute("aria-label"),
- el.getAttribute("placeholder"),
- ]
- .filter(Boolean)
- .join(" ")
- .toLowerCase();
+function updateGutter(textarea, gutter) {
+ const lineCount = Math.max(1, textarea.value.split("\n").length);
+ gutter.textContent = Array.from({ length: lineCount }, (_, i) => String(i + 1)).join("\n");
+ gutter.style.transform = `translateY(${-textarea.scrollTop}px)`;
+}
- return attrs.includes("script") || hasNearbyScriptLabel(el);
- }
+function addBracketAutoClose(textarea) {
+ textarea.addEventListener("keydown", (e) => {
+ const key = e.key;
+ if (!OPENING_BRACKETS.has(key)) return;
+ const start = textarea.selectionStart;
+ const end = textarea.selectionEnd;
+ if (start !== end) return;
- function hideNearbyScriptLabels(textarea) {
- const root = textarea.closest("div, section, article") ?? textarea.parentElement;
- if (!root) return;
- const labels = root.querySelectorAll("label, .label, .input-label, .property_name, .v-label, span, div");
- for (const el of labels) {
- if (el.dataset.mrmthHiddenLabel === "1") continue;
- const t = (el.textContent || "").trim().toLowerCase();
- if (t === "script") {
- el.style.display = "none";
- el.dataset.mrmthHiddenLabel = "1";
- }
- }
- }
+ const after = textarea.value[start] ?? "";
+ const closing = BRACKET_PAIRS[key];
+ if (/[a-zA-Z0-9_]/.test(after) || after === closing) return;
- function addBracketAutoClose(textarea) {
- const pairs = { "(": ")", "[": "]", "{": "}" };
- const opening = new Set(Object.keys(pairs));
-
- textarea.addEventListener("keydown", (e) => {
- if (!opening.has(e.key)) return;
-
- const start = textarea.selectionStart;
- const end = textarea.selectionEnd;
- if (start !== end) return;
-
- const nextChar = textarea.value[start] ?? "";
- const closing = pairs[e.key];
- if (/[a-zA-Z0-9_]/.test(nextChar) || nextChar === closing) return;
-
- e.preventDefault();
- textarea.value = `${textarea.value.slice(0, start)}${e.key}${closing}${textarea.value.slice(start)}`;
- textarea.selectionStart = textarea.selectionEnd = start + 1;
- textarea.dispatchEvent(new Event("input", { bubbles: true }));
- });
- }
-
- function getLineHeightPx(textarea) {
- const style = getComputedStyle(textarea);
- const lh = parseFloat(style.lineHeight);
- if (Number.isFinite(lh) && lh > 0) return lh;
-
- const probe = document.createElement("span");
- probe.textContent = "A";
- probe.style.position = "absolute";
- probe.style.visibility = "hidden";
- probe.style.fontFamily = style.fontFamily;
- probe.style.fontSize = style.fontSize;
- probe.style.fontWeight = style.fontWeight;
- probe.style.fontStyle = style.fontStyle;
- document.body.appendChild(probe);
- const h = probe.getBoundingClientRect().height || 16;
- probe.remove();
- return h;
- }
-
- function buildMeasureHtml(lines) {
- let html = "";
- for (let i = 0; i < lines.length; i++) {
- const line = lines[i] === "" ? " " : lines[i];
- html += `${escapeHtml(line)}`;
- if (i < lines.length - 1) html += "\n";
- }
- return html;
- }
-
- function updateNumbersWrapped(textarea, gutterContent, measureEl, lineHeightPx) {
- const rawLines = textarea.value.split("\n");
- const lineCount = Math.max(1, rawLines.length);
-
- const scrollbarWidth = textarea.offsetWidth - textarea.clientWidth;
- const widthPx = Math.max(0, textarea.clientWidth - scrollbarWidth);
-
- const cs = getComputedStyle(textarea);
- measureEl.style.fontFamily = cs.fontFamily;
- measureEl.style.fontSize = cs.fontSize;
- measureEl.style.fontWeight = cs.fontWeight;
- measureEl.style.fontStyle = cs.fontStyle;
- measureEl.style.lineHeight = cs.lineHeight;
- measureEl.style.padding = cs.padding;
- measureEl.style.width = `${widthPx}px`;
-
- measureEl.innerHTML = buildMeasureHtml(rawLines);
- const totalHeight = measureEl.offsetHeight;
-
- const gutterLines = [];
- for (let i = 0; i < lineCount; i++) {
- const marker = measureEl.querySelector(`[data-ln="${i}"]`);
- const nextMarker = i < lineCount - 1 ? measureEl.querySelector(`[data-ln="${i + 1}"]`) : null;
-
- const y0 = marker ? marker.offsetTop : i * lineHeightPx;
- const y1 = nextMarker ? nextMarker.offsetTop : totalHeight;
-
- const visualLines = Math.max(1, Math.round((y1 - y0) / lineHeightPx));
- gutterLines.push(String(i + 1));
- for (let j = 1; j < visualLines; j++) gutterLines.push(" ");
- }
-
- gutterContent.textContent = gutterLines.join("\n");
- gutterContent.style.transform = `translateY(${-textarea.scrollTop}px)`;
- }
-
- function attachToTextarea(textarea) {
- if (!isLikelyScriptTextarea(textarea)) return;
- if (textarea.closest(".mrmth-editor")) {
- textarea.dataset.mrmthEnhanced = "1";
- return;
- }
-
- const parent = textarea.parentElement;
- if (!parent) return;
-
- ensureStyles();
- hideNearbyScriptLabels(textarea);
-
- const wrapper = document.createElement("div");
- wrapper.className = "mrmth-editor";
-
- const gutter = document.createElement("div");
- gutter.className = "mrmth-gutter";
-
- const gutterContent = document.createElement("div");
- gutterContent.className = "mrmth-gutter-content";
- gutter.appendChild(gutterContent);
-
- const editorContainer = document.createElement("div");
- editorContainer.className = "mrmth-editor-container";
-
- const measureEl = document.createElement("pre");
- measureEl.className = "mrmth-measure";
-
- parent.replaceChild(wrapper, textarea);
- wrapper.appendChild(gutter);
- wrapper.appendChild(editorContainer);
- editorContainer.appendChild(measureEl);
- editorContainer.appendChild(textarea);
-
- textarea.classList.add("mrmth-script-textarea");
- textarea.dataset.mrmthEnhanced = "1";
- textarea.spellcheck = false;
- textarea.autocapitalize = "off";
- textarea.autocomplete = "off";
- textarea.autocorrect = "off";
-
- textarea.style.color = "transparent";
- textarea.style.caretColor = "#ffffff";
- textarea.style.mixBlendMode = "lighten";
- textarea.style.background = "transparent";
-
- addBracketAutoClose(textarea);
- const lineHeightPx = getLineHeightPx(textarea);
-
- const refresh = () => updateNumbersWrapped(textarea, gutterContent, measureEl, lineHeightPx);
-
- textarea.addEventListener("input", refresh);
- textarea.addEventListener("scroll", refresh);
-
- if (window.ResizeObserver) {
- const ro = new ResizeObserver(refresh);
- ro.observe(textarea);
- }
-
- requestAnimationFrame(refresh);
- }
-
- function scan(root = document) {
- root.querySelectorAll("textarea").forEach(attachToTextarea);
- }
-
- app.registerExtension({
- name: "mrmth.ScriptTextInput.Unified",
- async setup() {
- scan(document);
-
- const observer = new MutationObserver((mutations) => {
- for (const m of mutations) {
- for (const node of m.addedNodes) {
- if (!(node instanceof Element)) continue;
- if (node.matches?.("textarea")) attachToTextarea(node);
- scan(node);
- }
- }
- });
-
- observer.observe(document.body, { childList: true, subtree: true });
- },
+ e.preventDefault();
+ textarea.value = `${textarea.value.slice(0, start)}${key}${closing}${textarea.value.slice(start)}`;
+ textarea.selectionStart = textarea.selectionEnd = start + 1;
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
});
}
-const KEYWORDS = new Set(["if", "else", "while", "for", "in", "break", "continue", "return", "none", "None", "null", "NULL"]);
-const CONSTANTS = new Set(["pi", "PI", "e", "E"]);
-const FUNCTIONS = new Set([
- "sin", "cos", "tan", "asin", "acos", "atan", "atan2", "sinh", "cosh", "tanh", "asinh", "acosh", "atanh",
- "abs", "sqrt", "ln", "log", "exp", "floor", "ceil", "round", "pow", "clamp",
- "fft", "ifft", "print", "where", "rgb_to_int", "int_to_rgb", "hsv_to_rgb", "rgb_to_hsv",
- "sum", "mean", "std", "var", "min", "max", "sort", "argsort", "argmin", "argmax"
-]);
+function attachToTextarea(textarea) {
+ if (!isLikelyScriptTextarea(textarea)) return;
+ const parent = textarea.parentElement;
+ if (!parent) return;
-const BRACKET_PAIRS = { "(": ")", "[": "]", "{": "}" };
-const OPENING_BRACKETS = new Set(["(", "[", "{"]);
-const CLOSING_BRACKETS = new Set([")", "]", "}"]);
+ ensureStyles();
-function tokenize(text) {
- const tokens = [];
- const pattern = /#.*|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\\r\n])*"|'(?:\\.|[^'\\\r\n])*'|\b\d+(?:\.\d*)?(?:[eE][+-]?\d+)?\b|\B\.\d+(?:[eE][+-]?\d+)?\b|==|!=|>=|<=|<<|>>|->|\+=|-=|\*=|\/=|%=|[+\-*/%^=<>|?:,;()\[\]{}]|\b[a-zA-Z_][a-zA-Z_0-9]*\b|\s+|./g;
+ const wrapper = document.createElement("div");
+ wrapper.className = "mrmth-v2-editor";
- const bracketStack = [];
- const depthByType = { "(": 0, "[": 0, "{": 0 };
- let m;
+ const gutter = document.createElement("div");
+ gutter.className = "mrmth-v2-gutter";
- while ((m = pattern.exec(text)) !== null) {
- const value = m[0];
- let type = "text";
- let depth = 0;
+ parent.replaceChild(wrapper, textarea);
+ wrapper.appendChild(gutter);
+ wrapper.appendChild(textarea);
- if (value.startsWith("#") || value.startsWith("/*")) {
- type = "comment";
- } else if (value.startsWith('"') || value.startsWith("'")) {
- type = "string";
- } else if (OPENING_BRACKETS.has(value)) {
- depth = depthByType[value];
- depthByType[value]++;
- bracketStack.push(value);
- type = "bracket";
- } else if (CLOSING_BRACKETS.has(value)) {
- if (bracketStack.length) {
- const lastOpen = bracketStack[bracketStack.length - 1];
- if (BRACKET_PAIRS[lastOpen] === value) {
- depth = depthByType[lastOpen] - 1;
- depthByType[lastOpen]--;
- bracketStack.pop();
+ textarea.classList.add("mrmth-v2-textarea");
+ textarea.dataset.mrmthV2Script = "1";
+
+ const refresh = () => updateGutter(textarea, gutter);
+ textarea.addEventListener("input", refresh);
+ textarea.addEventListener("scroll", refresh);
+
+ addBracketAutoClose(textarea);
+ refresh();
+}
+
+function scan(root = document) {
+ root.querySelectorAll("textarea").forEach(attachToTextarea);
+}
+
+app.registerExtension({
+ name: "mrmth.ScriptTextInput.V2",
+ async setup() {
+ scan(document);
+
+ const observer = new MutationObserver((mutations) => {
+ for (const m of mutations) {
+ for (const node of m.addedNodes) {
+ if (!(node instanceof Element)) continue;
+ if (node.matches?.("textarea")) attachToTextarea(node);
+ scan(node);
}
}
- type = "bracket";
- } else if (/^[+\-*/%^=<>|?:,;]|==|!=|>=|<=|<<|>>|->$/.test(value)) {
- type = "operator";
- } else if (/^(?:\d+\.\d*|\d+|\.\d+)(?:[eE][+-]?\d+)?$/.test(value)) {
- type = "number";
- } else if (/^[a-zA-Z_]/.test(value)) {
- if (KEYWORDS.has(value)) type = "keyword";
- else if (CONSTANTS.has(value)) type = "constant";
- else if (FUNCTIONS.has(value)) type = "function";
- else type = "variable";
- }
+ });
- tokens.push({ type, value, depth });
- }
- return tokens;
-}
-
-function renderHighlight(text) {
- return tokenize(text).map((t) => {
- if (t.type === "text") return escapeHtml(t.value);
- if (t.type === "bracket") {
- const d = t.depth % 6;
- return `${escapeHtml(t.value)}`;
- }
- return `${escapeHtml(t.value)}`;
- }).join("");
-}
+ observer.observe(document.body, { childList: true, subtree: true });
+ },
+});
\ No newline at end of file