diff --git a/web/script_text_input.js b/web/script_text_input.js
index f4b35a7..b90c95a 100644
--- a/web/script_text_input.js
+++ b/web/script_text_input.js
@@ -2,6 +2,15 @@ import { app } from "/scripts/app.js";
const STYLE_ID = "mrmth-script-line-numbers-unified";
+function escapeHtml(value) {
+ return value
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
if (!window.__mrmthScriptInputUnifiedInit) {
window.__mrmthScriptInputUnifiedInit = true;
@@ -43,33 +52,40 @@ if (!window.__mrmthScriptInputUnifiedInit) {
will-change: transform;
}
- .mrmth-editor-container {
- position: relative;
- flex: 1;
- min-width: 0;
- min-height: 170px;
- overflow: hidden;
+ .mrmth-editor-container {
+ position: relative;
+ flex: 1;
+ min-width: 0;
+ min-height: 170px;
+ overflow: hidden;
+ background: transparent; /* ⬅️ DŮLEŽITÉ */
+ }
+
+ .mrmth-syntax-layer {
+ position: absolute;
+ inset: 0;
+ z-index: 3; /* ⬅️ výš než textarea */
+ pointer-events: none;
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+ box-sizing: border-box;
+ overflow: hidden;
+ opacity: 1;
}
+
.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;
+ position: absolute; /* ⬅️ ne relative */
+ inset: 0;
+ z-index: 2;
- font-family: monospace !important;
- font-size: 12px !important;
- line-height: 1.45 !important;
- padding: 8px 10px !important;
+ color: transparent !important;
+ caret-color: #ffffff !important;
+ background: transparent !important;
- white-space: pre-wrap !important;
- overflow-wrap: anywhere !important;
- word-break: break-word !important;
+ mix-blend-mode: normal; /* ⬅️ ZRUŠ lighten */
+ -webkit-text-fill-color: transparent;
}
.mrmth-measure {
@@ -83,19 +99,26 @@ if (!window.__mrmthScriptInputUnifiedInit) {
box-sizing: border-box;
height: auto;
}
+
+ .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);
}
- function escapeHtml(value) {
- return value
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
- }
-
function hasNearbyScriptLabel(el) {
const root = el.closest("div, section, article") ?? el.parentElement;
if (!root) return false;
@@ -255,10 +278,14 @@ if (!window.__mrmthScriptInputUnifiedInit) {
const measureEl = document.createElement("pre");
measureEl.className = "mrmth-measure";
+ const syntaxLayer = document.createElement("pre");
+ syntaxLayer.className = "mrmth-syntax-layer";
+
parent.replaceChild(wrapper, textarea);
wrapper.appendChild(gutter);
wrapper.appendChild(editorContainer);
editorContainer.appendChild(measureEl);
+ editorContainer.appendChild(syntaxLayer);
editorContainer.appendChild(textarea);
textarea.classList.add("mrmth-script-textarea");
@@ -269,12 +296,26 @@ if (!window.__mrmthScriptInputUnifiedInit) {
textarea.autocorrect = "off";
addBracketAutoClose(textarea);
- const lineHeightPx = getLineHeightPx(textarea);
+ const cs = getComputedStyle(textarea);
+ syntaxLayer.style.fontFamily = cs.fontFamily;
+ syntaxLayer.style.fontSize = cs.fontSize;
+ syntaxLayer.style.fontWeight = cs.fontWeight;
+ syntaxLayer.style.fontStyle = cs.fontStyle;
+ syntaxLayer.style.lineHeight = cs.lineHeight;
+ syntaxLayer.style.padding = cs.padding;
- const refresh = () => updateNumbersWrapped(textarea, gutterContent, measureEl, lineHeightPx);
+ const lineHeightPx = getLineHeightPx(textarea);
+ const refresh = () => {
+ updateNumbersWrapped(textarea, gutterContent, measureEl, lineHeightPx);
+ updateHighlight(textarea, syntaxLayer);
+ };
textarea.addEventListener("input", refresh);
- textarea.addEventListener("scroll", refresh);
+ textarea.addEventListener("scroll", () => {
+ gutterContent.style.transform = `translateY(${-textarea.scrollTop}px)`;
+ syntaxLayer.scrollTop = textarea.scrollTop;
+ syntaxLayer.scrollLeft = textarea.scrollLeft;
+ });
if (window.ResizeObserver) {
const ro = new ResizeObserver(refresh);
@@ -283,6 +324,19 @@ if (!window.__mrmthScriptInputUnifiedInit) {
requestAnimationFrame(refresh);
}
+ function updateHighlight(textarea, syntaxLayer) {
+ // trik: přidat mezeru, pokud končí novým řádkem
+ const source =
+ textarea.value.endsWith("\n")
+ ? textarea.value + " "
+ : textarea.value;
+
+ syntaxLayer.innerHTML = renderHighlight(source);
+
+ // synchronizace scrollu
+ syntaxLayer.scrollTop = textarea.scrollTop;
+ syntaxLayer.scrollLeft = textarea.scrollLeft;
+ }
function scan(root = document) {
root.querySelectorAll("textarea").forEach(attachToTextarea);
@@ -306,4 +360,76 @@ if (!window.__mrmthScriptInputUnifiedInit) {
observer.observe(document.body, { childList: true, subtree: 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"
+]);
+
+const BRACKET_PAIRS = { "(": ")", "[": "]", "{": "}" };
+const OPENING_BRACKETS = new Set(["(", "[", "{"]);
+const CLOSING_BRACKETS = new Set([")", "]", "}"]);
+
+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 bracketStack = [];
+ const depthByType = { "(": 0, "[": 0, "{": 0 };
+ let m;
+
+ while ((m = pattern.exec(text)) !== null) {
+ const value = m[0];
+ let type = "text";
+ let depth = 0;
+
+ 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();
+ }
+ }
+ 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("");
}
\ No newline at end of file