- improvement of marked editor in eveai_chat_client by modernising options approach

- removal of old and obsolete HTML files
- change of package.json to point to a specific version of marked
This commit is contained in:
Josako
2025-10-03 07:59:43 +02:00
parent 7b0e3cee7f
commit 79a3f94ac2
10 changed files with 787 additions and 211 deletions

View File

@@ -18,9 +18,9 @@ export function renderMarkdown(text, options = {}) {
const input = typeof text === 'string' ? text : String(text ?? '');
if (!input) return '';
// Optional sidebar accent [[...]] -> <strong>...</strong> BEFORE parsing
// Optional sidebar accent [[...]] -> keep content but mark with markdown strong (no raw HTML)
const preprocessed = opts.sidebarAccent
? input.replace(/\[\[(.*?)\]\]/g, '<strong>$1</strong>')
? input.replace(/\[\[(.*?)\]\]/g, '**$1**')
: input;
try {
@@ -29,72 +29,82 @@ export function renderMarkdown(text, options = {}) {
if (!hasMarked) {
if (opts.consoleWarnOnFallback) {
console.warn('renderMarkdown: Marked not available; falling back to plain text');
console.log('renderMarkdown: input:', input);
}
return escapeToPlainHtml(preprocessed);
}
// Configure marked options defensively
if (typeof window.marked?.use === 'function') {
// Create a lightweight renderer with restricted features
const renderer = new window.marked.Renderer();
// Use the global singleton API exposed by Marked in browser builds
const md = window.marked;
// Links: target _blank + rel attributes; block unsafe protocols
renderer.link = (href, title, text) => {
const safeHref = sanitizeUrl(href);
if (!safeHref) {
return text; // drop link, keep text
}
const t = opts.linkTargetBlank ? ' target="_blank"' : '';
const rel = ' rel="noopener noreferrer nofollow"';
const titleAttr = title ? ` title="${escapeHtmlAttr(title)}"` : '';
return `<a href="${escapeHtmlAttr(safeHref)}"${t}${rel}${titleAttr}>${text}</a>`;
// One-time registration of our token-based renderer; renderer reads dynamic opts from md.__evieOptions
if (!md.__evieRendererApplied) {
md.use({
renderer: {
link(token) {
const { href, title, text } = token;
const cur = md.__evieOptions || {};
const safeHref = sanitizeUrl(href);
if (!safeHref) return text || '';
const t = cur.linkTargetBlank ? ' target="_blank"' : '';
const rel = ' rel="noopener noreferrer nofollow"';
const titleAttr = title ? ` title="${escapeHtmlAttr(title)}"` : '';
return `<a href="${escapeHtmlAttr(safeHref)}"${t}${rel}${titleAttr}>${text}</a>`;
},
image(token) {
const { href, title, text } = token;
const cur = md.__evieOptions || {};
if (!cur.enableImages) return text ? escapeToPlainHtml(text) : '';
const safeHref = sanitizeUrl(href);
if (!safeHref) return text ? escapeToPlainHtml(text) : '';
const titleAttr = title ? ` title="${escapeHtmlAttr(title)}"` : '';
return `<img src="${escapeHtmlAttr(safeHref)}" alt="${escapeHtmlAttr(text || '')}"${titleAttr} />`;
},
code(token) {
const { text } = token;
// Render as escaped pre/code regardless of highlighting
return `<pre><code>${escapeToPlainHtml(text)}</code></pre>`;
},
codespan(token) {
const { text } = token;
const cur = md.__evieOptions || {};
if (!cur.allowInlineCode) return escapeToPlainHtml(text);
return `<code>${escapeToPlainHtml(text)}</code>`;
},
table(token) {
const cur = md.__evieOptions || {};
if (cur.enableTables) return undefined; // default rendering
const headerCells = token.header?.map(h => h.text).join(' | ') || '';
const rowTexts = (token.rows || []).map(row => row.map(c => c.text).join(' | '));
const all = [headerCells, ...rowTexts].filter(Boolean).join('\n');
return `<p>${escapeToPlainHtml(all)}</p>`;
}
}
});
md.__evieRendererApplied = true;
}
// Set dynamic options per render
md.__evieOptions = {
allowInlineHTML: !!opts.allowInlineHTML,
enableTables: !!opts.enableTables,
enableBreaks: !!opts.enableBreaks,
enableImages: !!opts.enableImages,
enableCodeBlocks: !!opts.enableCodeBlocks,
allowInlineCode: !!opts.allowInlineCode,
linkTargetBlank: !!opts.linkTargetBlank
};
// Images disabled: either drop or render alt text
renderer.image = (href, title, text) => {
if (!opts.enableImages) {
return text ? escapeToPlainHtml(text) : '';
}
// If enabled in future, still sanitize
const safeHref = sanitizeUrl(href);
if (!safeHref) return text ? escapeToPlainHtml(text) : '';
const titleAttr = title ? ` title="${escapeHtmlAttr(title)}"` : '';
return `<img src="${escapeHtmlAttr(safeHref)}" alt="${escapeHtmlAttr(text || '')}"${titleAttr} />`;
};
// Code blocks disabled
renderer.code = (code, infostring) => {
if (!opts.enableCodeBlocks) {
return `<pre><code>${escapeToPlainHtml(code)}</code></pre>`; // still show as plain
}
return `<pre><code>${escapeToPlainHtml(code)}</code></pre>`;
};
// Inline code disabled
renderer.codespan = (code) => {
if (!opts.allowInlineCode) {
return escapeToPlainHtml(code);
}
return `<code>${escapeToPlainHtml(code)}</code>`;
};
// Disallow raw HTML if not allowed
const mOptions = {
md.setOptions({
gfm: true,
breaks: !!opts.enableBreaks,
headerIds: false,
mangle: false,
renderer
};
mangle: false
});
// Table support via GFM is on by default; if disabled, override table renderers to simple paragraphs
if (!opts.enableTables) {
renderer.table = (header, body) => `${header}${body}`;
}
const html = window.marked.parse(preprocessed, mOptions);
// Sanitize output
const html = md.parse(preprocessed);
const sanitized = sanitizeHtml(html);
return sanitized;
}