AI: Checkpoint 2
This commit is contained in:
+302
-177
@@ -1,184 +1,309 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
|
||||
const STYLE_ID = "mrmth-script-line-numbers-v2";
|
||||
const BRACKET_PAIRS = { "(": ")", "[": "]", "{": "}" };
|
||||
const OPENING_BRACKETS = new Set(["(", "[", "{"]);
|
||||
const STYLE_ID = "mrmth-script-line-numbers-unified";
|
||||
|
||||
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;
|
||||
if (!window.__mrmthScriptInputUnifiedInit) {
|
||||
window.__mrmthScriptInputUnifiedInit = true;
|
||||
|
||||
white-space: pre !important;
|
||||
overflow: auto !important;
|
||||
word-break: normal !important;
|
||||
overflow-wrap: normal !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;
|
||||
|
||||
// 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 isLikelyScriptTextarea(el) {
|
||||
if (!(el instanceof HTMLTextAreaElement)) return false;
|
||||
if (el.dataset.mrmthV2Script === "1") return false;
|
||||
|
||||
const attrs = [
|
||||
el.name,
|
||||
el.id,
|
||||
el.getAttribute("data-name"),
|
||||
el.getAttribute("aria-label"),
|
||||
el.getAttribute("placeholder"),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
|
||||
// accept either direct attribute match OR nearby label
|
||||
return attrs.includes("script") || hasNearbyScriptLabel(el);
|
||||
}
|
||||
|
||||
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)`;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const after = textarea.value[start] ?? "";
|
||||
const closing = BRACKET_PAIRS[key];
|
||||
if (/[a-zA-Z0-9_]/.test(after) || after === closing) return;
|
||||
|
||||
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 }));
|
||||
});
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function attachToTextarea(textarea) {
|
||||
if (!isLikelyScriptTextarea(textarea)) return;
|
||||
|
||||
const parent = textarea.parentElement;
|
||||
if (!parent) return;
|
||||
|
||||
ensureStyles();
|
||||
hideNearbyScriptLabels(textarea);
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "mrmth-v2-editor";
|
||||
|
||||
const gutter = document.createElement("div");
|
||||
gutter.className = "mrmth-v2-gutter";
|
||||
|
||||
parent.replaceChild(wrapper, textarea);
|
||||
wrapper.appendChild(gutter);
|
||||
wrapper.appendChild(textarea);
|
||||
|
||||
textarea.classList.add("mrmth-v2-textarea");
|
||||
textarea.dataset.mrmthV2Script = "1";
|
||||
|
||||
// disable red spell-check underline for code
|
||||
textarea.spellcheck = false;
|
||||
textarea.autocapitalize = "off";
|
||||
textarea.autocomplete = "off";
|
||||
textarea.autocorrect = "off";
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
},
|
||||
});
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function isLikelyScriptTextarea(el) {
|
||||
if (!(el instanceof HTMLTextAreaElement)) return false;
|
||||
if (el.dataset.mrmthEnhanced === "1") return false;
|
||||
|
||||
const attrs = [
|
||||
el.name,
|
||||
el.id,
|
||||
el.getAttribute("data-name"),
|
||||
el.getAttribute("aria-label"),
|
||||
el.getAttribute("placeholder"),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
|
||||
return attrs.includes("script") || hasNearbyScriptLabel(el);
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 += `<span class="mrmth-ln-marker" data-ln="${i}"></span>${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";
|
||||
|
||||
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 });
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user