mirror of
https://github.com/Darkatse/TauriTavern.git
synced 2026-10-19 15:45:12 +00:00
fix(code-render): defer to JSR and LittleWhiteBox renderers
This commit is contained in:
parent
98f81238e9
commit
4bec253704
6 changed files with 211 additions and 11 deletions
|
|
@ -248,7 +248,7 @@ import {
|
|||
} from './scripts/utils.js';
|
||||
import { debounce_timeout, GENERATION_TYPE_TRIGGERS, IGNORE_SYMBOL, inject_ids, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, OVERSWIPE_BEHAVIOR, SCROLL_BEHAVIOR, SWIPE_DIRECTION, SWIPE_SOURCE, SWIPE_STATE } from './scripts/constants.js';
|
||||
|
||||
import { activateDeferredThirdPartyExtensions, activateStartupSystemExtensions, applyExtensionSettings, cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, runGenerationInterceptors, startOfflineExtensionsDiscovery } from './scripts/extensions.js';
|
||||
import { activateDeferredThirdPartyExtensions, activateStartupSystemExtensions, applyExtensionSettings, cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, isCodeRenderDelegatedToThirdPartyRenderer, runGenerationInterceptors, startOfflineExtensionsDiscovery } from './scripts/extensions.js';
|
||||
import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, initDefaultSlashCommands, initSlashCommandAutoComplete, isExecutingCommandsFromChatInput, pauseScriptExecution, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js';
|
||||
import { initMacroAutoComplete } from './scripts/autocomplete/MacroAutoComplete.js';
|
||||
import {
|
||||
|
|
@ -315,6 +315,7 @@ import {
|
|||
renderInteractiveHtmlCodeBlocks,
|
||||
setHtmlCodeRenderEnabled,
|
||||
setHtmlCodeRenderReplaceLastMessageByDefault,
|
||||
setHtmlCodeRenderSuppressedByExternalRenderer,
|
||||
} from './scripts/html-code-preview.js';
|
||||
import { getPresetManager, initPresetManager } from './scripts/preset-manager.js';
|
||||
import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';
|
||||
|
|
@ -2964,8 +2965,10 @@ export function appendMediaToMessage(mes, messageElement, scrollBehavior = SCROL
|
|||
}
|
||||
|
||||
export function addCopyToCodeBlocks(messageElement) {
|
||||
setHtmlCodeRenderEnabled(extension_settings.code_render?.enabled === true);
|
||||
const shouldRunHtmlCodeRender = extension_settings.code_render?.enabled === true;
|
||||
setHtmlCodeRenderEnabled(shouldRunHtmlCodeRender);
|
||||
setHtmlCodeRenderReplaceLastMessageByDefault(extension_settings.code_render?.replace_last_message_by_default === true);
|
||||
setHtmlCodeRenderSuppressedByExternalRenderer(shouldRunHtmlCodeRender && isCodeRenderDelegatedToThirdPartyRenderer());
|
||||
renderInteractiveHtmlCodeBlocks(messageElement);
|
||||
|
||||
const coordinator = getCodeHighlightCoordinator();
|
||||
|
|
|
|||
|
|
@ -616,6 +616,19 @@ export function findExtension(name) {
|
|||
return { name: internalExtensionName, enabled: isEnabled };
|
||||
}
|
||||
|
||||
const CODE_RENDER_DELEGATE_EXTENSION_NAMES = Object.freeze([
|
||||
'JS-Slash-Runner',
|
||||
'LittleWhiteBox',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Returns true when a known third-party message code renderer should own HTML/script code blocks.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isCodeRenderDelegatedToThirdPartyRenderer() {
|
||||
return CODE_RENDER_DELEGATE_EXTENSION_NAMES.some(name => findExtension(name)?.enabled === true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a deep clone of the manifest for the given extension name.
|
||||
* Accepts either the short name (e.g. `SillyTavern-MyExtension`) or the full internal key
|
||||
|
|
|
|||
|
|
@ -2,10 +2,15 @@ import {
|
|||
reloadCurrentChat,
|
||||
saveSettingsDebounced,
|
||||
} from '../../../script.js';
|
||||
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import {
|
||||
extension_settings,
|
||||
isCodeRenderDelegatedToThirdPartyRenderer,
|
||||
renderExtensionTemplateAsync,
|
||||
} from '../../extensions.js';
|
||||
import {
|
||||
setHtmlCodeRenderEnabled,
|
||||
setHtmlCodeRenderReplaceLastMessageByDefault,
|
||||
setHtmlCodeRenderSuppressedByExternalRenderer,
|
||||
} from '../../html-code-preview.js';
|
||||
|
||||
const MODULE_NAME = 'code-render';
|
||||
|
|
@ -14,6 +19,18 @@ const defaultSettings = {
|
|||
replace_last_message_by_default: false,
|
||||
};
|
||||
|
||||
function syncHtmlCodeRenderState() {
|
||||
const delegated = isCodeRenderDelegatedToThirdPartyRenderer();
|
||||
|
||||
setHtmlCodeRenderEnabled(extension_settings.code_render.enabled);
|
||||
setHtmlCodeRenderReplaceLastMessageByDefault(extension_settings.code_render.replace_last_message_by_default);
|
||||
setHtmlCodeRenderSuppressedByExternalRenderer(delegated);
|
||||
|
||||
$('#code_render_enabled').prop('checked', extension_settings.code_render.enabled);
|
||||
$('#code_render_replace_last_message_by_default').prop('checked', extension_settings.code_render.replace_last_message_by_default);
|
||||
$('#code_render_delegated_notice').toggle(delegated);
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
if (!extension_settings.code_render || typeof extension_settings.code_render !== 'object') {
|
||||
extension_settings.code_render = { ...defaultSettings };
|
||||
|
|
@ -27,17 +44,13 @@ function loadSettings() {
|
|||
extension_settings.code_render.replace_last_message_by_default = defaultSettings.replace_last_message_by_default;
|
||||
}
|
||||
|
||||
setHtmlCodeRenderEnabled(extension_settings.code_render.enabled);
|
||||
setHtmlCodeRenderReplaceLastMessageByDefault(extension_settings.code_render.replace_last_message_by_default);
|
||||
$('#code_render_enabled').prop('checked', extension_settings.code_render.enabled);
|
||||
$('#code_render_replace_last_message_by_default').prop('checked', extension_settings.code_render.replace_last_message_by_default);
|
||||
syncHtmlCodeRenderState();
|
||||
}
|
||||
|
||||
async function onSettingsInput() {
|
||||
extension_settings.code_render.enabled = !!$('#code_render_enabled').prop('checked');
|
||||
extension_settings.code_render.replace_last_message_by_default = !!$('#code_render_replace_last_message_by_default').prop('checked');
|
||||
setHtmlCodeRenderEnabled(extension_settings.code_render.enabled);
|
||||
setHtmlCodeRenderReplaceLastMessageByDefault(extension_settings.code_render.replace_last_message_by_default);
|
||||
syncHtmlCodeRenderState();
|
||||
saveSettingsDebounced();
|
||||
await reloadCurrentChat();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@
|
|||
data-i18n="When enabled, previews in the newest message automatically replace the current last message view.">
|
||||
When enabled, previews in the newest message automatically replace the current last message view.
|
||||
</small>
|
||||
<small id="code_render_delegated_notice" class="extensions_info" style="display: none;"
|
||||
data-i18n="Code Render is disabled while JS-Slash-Runner or LittleWhiteBox is enabled.">
|
||||
Code Render is disabled while JS-Slash-Runner or LittleWhiteBox is enabled.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const HTML_ROOT_PATTERN = /<\s*html[\s>]/i;
|
|||
const DOCTYPE_PATTERN = /<!doctype\b/i;
|
||||
const SCRIPT_PATTERN = /<\s*script\b/i;
|
||||
let htmlCodeRenderEnabled = false;
|
||||
let htmlCodeRenderSuppressedByExternalRenderer = false;
|
||||
let replaceLastMessageByDefault = false;
|
||||
let previewCounter = 0;
|
||||
let isPreviewMessageListenerBound = false;
|
||||
|
|
@ -599,6 +600,15 @@ export function setHtmlCodeRenderEnabled(enabled) {
|
|||
htmlCodeRenderEnabled = !!enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppresses the built-in renderer while a richer third-party renderer owns code blocks.
|
||||
* @param {boolean} suppressed
|
||||
* @returns {void}
|
||||
*/
|
||||
export function setHtmlCodeRenderSuppressedByExternalRenderer(suppressed) {
|
||||
htmlCodeRenderSuppressedByExternalRenderer = !!suppressed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures whether the newest rendered preview should replace the last message by default.
|
||||
* @param {boolean} enabled
|
||||
|
|
@ -614,7 +624,7 @@ export function setHtmlCodeRenderReplaceLastMessageByDefault(enabled) {
|
|||
* @returns {void}
|
||||
*/
|
||||
export function renderInteractiveHtmlCodeBlocks(messageElement) {
|
||||
if (!htmlCodeRenderEnabled) {
|
||||
if (!htmlCodeRenderEnabled || htmlCodeRenderSuppressedByExternalRenderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
157
tests/code-render-delegation-contract.test.mjs
Normal file
157
tests/code-render-delegation-contract.test.mjs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
import { installFakeDom } from './helpers/fake-dom.mjs';
|
||||
|
||||
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
async function importFresh(modulePath) {
|
||||
const url = `${pathToFileURL(modulePath).href}?t=${Date.now()}-${Math.random()}`;
|
||||
return import(url);
|
||||
}
|
||||
|
||||
function installJqueryShim() {
|
||||
const previousDollar = Object.getOwnPropertyDescriptor(globalThis, '$');
|
||||
const previousJquery = Object.getOwnPropertyDescriptor(globalThis, 'jQuery');
|
||||
|
||||
const createCollection = (elements) => ({
|
||||
length: elements.length,
|
||||
get(index) {
|
||||
return elements[index];
|
||||
},
|
||||
is(selector) {
|
||||
return Boolean(elements[0]?.matches?.(selector));
|
||||
},
|
||||
closest(selector) {
|
||||
return createCollection(elements.map(el => el?.closest?.(selector)).filter(Boolean));
|
||||
},
|
||||
find(selector) {
|
||||
const found = [];
|
||||
for (const el of elements) {
|
||||
if (!el?.querySelectorAll) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (selector === 'pre > code') {
|
||||
found.push(...el.querySelectorAll('code').filter(code => code.parentElement?.matches('pre')));
|
||||
continue;
|
||||
}
|
||||
|
||||
found.push(...el.querySelectorAll(selector));
|
||||
}
|
||||
return createCollection(found);
|
||||
},
|
||||
});
|
||||
|
||||
const shim = (input) => createCollection(input ? [input] : []);
|
||||
Object.defineProperty(globalThis, '$', { value: shim, configurable: true });
|
||||
Object.defineProperty(globalThis, 'jQuery', { value: shim, configurable: true });
|
||||
|
||||
return () => {
|
||||
if (previousDollar) {
|
||||
Object.defineProperty(globalThis, '$', previousDollar);
|
||||
} else {
|
||||
delete globalThis.$;
|
||||
}
|
||||
|
||||
if (previousJquery) {
|
||||
Object.defineProperty(globalThis, 'jQuery', previousJquery);
|
||||
} else {
|
||||
delete globalThis.jQuery;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function installButtonElementAlias() {
|
||||
const previous = Object.getOwnPropertyDescriptor(globalThis, 'HTMLButtonElement');
|
||||
Object.defineProperty(globalThis, 'HTMLButtonElement', {
|
||||
value: globalThis.HTMLElement,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (previous) {
|
||||
Object.defineProperty(globalThis, 'HTMLButtonElement', previous);
|
||||
} else {
|
||||
delete globalThis.HTMLButtonElement;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createMessageWithFrontendCode() {
|
||||
const message = document.createElement('div');
|
||||
message.classList.add('mes');
|
||||
|
||||
const mesText = document.createElement('div');
|
||||
mesText.classList.add('mes_text');
|
||||
|
||||
const pre = document.createElement('pre');
|
||||
const code = document.createElement('code');
|
||||
code.textContent = '<html><body>preview</body></html>';
|
||||
pre.append(code);
|
||||
mesText.append(pre);
|
||||
message.append(mesText);
|
||||
document.body.append(message);
|
||||
|
||||
return { message, pre };
|
||||
}
|
||||
|
||||
test('html code preview suppression preserves code blocks for delegated renderers', async () => {
|
||||
const dom = installFakeDom();
|
||||
const cleanupJquery = installJqueryShim();
|
||||
const cleanupButtonAlias = installButtonElementAlias();
|
||||
|
||||
try {
|
||||
const {
|
||||
renderInteractiveHtmlCodeBlocks,
|
||||
setHtmlCodeRenderEnabled,
|
||||
setHtmlCodeRenderReplaceLastMessageByDefault,
|
||||
setHtmlCodeRenderSuppressedByExternalRenderer,
|
||||
} = await importFresh(path.join(REPO_ROOT, 'src/scripts/html-code-preview.js'));
|
||||
|
||||
const suppressed = createMessageWithFrontendCode();
|
||||
setHtmlCodeRenderEnabled(true);
|
||||
setHtmlCodeRenderReplaceLastMessageByDefault(false);
|
||||
setHtmlCodeRenderSuppressedByExternalRenderer(true);
|
||||
renderInteractiveHtmlCodeBlocks(suppressed.message);
|
||||
|
||||
assert.equal(suppressed.pre.isConnected, true);
|
||||
assert.equal(suppressed.message.querySelector('.mes-code-preview'), null);
|
||||
|
||||
const fallback = createMessageWithFrontendCode();
|
||||
setHtmlCodeRenderSuppressedByExternalRenderer(false);
|
||||
renderInteractiveHtmlCodeBlocks(fallback.message);
|
||||
|
||||
assert.equal(fallback.pre.isConnected, false);
|
||||
assert.ok(fallback.message.querySelector('.mes-code-preview'));
|
||||
} finally {
|
||||
cleanupButtonAlias();
|
||||
cleanupJquery();
|
||||
dom.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('message render path delegates code-render to known third-party renderers', async () => {
|
||||
const extensionsSource = await readFile(path.join(REPO_ROOT, 'src/scripts/extensions.js'), 'utf8');
|
||||
assert.match(extensionsSource, /'JS-Slash-Runner'/);
|
||||
assert.match(extensionsSource, /'LittleWhiteBox'/);
|
||||
assert.match(extensionsSource, /export function isCodeRenderDelegatedToThirdPartyRenderer\(\)/);
|
||||
assert.match(extensionsSource, /findExtension\(name\)\?\.enabled === true/);
|
||||
|
||||
const scriptSource = await readFile(path.join(REPO_ROOT, 'src/script.js'), 'utf8');
|
||||
const start = scriptSource.indexOf('export function addCopyToCodeBlocks');
|
||||
assert.ok(start >= 0, 'addCopyToCodeBlocks must exist');
|
||||
const end = scriptSource.indexOf('const coordinator = getCodeHighlightCoordinator();', start);
|
||||
assert.ok(end > start, 'addCopyToCodeBlocks setup section must contain code highlight coordinator');
|
||||
const section = scriptSource.slice(start, end);
|
||||
|
||||
const suppressionIndex = section.indexOf('setHtmlCodeRenderSuppressedByExternalRenderer(');
|
||||
const delegationIndex = section.indexOf('isCodeRenderDelegatedToThirdPartyRenderer()', suppressionIndex);
|
||||
const renderIndex = section.indexOf('renderInteractiveHtmlCodeBlocks(messageElement);');
|
||||
assert.ok(suppressionIndex >= 0, 'message render path must sync code-render delegation');
|
||||
assert.ok(delegationIndex > suppressionIndex, 'delegation sync must consult known third-party renderers');
|
||||
assert.ok(renderIndex > suppressionIndex, 'delegation state must be synced before rendering previews');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue