From 53fa22f39c73664f09f6993f5ab6b5c2b039f1cb Mon Sep 17 00:00:00 2001 From: Will Miao Date: Mon, 7 Sep 2026 19:20:15 +0800 Subject: [PATCH] fix(lora-loader): preserve repeated spaces inside lora names Whitespace cleanup in cleanupLoraSyntax() and the autocomplete blur formatter collapsed all whitespace runs, including inside tags. A file named 'test - 0021.safetensors' was rewritten to 'test - 0021' in the node text, so runtime file resolution failed. Protect lora tags with placeholders (or segment splitting) so only whitespace between entries is normalized; names inside tags are kept byte-for-byte. --- .../autocomplete.formatOnBlur.test.js | 58 +++++++++++++++++++ .../frontend/managers/loraSyntaxUtils.test.js | 21 +++++++ web/comfyui/autocomplete.js | 14 ++++- web/comfyui/lora_syntax_utils.js | 16 ++++- 4 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 tests/frontend/components/autocomplete.formatOnBlur.test.js diff --git a/tests/frontend/components/autocomplete.formatOnBlur.test.js b/tests/frontend/components/autocomplete.formatOnBlur.test.js new file mode 100644 index 00000000..0e8f7b89 --- /dev/null +++ b/tests/frontend/components/autocomplete.formatOnBlur.test.js @@ -0,0 +1,58 @@ +import { describe, it, expect, vi } from 'vitest'; + +const { + API_MODULE, + APP_MODULE, + AUTOCOMPLETE_MODULE, +} = vi.hoisted(() => ({ + API_MODULE: new URL('../../../scripts/api.js', import.meta.url).pathname, + APP_MODULE: new URL('../../../scripts/app.js', import.meta.url).pathname, + AUTOCOMPLETE_MODULE: new URL('../../../web/comfyui/autocomplete.js', import.meta.url).pathname, +})); + +vi.mock(API_MODULE, () => ({ + api: { + fetchApi: vi.fn(), + }, +})); + +vi.mock(APP_MODULE, () => ({ + app: { + canvas: { + ds: { scale: 1 }, + }, + extensionManager: { + setting: { + get: vi.fn(), + set: vi.fn(), + }, + }, + registerExtension: vi.fn(), + }, +})); + +describe('formatAutocompleteTextOnBlur', () => { + it('preserves repeated spaces inside LoRA names', async () => { + const { formatAutocompleteTextOnBlur } = await import(AUTOCOMPLETE_MODULE); + + expect(formatAutocompleteTextOnBlur('')).toBe( + '' + ); + }); + + it('preserves repeated spaces across multiple LoRA entries', async () => { + const { formatAutocompleteTextOnBlur } = await import(AUTOCOMPLETE_MODULE); + + expect( + formatAutocompleteTextOnBlur(',') + ).toBe(', '); + }); + + it('still normalizes whitespace outside LoRA tags', async () => { + const { formatAutocompleteTextOnBlur } = await import(AUTOCOMPLETE_MODULE); + + expect(formatAutocompleteTextOnBlur('masterpiece, best quality')).toBe( + 'masterpiece, best quality' + ); + }); +}); diff --git a/tests/frontend/managers/loraSyntaxUtils.test.js b/tests/frontend/managers/loraSyntaxUtils.test.js index 0fee8e45..a4bdf50c 100644 --- a/tests/frontend/managers/loraSyntaxUtils.test.js +++ b/tests/frontend/managers/loraSyntaxUtils.test.js @@ -40,6 +40,15 @@ describe("applyLoraValuesToText", () => { expect(result).toBe(""); }); + + it("preserves repeated spaces inside LoRA names", () => { + const original = ""; + const result = applyLoraValuesToText(original, [ + { name: "test - 0021", strength: 0.5 } + ]); + + expect(result).toBe(""); + }); }); describe("normalizeStrengthValue", () => { @@ -74,6 +83,18 @@ describe("cleanupLoraSyntax", () => { it("collapses whitespace and stray commas", () => { expect(cleanupLoraSyntax(" , ," )).toBe(""); }); + + it("preserves repeated spaces inside LoRA names", () => { + expect(cleanupLoraSyntax(" , ,")).toBe( + "" + ); + }); + + it("still normalizes whitespace between entries", () => { + expect( + cleanupLoraSyntax(" ") + ).toBe(" "); + }); }); describe("debounce", () => { diff --git a/web/comfyui/autocomplete.js b/web/comfyui/autocomplete.js index 6d2ffdfe..3e7e609c 100644 --- a/web/comfyui/autocomplete.js +++ b/web/comfyui/autocomplete.js @@ -227,8 +227,20 @@ function formatAutocompleteInsertion(text = '') { return getAutocompleteAppendCommaPreference() ? `${trimmed},` : `${trimmed} `; } +// Matches a complete tag. Kept +// permissive on the strength fields (mirrors the backend parser) so tags +// are still protected while the user is mid-edit. +const LORA_TAG_PATTERN = /(]+:[^:>]+(?::[^:>]+)?>)/gi; + function normalizeAutocompleteSegment(segment = '') { - return segment.replace(/\s+/g, ' ').trim(); + // Collapse whitespace only outside tags: names inside the tags + // may legitimately contain repeated spaces (e.g. "test - 0021"), and + // collapsing them breaks file resolution at runtime. + return segment + .split(LORA_TAG_PATTERN) + .map((part, index) => (index % 2 === 1 ? part : part.replace(/\s+/g, ' '))) + .join('') + .trim(); } export function formatAutocompleteTextOnBlur(text = '') { diff --git a/web/comfyui/lora_syntax_utils.js b/web/comfyui/lora_syntax_utils.js index b7be3d2b..5770fc38 100644 --- a/web/comfyui/lora_syntax_utils.js +++ b/web/comfyui/lora_syntax_utils.js @@ -38,7 +38,17 @@ function cleanupLoraSyntax(text) { return ""; } - let cleaned = text + // Protect tags with placeholders before cleanup: names inside + // the tags may legitimately contain repeated spaces or commas (e.g. + // "test - 0021"), and collapsing them breaks file resolution at runtime. + const protectedTags = []; + LORA_PATTERN.lastIndex = 0; + const masked = text.replace(LORA_PATTERN, (match) => { + protectedTags.push(match); + return `\u0000${protectedTags.length - 1}\u0000`; + }); + + let cleaned = masked .replace(/\s+/g, " ") .replace(/,\s*,+/g, ",") .replace(/\s*,\s*/g, ",") @@ -51,7 +61,9 @@ function cleanupLoraSyntax(text) { cleaned = cleaned.replace(/(^,)|(,$)/g, ""); cleaned = cleaned.replace(/,\s*/g, ", "); - return cleaned.trim(); + return cleaned + .trim() + .replace(/\u0000(\d+)\u0000/g, (_, index) => protectedTags[Number(index)]); } export function applyLoraValuesToText(originalText, loras) {