297 lines
12 KiB
JavaScript
297 lines
12 KiB
JavaScript
import { app } from "/scripts/app.js";
|
|
import { api } from "../../../scripts/api.js"; // Assuming textHighlight's relative path works
|
|
|
|
// Flag to prevent multiple enhancements (mimicking textHighlight)
|
|
const enhancedTextareas = new WeakSet();
|
|
|
|
console.log("SmartPrompt Extension: Loading script");
|
|
|
|
// Helper function to escape HTML characters
|
|
function escapeHtml(text) {
|
|
const map = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
// Keep only essential HTML entity escaping
|
|
return text.replace(/[&<>\"\']/g, m => map[m]);
|
|
}
|
|
|
|
// NEW: Helper function to wrap text in a styled span
|
|
function colorizeText(style, text) {
|
|
let resultHtml = '';
|
|
let currentWord = '';
|
|
for (let i = 0; i < text.length; i++) {
|
|
const char = text[i];
|
|
if (char === ' ') {
|
|
if (currentWord) {
|
|
resultHtml += `<span style="${style}">${escapeHtml(currentWord)}</span>`;
|
|
currentWord = '';
|
|
}
|
|
resultHtml += ' '; // Append the actual space
|
|
} else {
|
|
currentWord += char;
|
|
}
|
|
}
|
|
// Append any remaining word at the end
|
|
if (currentWord) {
|
|
resultHtml += `<span style="${style}">${escapeHtml(currentWord)}</span>`;
|
|
}
|
|
return resultHtml;
|
|
}
|
|
|
|
// Helper function to style default text, highlighting punctuation
|
|
function styleDefaultText(text) {
|
|
const defaultTextStyle = "color: rgb(200, 200, 200);"; // Your default grey
|
|
const punctuationStyle = "color: black;"; // Style for punctuation
|
|
const pipeStyle = "color: red;"; // Style for vertical line / braces
|
|
let resultHtml = '';
|
|
// Iterate character by character
|
|
for (let i = 0; i < text.length; i++) {
|
|
const char = text[i];
|
|
if (['.', ',', '(', ')', ';', ':'].includes(char)) { // Removed { and }
|
|
resultHtml += colorizeText(punctuationStyle, char);
|
|
} else if (['|', '{', '}'].includes(char)) { // Added { and }
|
|
resultHtml += colorizeText(pipeStyle, char);
|
|
} else {
|
|
// Optimization: Group consecutive non-punctuation characters
|
|
let nonPunctSubstring = char;
|
|
// Keep the while loop condition as is, it correctly handles braces and pipe
|
|
while (i + 1 < text.length && !['.', ',', '(', ')', ';', ':', '{', '}', '|'].includes(text[i + 1])) {
|
|
i++;
|
|
nonPunctSubstring += text[i];
|
|
}
|
|
resultHtml += colorizeText(defaultTextStyle, nonPunctSubstring);
|
|
}
|
|
}
|
|
return resultHtml;
|
|
}
|
|
|
|
// Function to sync textarea content to the overlay (styles comments, floats, preceding colon, punctuation, and default text)
|
|
function syncSmartPromptText(textarea, overlay) {
|
|
const lines = textarea.value.split('\n');
|
|
let htmlContent = '';
|
|
// Main regex for comments and colon-floats
|
|
const highlightRegex = /(\/\/.*)|:(\s*\d+(?:\.\d*)?|\s*\.\d+)/g;
|
|
// Define styles
|
|
const defaultTextStyle = "color: rgb(200, 200, 200);";
|
|
const commentStyle = "color: rgb(0, 0, 255);";
|
|
const floatStyle = "color: rgba(0, 255, 0, 1);";
|
|
|
|
lines.forEach(line => {
|
|
let lastIndex = 0;
|
|
let lineHtml = '';
|
|
let match;
|
|
console.log("line: " + line);
|
|
|
|
while ((match = highlightRegex.exec(line)) !== null) {
|
|
// Append text before the match, processed by the helper
|
|
if (match.index > lastIndex) {
|
|
lineHtml += styleDefaultText(line.substring(lastIndex, match.index));
|
|
}
|
|
console.log(lineHtml + " , " + match);
|
|
// Append the matched part(s) using colorizeText
|
|
if (match[1]) { // Comment
|
|
lineHtml += colorizeText(commentStyle, match[1]);
|
|
} else if (match[2]) { // Float
|
|
lineHtml += ':' + colorizeText(floatStyle, match[2]); // Float text
|
|
}
|
|
lastIndex = highlightRegex.lastIndex;
|
|
}
|
|
|
|
// Append any remaining text after the last match, processed by the helper
|
|
if (lastIndex < line.length) {
|
|
lineHtml += styleDefaultText(line.substring(lastIndex));
|
|
}
|
|
|
|
// Handle empty lines: Render a styled non-breaking space using default color
|
|
// Explicitly use nbsp for empty lines to ensure they have content/height
|
|
htmlContent += (lineHtml || `<span style="${defaultTextStyle}"> </span>`) + '<br>';
|
|
});
|
|
|
|
if (htmlContent.endsWith('<br>')) {
|
|
htmlContent = htmlContent.substring(0, htmlContent.length - 4);
|
|
}
|
|
|
|
overlay.innerHTML = htmlContent;
|
|
overlay.scrollTop = textarea.scrollTop;
|
|
overlay.scrollLeft = textarea.scrollLeft;
|
|
}
|
|
|
|
// Function to set overlay style based on textarea
|
|
function setOverlayStyle(inputEl, overlayEl) {
|
|
const textareaStyle = window.getComputedStyle(inputEl);
|
|
|
|
// Base styles for the overlay
|
|
overlayEl.style.cssText = `
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: ${textareaStyle.width};
|
|
height: ${textareaStyle.height};
|
|
font-family: ${textareaStyle.fontFamily};
|
|
font-size: ${textareaStyle.fontSize};
|
|
font-weight: ${textareaStyle.fontWeight};
|
|
line-height: ${textareaStyle.lineHeight};
|
|
letter-spacing: ${textareaStyle.letterSpacing};
|
|
padding: ${textareaStyle.padding};
|
|
margin: ${textareaStyle.margin};
|
|
border: ${textareaStyle.border};
|
|
box-sizing: ${textareaStyle.boxSizing};
|
|
overflow: hidden; /* Let scroll sync handle visible area */
|
|
pointer-events: none;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
color: transparent; /* Keep overlay text transparent */
|
|
background-color: var(--comfy-input-bg); /* Explicitly use the input background variable */
|
|
z-index: 1; /* Ensure overlay is above textarea background */
|
|
`;
|
|
|
|
// Adjust padding for scrollbar to prevent text overlap
|
|
const scrollbarWidth = inputEl.offsetWidth - inputEl.clientWidth - parseFloat(textareaStyle.borderLeftWidth) - parseFloat(textareaStyle.borderRightWidth);
|
|
if (inputEl.scrollHeight > inputEl.clientHeight && scrollbarWidth > 0) {
|
|
overlayEl.style.paddingRight = `${parseFloat(textareaStyle.paddingRight) + scrollbarWidth}px`;
|
|
}
|
|
|
|
// --- Make underlying textarea text semi-transparent ---
|
|
inputEl.style.opacity = '0.4'; // Adjust value (0.0 to 1.0) as needed
|
|
|
|
// Ensure textarea background remains transparent
|
|
inputEl.style.background = "transparent !important"; // Use !important cautiously
|
|
inputEl.style.position = "relative";
|
|
inputEl.style.zIndex = "0";
|
|
// Set caret color explicitly to ensure visibility against potentially dimmed text
|
|
inputEl.style.caretColor = textareaStyle.color || 'var(--input-text)';
|
|
}
|
|
|
|
// Adopted enhanceTextarea structure
|
|
function enhanceSmartPromptTextarea(textarea) {
|
|
if (enhancedTextareas.has(textarea)) return;
|
|
enhancedTextareas.add(textarea);
|
|
|
|
// Create overlay div (similar to textHighlight.js)
|
|
const overlayEl = document.createElement("div");
|
|
overlayEl.className = "smartprompt-overlay"; // Use our own class
|
|
// Insert before the textarea
|
|
textarea.parentNode.insertBefore(overlayEl, textarea);
|
|
|
|
// Make textarea transparent and relative
|
|
textarea.style.position = "relative";
|
|
textarea.style.background = "transparent";
|
|
textarea.style.zIndex = "0";
|
|
textarea.style.caretColor = window.getComputedStyle(textarea).color; // Ensure caret is visible
|
|
|
|
// Initial setup
|
|
setOverlayStyle(textarea, overlayEl);
|
|
syncSmartPromptText(textarea, overlayEl); // Initial sync with our logic
|
|
|
|
// Scroll sync
|
|
textarea.addEventListener("scroll", () => {
|
|
overlayEl.scrollTop = textarea.scrollTop;
|
|
overlayEl.scrollLeft = textarea.scrollLeft;
|
|
});
|
|
|
|
// Input sync
|
|
textarea.addEventListener("input", () => {
|
|
syncSmartPromptText(textarea, overlayEl);
|
|
// Re-apply style in case of resize/reflow needed due to content change? Maybe not needed.
|
|
// setOverlayStyle(textarea, overlayEl);
|
|
});
|
|
|
|
// Observe textarea style/size changes (like textHighlight.js)
|
|
const observer = new MutationObserver(() => {
|
|
// Use requestAnimationFrame to avoid layout thrashing
|
|
requestAnimationFrame(() => {
|
|
if (document.contains(textarea)) { // Check if textarea still exists
|
|
setOverlayStyle(textarea, overlayEl);
|
|
// Resync needed if width/height affects wrapping
|
|
syncSmartPromptText(textarea, overlayEl);
|
|
} else {
|
|
observer.disconnect(); // Stop observing if textarea is gone
|
|
}
|
|
});
|
|
});
|
|
observer.observe(textarea, {
|
|
attributes: true, attributeFilter: ["style", "class"], // Watch style/class changes
|
|
childList: false, subtree: false // Don't need these for textarea
|
|
});
|
|
|
|
// Also observe size changes using ResizeObserver (more reliable for size)
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
requestAnimationFrame(() => {
|
|
if (document.contains(textarea)) {
|
|
setOverlayStyle(textarea, overlayEl);
|
|
syncSmartPromptText(textarea, overlayEl);
|
|
} else {
|
|
resizeObserver.disconnect();
|
|
}
|
|
});
|
|
});
|
|
resizeObserver.observe(textarea);
|
|
|
|
|
|
// Clean up overlay and observers if textarea's node is removed
|
|
const onRemoved = textarea.closest('.comfy-node')?.onRemoved; // Find the node's onRemoved
|
|
if(textarea.closest('.comfy-node')) {
|
|
textarea.closest('.comfy-node').onRemoved = function() {
|
|
console.log("SmartPrompt: Cleaning up overlay and observers for removed node.");
|
|
observer.disconnect();
|
|
resizeObserver.disconnect();
|
|
if (overlayEl && overlayEl.parentNode) {
|
|
overlayEl.remove();
|
|
}
|
|
onRemoved?.apply(this, arguments); // Call original if exists
|
|
}
|
|
}
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "Comfy.SmartHelperNodes.SmartPrompt",
|
|
async beforeRegisterNodeDef(nodeType, nodeData, appInstance) { // Renamed app to avoid conflict
|
|
if (nodeData.name === "SmartPrompt") {
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
onNodeCreated?.apply(this, arguments);
|
|
const widget = this.widgets.find(w => w.name === "text");
|
|
if (widget && widget.inputEl) {
|
|
// Use a polling mechanism with requestAnimationFrame to wait for the textarea to be in the DOM.
|
|
// This is more reliable than a fixed timeout, especially on slow systems or complex workflows.
|
|
let attempts = 0;
|
|
const maxAttempts = 100; // Roughly 1.6 seconds
|
|
|
|
const tryEnhance = () => {
|
|
if (document.contains(widget.inputEl)) {
|
|
enhanceSmartPromptTextarea(widget.inputEl);
|
|
} else if (attempts < maxAttempts) {
|
|
attempts++;
|
|
requestAnimationFrame(tryEnhance);
|
|
} else {
|
|
console.error("SmartPrompt: Textarea not in DOM for enhancement after " + maxAttempts + " attempts.");
|
|
}
|
|
};
|
|
tryEnhance();
|
|
}
|
|
};
|
|
} else {
|
|
// Optional: Log other nodes being processed if needed for debugging
|
|
// console.log(`SmartPrompt Extension: Skipping node type ${nodeData.name}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log("SmartPrompt Extension: Script loaded and registered");
|
|
|
|
// Function to set overlay position based on textarea (Keep this separate)
|
|
function setOverlayPosition(textarea, overlay) {
|
|
const textareaStyle = window.getComputedStyle(textarea);
|
|
requestAnimationFrame(() => {
|
|
const styleProps = ['left', 'top', 'width', 'height', 'display', 'transform', 'transformOrigin'];
|
|
styleProps.forEach(prop => {
|
|
if (overlay.style[prop] !== textareaStyle[prop]) {
|
|
overlay.style[prop] = textareaStyle[prop];
|
|
}
|
|
});
|
|
});
|
|
}
|