// ECMAScript 2020+ // Разбор расширений и подготовка для табов class TabsExtensionParser { /** Клонируем label с чекбоксом и возвращаем именно label (или null) * @param {boolean} enabled * @returns {HTMLLabelElement|null} */ static #cloneCheckbox(enabled) { const base = document.getElementById("TABSEX_CHECKBOX"); if (!base) return null; const label = base.querySelector('label')?.cloneNode(true); if (!label) return null; label.style.margin = "1em 0em"; label.checkbox = label.querySelector("input"); if (label.checkbox) label.checkbox.checked = !!enabled; return label; } /** * Убираем версию из имени (если есть). Пустые строки -> null. * @param {string} name * @returns {string|null} */ static #sanitizeExtensionName(name) { if (typeof name !== 'string' || name.trim().length === 0) return null; const version_pattern = /([Vv](?:er)?[\.\s]*\d)/; return name.split(version_pattern)[0].trim(); } /** * Разбор одного узла из контейнера расширений * @param {HTMLDivElement} node * @param {string[]} keep * @returns {[string|null, HTMLDivElement|null]} */ static #parseObject(node, keep) { if (!node) return [null, null]; // Спец-случай: форма со списком скриптов if (node.classList.contains("form")) { const scripts = node.querySelector(".gradio-dropdown"); if (!scripts) return [null, null]; const script_block = document.createElement("div"); script_block.style.display = 'none'; scripts.style.margin = '10px 0px'; script_block.appendChild(scripts); script_block.setAttribute("ext-label", "Scripts"); // прячем исходный form-узел, чтобы не было дубля; решение о дубликате принимает parse() node.style.display = "none"; return ["Scripts", script_block]; } const styler = node.querySelector(".styler"); if (!styler) return [null, null]; const accordion = node.querySelector(".gradio-accordion"); if (!accordion) return [null, null]; const isInput = accordion.classList.contains("input-accordion"); const displayName = accordion.querySelector(".label-wrap>span")?.textContent; if (!displayName) return [null, null]; const extensionName = this.#sanitizeExtensionName(displayName); if (!extensionName || keep.includes(extensionName)) return [null, null]; const contents = [...accordion.children].filter( (div) => !div.classList.contains("hide") && !div.classList.contains("label-wrap") && div.children.length > 0 ); if (contents.length === 0) return [null, null]; const content = contents[0]; if (isInput) { const checkbox = accordion.querySelector(".input-accordion-checkbox"); const dummy = this.#cloneCheckbox(checkbox?.checked ?? false); // Ставим обработчики только если всё есть if (dummy && dummy.checkbox && checkbox) { dummy.checkbox.onchange = () => { if (checkbox.checked !== dummy.checkbox.checked) checkbox.click(); }; checkbox.onchange = () => { if (checkbox.checked !== dummy.checkbox.checked) dummy.checkbox.click(); }; content.insertBefore(dummy, content.firstElementChild); } } // Прячем исходный блок с аккордеоном node.style.display = "none"; // Переносим якорь на новый content (и переименовываем старый id безопасно) if (accordion.id && !accordion.id.startsWith("component-")) { content.id = accordion.id; accordion.id = `moved-${accordion.id}`; if (isInput) { // Предотвращаем ошибки в консоли в сетапе webui content.visibleCheckbox = { checked: null }; content.onVisibleCheckboxChange = () => {}; } } content.setAttribute("ext-label", displayName); return [extensionName, content]; } /** * Доп. блок "Extra Options" * @param {'txt'|'img'} mode * @returns {[string|null, HTMLDivElement|null]} */ static #extra_options(mode) { const extra_options = document.getElementById(`extra_options_${mode}2img`); if (!extra_options || !extra_options.classList.contains("gradio-accordion")) return [null, null]; const styler = extra_options.parentElement; if (styler) styler.style.display = "none"; const contents = [...extra_options.children].filter( (div) => !div.classList.contains("hide") && !div.classList.contains("label-wrap") && div.children.length > 0 ); if (contents.length === 0) return [null, null]; const content = contents[0]; content.setAttribute("ext-label", "Extra Options"); return ["Extra Options", content]; } /** * Разбор всех расширений * @param {'txt'|'img'} mode * @param {string[]} keep * @returns {Object.} */ static parse(mode, keep) { const validExtensions = {}; const container = document.getElementById(`${mode}2img_script_container`); const styler = container?.querySelector(".styler"); if (!container || !styler) return validExtensions; const children = Array.from(styler.children); let foundScripts = false; // флаг "Scripts уже встретился" в ЭТОМ контейнере // Важно: НИЧЕГО больше не «скидываем» в Scripts — разбираем каждый узел отдельно. for (const node of children) { // не допускаем второй Scripts в пределах текущего контейнера if (foundScripts && node.classList.contains("form")) { node.style.display = "none"; continue; } const [name, content] = this.#parseObject(node, keep); if (name && content) validExtensions[name] = content; if (name === "Scripts") foundScripts = true; } const [extra, options] = this.#extra_options(mode); if (extra && options) validExtensions[extra] = options; return validExtensions; } }