Merge pull request #50 from newtextdoc1111/feature/auto_format_blocklist
Feature/auto format blocklist
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
formatPromptText,
|
||||
__test__
|
||||
} from "../../web/js/auto-formatter.js";
|
||||
|
||||
const {
|
||||
shouldAutoFormat
|
||||
} = __test__;
|
||||
|
||||
describe('AutoFormatter Functions', () => {
|
||||
|
||||
describe('shouldAutoFormat', () => {
|
||||
const mockNodeInfo = (nodeType, inputName) => ({
|
||||
nodeType,
|
||||
inputName
|
||||
});
|
||||
|
||||
test('should return false for empty text', () => {
|
||||
expect(shouldAutoFormat('', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat(' ', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for blocklisted nodes', () => {
|
||||
expect(shouldAutoFormat('some text,', mockNodeInfo('Power Puter (rgthree)', 'code'))).toBe(false);
|
||||
expect(shouldAutoFormat('some text,', mockNodeInfo('LoraLoaderBlockWeight //Inspire', 'block_vector'))).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for numeric data or single-letter placeholders', () => {
|
||||
expect(shouldAutoFormat('0,0,0,1,1,1', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat('0.5, -1.2, 0.8', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat('A,B,R', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat('X, 1.5, Y', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true for text with "word + comma" pattern', () => {
|
||||
expect(shouldAutoFormat('1girl, blue hair,', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(true);
|
||||
expect(shouldAutoFormat('tag1, tag2', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false if "word + comma" pattern is not found', () => {
|
||||
expect(shouldAutoFormat('hello world', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
expect(shouldAutoFormat('tag1 tag2', mockNodeInfo('CLIPTextEncode', 'text'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatPromptText', () => {
|
||||
test('should format text by adding comma and space after tags', () => {
|
||||
const input = 'tag1,tag2,tag3';
|
||||
const expected = 'tag1, tag2, tag3, ';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should remove extra spaces around tags', () => {
|
||||
const input = ' tag1 , tag2 ';
|
||||
const expected = 'tag1, tag2, ';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should preserve special syntax like weights', () => {
|
||||
const input = '(tag1:1.2), [tag2]';
|
||||
// Note: The current implementation splits by comma.
|
||||
// If the input is "(tag1:1.2), [tag2]", it splits into "(tag1:1.2)" and "[tag2]".
|
||||
// Then joins with ", ".
|
||||
const expected = '(tag1:1.2), [tag2], ';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should handle multiple lines', () => {
|
||||
const input = 'tag1, tag2\ntag3, tag4';
|
||||
const expected = 'tag1, tag2, \ntag3, tag4, ';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should keep empty lines unchanged', () => {
|
||||
const input = 'tag1, tag2\n\ntag3, tag4';
|
||||
const expected = 'tag1, tag2, \n\ntag3, tag4, ';
|
||||
expect(formatPromptText(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test('should handle empty input', () => {
|
||||
expect(formatPromptText('')).toBe('');
|
||||
expect(formatPromptText(null)).toBe(null);
|
||||
expect(formatPromptText(undefined)).toBe(undefined);
|
||||
});
|
||||
|
||||
test('should handle input with only spaces', () => {
|
||||
expect(formatPromptText(' ')).toBe(' ');
|
||||
});
|
||||
});
|
||||
});
|
||||
+103
-2
@@ -1,4 +1,65 @@
|
||||
import { settingValues } from './settings.js';
|
||||
import { NodeInfo } from './node-info.js';
|
||||
|
||||
|
||||
/**
|
||||
* Determines if the text content should be auto-formatted.
|
||||
*
|
||||
* Format conditions:
|
||||
* 1. Skip formatting if node is in blocklist
|
||||
* 2. Skip formatting if text contains only numbers or single letters (separated by commas)
|
||||
* 3. Format if text contains "word + comma" pattern at least twice
|
||||
* 4. Otherwise, don't format
|
||||
*
|
||||
* @param {NodeInfo} nodeInfo - The node information.
|
||||
* @returns {boolean} - True if the text should be formatted, false otherwise.
|
||||
*/
|
||||
function shouldAutoFormat(text, nodeInfo) {
|
||||
if (!text || text.trim().length === 0) return false;
|
||||
|
||||
// 1. Check if the node name is in the blocklist
|
||||
const blocklist = [
|
||||
["Power Puter (rgthree)", "code"],
|
||||
["LoraLoaderBlockWeight //Inspire", "block_vector"]
|
||||
];
|
||||
|
||||
const isBlocklisted = blocklist.some(([type, input]) =>
|
||||
type === nodeInfo.nodeType && input === nodeInfo.inputName
|
||||
);
|
||||
|
||||
if (isBlocklisted) {
|
||||
// console.debug(`[Autocomplete-Plus] auto-formatter on blur => nodeType: ${nodeInfo.nodeType}, inputName: ${nodeInfo.inputName} => blocklisted`);
|
||||
return false;
|
||||
} else {
|
||||
// console.debug(`[Autocomplete-Plus] auto-formatter on blur => nodeType: ${nodeInfo.nodeType}, inputName: ${nodeInfo.inputName}`);
|
||||
}
|
||||
|
||||
|
||||
const trimmedText = text.trim();
|
||||
|
||||
// 2. Check if the text is purely numeric data or single-letter placeholders with commas
|
||||
// (e.g., "0,0,0,1,1,1" or "0.5, -1.2, 0.8" or "A,B,R" for LoRA Block Weight)
|
||||
const elements = trimmedText.split(',').map(el => el.trim());
|
||||
const isSingleLetterOrNumeric = elements.every(el => {
|
||||
if (/^[A-Za-z]$/.test(el)) return true;
|
||||
if (/^-?\d+(\.\d+)?$/.test(el)) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (isSingleLetterOrNumeric && elements.length > 0) {
|
||||
return false; // Don't format numeric data or single-letter template patterns
|
||||
}
|
||||
|
||||
// 3. Check if the text contains the pattern "word + comma"
|
||||
const wordCommaPattern = /\w+\s*,/g;
|
||||
const matches = trimmedText.match(wordCommaPattern);
|
||||
|
||||
if (matches == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true; // Text should be formatted
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the prompt text: add a comma and space after each tag, and remove extra spaces.
|
||||
@@ -79,12 +140,19 @@ export class AutoFormatterEventHandler {
|
||||
* Handle blur event to trigger auto-formatting
|
||||
* @param {Event} event - The blur event
|
||||
*/
|
||||
handleBlur(event) {
|
||||
handleBlur(event, nodeInfo) {
|
||||
if (
|
||||
settingValues.enableAutoFormat &&
|
||||
settingValues.autoFormatTrigger === 'auto' &&
|
||||
event.target.tagName === 'TEXTAREA'
|
||||
) {
|
||||
formatTextareaOnBlur(event.target);
|
||||
const textarea = event.target;
|
||||
const text = textarea.value;
|
||||
|
||||
// Check if the content should be auto-formatted
|
||||
if (shouldAutoFormat(text, nodeInfo)) {
|
||||
formatTextareaOnBlur(textarea);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,4 +163,37 @@ export class AutoFormatterEventHandler {
|
||||
handleKeyUp(event) { }
|
||||
handleMouseMove(event) { }
|
||||
handleClick(event) { }
|
||||
|
||||
/**
|
||||
* Format textarea content via manual trigger (e.g., keyboard shortcut)
|
||||
* @param {HTMLTextAreaElement} textarea - The textarea element to format
|
||||
* @param {NodeInfo} nodeInfo - The node information
|
||||
* @returns {boolean} - True if formatting was performed, false otherwise
|
||||
*/
|
||||
applyFormatTextarea(textarea, nodeInfo) {
|
||||
if (!textarea || textarea.tagName !== 'TEXTAREA') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!settingValues.enableAutoFormat) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const text = textarea.value;
|
||||
|
||||
if (shouldAutoFormat(text, nodeInfo)) {
|
||||
formatTextareaOnBlur(textarea);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Export functions for testing
|
||||
const isTestEnvironment = typeof process !== 'undefined' && process.env.NODE_ENV === 'test';
|
||||
export const __test__ = isTestEnvironment
|
||||
? {
|
||||
shouldAutoFormat
|
||||
}
|
||||
: undefined;
|
||||
|
||||
+97
-16
@@ -7,24 +7,26 @@ import { TagSource, loadDataAsync } from "./data.js";
|
||||
import { AutocompleteEventHandler } from "./autocomplete.js";
|
||||
import { RelatedTagsEventHandler } from "./related-tags.js";
|
||||
import { AutoFormatterEventHandler } from "./auto-formatter.js";
|
||||
import { NodeInfo } from "./node-info.js";
|
||||
|
||||
// --- Constants ---
|
||||
const id = "AutocompletePlus";
|
||||
const name = "Autocomplete Plus";
|
||||
|
||||
// --- Module-level variables ---
|
||||
const autocompleteEventHandler = new AutocompleteEventHandler();
|
||||
const relatedTagsEventHandler = new RelatedTagsEventHandler();
|
||||
const autoFormatterEventHandler = new AutoFormatterEventHandler();
|
||||
const attachedElementNodeInfoMap = new WeakMap(); // Map to track attached elements and their node info
|
||||
|
||||
// --- Functions ---
|
||||
/**
|
||||
* Initialize event handlers for the autocomplete and related tags features.
|
||||
*/
|
||||
function initializeEventHandlers() {
|
||||
const autocompleteEventHandler = new AutocompleteEventHandler();
|
||||
const relatedTagsEventHandler = new RelatedTagsEventHandler();
|
||||
const autoFormatterEventHandler = new AutoFormatterEventHandler();
|
||||
const attachedElements = new WeakSet(); // Keep track of elements that have listeners attached
|
||||
|
||||
// Function to attach listeners
|
||||
function attachListeners(element) {
|
||||
if (attachedElements.has(element)) return; // Prevent double attachment
|
||||
function attachListeners(element, nodeInfo) {
|
||||
if (attachedElementNodeInfoMap.has(element)) return; // Prevent double attachment
|
||||
|
||||
element.addEventListener('input', handleInput);
|
||||
element.addEventListener('focus', handleFocus);
|
||||
@@ -37,7 +39,7 @@ function initializeEventHandlers() {
|
||||
element.addEventListener('mousemove', handleMouseMove);
|
||||
element.addEventListener('click', handleClick);
|
||||
|
||||
attachedElements.add(element); // Mark as attached
|
||||
attachedElementNodeInfoMap.set(element, nodeInfo); // Mark as attached and store node info
|
||||
}
|
||||
|
||||
// Attempt Widget Override as the primary method
|
||||
@@ -50,12 +52,14 @@ function initializeEventHandlers() {
|
||||
|
||||
// Check if the widget has an inputEl and if it's a TEXTAREA
|
||||
// This is to ensure we are targeting multiline text inputs, related to '.comfy-multiline-input'
|
||||
if (result && result.widget && result.widget.inputEl && result.widget.inputEl.tagName === 'TEXTAREA') {
|
||||
if (result && result.widget
|
||||
&& result.widget.inputEl && result.widget.inputEl.tagName === 'TEXTAREA' && !result.widget.inputEl.readOnly) {
|
||||
const widgetConfig = inputData && inputData[1] ? inputData[1] : {};
|
||||
// Future: Add checks for Autocomplete Plus specific configurations if needed
|
||||
// e.g., if (widgetConfig["AutocompletePlus.enabled"] === false) return result;
|
||||
|
||||
attachListeners(result.widget.inputEl);
|
||||
const nodeInfo = new NodeInfo(node.comfyClass || node.constructor.name, inputName);
|
||||
attachListeners(result.widget.inputEl, nodeInfo);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -75,9 +79,11 @@ function initializeEventHandlers() {
|
||||
targetSelectors.forEach(selector => {
|
||||
// Check if the added node itself matches or contains matching elements
|
||||
if (node.matches(selector)) {
|
||||
attachListeners(node);
|
||||
attachListeners(node, new NodeInfo('Fallback', 'unknown'));
|
||||
} else {
|
||||
node.querySelectorAll(selector).forEach(attachListeners);
|
||||
node.querySelectorAll(selector).forEach(el => {
|
||||
attachListeners(el, new NodeInfo('Fallback', 'unknown'));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -87,49 +93,72 @@ function initializeEventHandlers() {
|
||||
|
||||
// Initial scan for existing elements
|
||||
targetSelectors.forEach(selector => {
|
||||
document.querySelectorAll(selector).forEach(attachListeners);
|
||||
document.querySelectorAll(selector).forEach(el => {
|
||||
attachListeners(el, new NodeInfo('Fallback', 'unknown'));
|
||||
});
|
||||
});
|
||||
|
||||
// Start observing the document body for changes
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NodeInfo for the event target element
|
||||
* @param {Event} event - The DOM event
|
||||
* @returns {Object|null} NodeInfo object or undefined if not found
|
||||
*/
|
||||
function getNodeInfo(event) {
|
||||
const nodeInfo = attachedElementNodeInfoMap.get(event.target);
|
||||
if (!nodeInfo) {
|
||||
console.warn('[Autocomplete-Plus] Node info not found for element in ', event.target);
|
||||
return null;
|
||||
}
|
||||
|
||||
return nodeInfo;
|
||||
}
|
||||
|
||||
function handleInput(event) {
|
||||
autocompleteEventHandler.handleInput(event);
|
||||
relatedTagsEventHandler.handleInput(event);
|
||||
autoFormatterEventHandler.handleInput(event);
|
||||
}
|
||||
|
||||
function handleFocus(event) {
|
||||
autocompleteEventHandler.handleFocus(event);
|
||||
relatedTagsEventHandler.handleFocus(event);
|
||||
autoFormatterEventHandler.handleFocus(event);
|
||||
}
|
||||
|
||||
function handleBlur(event) {
|
||||
const nodeInfo = getNodeInfo(event); // Get node info to pass to auto formatter
|
||||
|
||||
autocompleteEventHandler.handleBlur(event);
|
||||
relatedTagsEventHandler.handleBlur(event);
|
||||
autoFormatterEventHandler.handleBlur(event);
|
||||
autoFormatterEventHandler.handleBlur(event, nodeInfo);
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
autocompleteEventHandler.handleKeyDown(event);
|
||||
relatedTagsEventHandler.handleKeyDown(event);
|
||||
autoFormatterEventHandler.handleKeyDown(event);
|
||||
}
|
||||
|
||||
function handleKeyUp(event) {
|
||||
autocompleteEventHandler.handleKeyUp(event);
|
||||
relatedTagsEventHandler.handleKeyUp(event);
|
||||
autoFormatterEventHandler.handleKeyUp(event);
|
||||
}
|
||||
|
||||
// New event handler for mousemove to show related tags on hover
|
||||
function handleMouseMove(event) {
|
||||
autocompleteEventHandler.handleMouseMove(event);
|
||||
relatedTagsEventHandler.handleMouseMove(event);
|
||||
autoFormatterEventHandler.handleMouseMove(event);
|
||||
}
|
||||
|
||||
// New event handler for click to show related tags
|
||||
function handleClick(event) {
|
||||
autocompleteEventHandler.handleClick(event);
|
||||
relatedTagsEventHandler.handleClick(event);
|
||||
autoFormatterEventHandler.handleClick(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +276,46 @@ app.registerExtension({
|
||||
await loadDataAsync();
|
||||
},
|
||||
|
||||
// --- Commands ---
|
||||
commands: [
|
||||
{
|
||||
id: id + ".formatPrompt",
|
||||
label: name + ": Format Prompt",
|
||||
function: () => {
|
||||
const activeEl = document.activeElement;
|
||||
|
||||
if (!activeEl || activeEl.tagName !== 'TEXTAREA') {
|
||||
// console.debug('[Autocomplete-Plus] Format command: No textarea is currently focused');
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeInfo = attachedElementNodeInfoMap.get(activeEl);
|
||||
if (!nodeInfo) {
|
||||
console.warn('[Autocomplete-Plus] Format command: Node info not found for focused textarea');
|
||||
// Use fallback NodeInfo
|
||||
const fallbackNodeInfo = new NodeInfo('Unknown', 'unknown');
|
||||
autoFormatterEventHandler.applyFormatTextarea(activeEl, fallbackNodeInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
const formatted = autoFormatterEventHandler.applyFormatTextarea(activeEl, nodeInfo);
|
||||
if (formatted) {
|
||||
// console.debug('[Autocomplete-Plus] Format command: Formatting applied');
|
||||
} else {
|
||||
// console.debug('[Autocomplete-Plus] Format command: Formatting skipped (blocklisted or not applicable)');
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
// --- Keybindings ---
|
||||
keybindings: [
|
||||
{
|
||||
combo: { key: "f", alt: true, shift: true },
|
||||
commandId: id + ".formatPrompt"
|
||||
}
|
||||
],
|
||||
|
||||
// One the Settings Screen, displays reverse order in same category
|
||||
settings: [
|
||||
// --- Tag source Settings ---
|
||||
@@ -421,6 +490,18 @@ app.registerExtension({
|
||||
},
|
||||
|
||||
// --- Auto format settings ---
|
||||
{
|
||||
id: id + '.AutoFormatter.Trigger',
|
||||
name: 'Auto Format Trigger',
|
||||
tooltip: 'Auto: Format automatically when leaving text field.\nManual: Format only via keyboard shortcut. default keybind: (Alt+Shift+F)',
|
||||
type: 'combo',
|
||||
options: ['auto', 'manual'],
|
||||
defaultValue: 'auto',
|
||||
category: [name, 'AutoFormatter', 'Auto Format Trigger'],
|
||||
onChange: (newVal, oldVal) => {
|
||||
settingValues.autoFormatTrigger = newVal;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: id + '.AutoFormatter.EnableAutoFormat',
|
||||
name: 'Enable Auto Format',
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Class to hold information about the node attached to an input element.
|
||||
* Used to control behavior based on node information.
|
||||
*/
|
||||
export class NodeInfo {
|
||||
/**
|
||||
* @param {string} nodeType - The type/class name of the node
|
||||
* @param {string} inputName - The name of the input widget
|
||||
*/
|
||||
constructor(nodeType, inputName) {
|
||||
this.nodeType = nodeType;
|
||||
this.inputName = inputName;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export const settingValues = {
|
||||
|
||||
// Auto format settings
|
||||
enableAutoFormat: true,
|
||||
autoFormatTrigger: 'auto', // Options: 'auto' (format on blur + shortcut), 'manual' (shortcut only)
|
||||
|
||||
|
||||
// Internal logic settings
|
||||
|
||||
Reference in New Issue
Block a user