- static/style.css: add td code / th code rules (font-size 0.85em, padding 1px 4px, vertical-align baseline) for both .msg-body and .preview-md to fix cramped inline code in table cells (#486) - static/ui.js inlineMd(): add image pass ( → <img class=msg-media-img>) running while _code_stash is active (protects image syntax inside backticks), add _img_stash (\x00G) to shield rendered <img> src= from autolink, add img to SAFE_INLINE (#487) - static/ui.js renderMd() outer: add image pass before outer link pass for images in plain paragraphs, add img to SAFE_TAGS allowlist (#487) - tests/test_issue486_487.py: 45 new tests covering CSS source checks, JS source structure, rendering behaviour, and combination edge cases (code + image + link in same table cell, image inside code span, etc.) Closes #486, closes #487
This commit is contained in:
@@ -536,7 +536,7 @@
|
||||
<div class="settings-section-title">System</div>
|
||||
<div class="settings-section-meta">Instance version and access controls.</div>
|
||||
</div>
|
||||
<span class="settings-version-badge">v0.50.43</span>
|
||||
<span class="settings-version-badge">v0.50.44</span>
|
||||
</div>
|
||||
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
|
||||
<label for="settingsPassword" data-i18n="settings_label_password">Access Password</label>
|
||||
|
||||
@@ -430,6 +430,8 @@
|
||||
.msg-body th{background:rgba(255,255,255,.07);padding:6px 10px;text-align:left;font-weight:600;border:1px solid var(--border2);}
|
||||
.msg-body td{padding:5px 10px;border:1px solid rgba(255,255,255,.06);}
|
||||
.msg-body tr:nth-child(even){background:rgba(255,255,255,.03);}
|
||||
/* #486: inline code inside table cells needs scaled sizing to avoid overflow/clipping */
|
||||
.msg-body td code,.msg-body th code{font-size:0.85em;padding:1px 4px;vertical-align:baseline;}
|
||||
/* KaTeX math rendering */
|
||||
.katex-block{display:block;text-align:center;margin:12px 0;overflow-x:auto;}
|
||||
.katex-inline{display:inline;}
|
||||
@@ -589,6 +591,8 @@
|
||||
.preview-md th{background:rgba(255,255,255,.07);padding:6px 10px;text-align:left;font-weight:600;border:1px solid var(--border2);}
|
||||
.preview-md td{padding:5px 10px;border:1px solid rgba(255,255,255,.06);}
|
||||
.preview-md tr:nth-child(even){background:rgba(255,255,255,.03);}
|
||||
/* #486: inline code inside table cells needs scaled sizing to avoid overflow/clipping */
|
||||
.preview-md td code,.preview-md th code{font-size:0.85em;padding:1px 4px;vertical-align:baseline;}
|
||||
/* File type badge in preview path bar */
|
||||
.preview-badge{display:inline-block;font-size:10px;font-weight:600;padding:2px 6px;border-radius:4px;margin-left:8px;text-transform:uppercase;letter-spacing:.06em;}
|
||||
.preview-badge.img{background:rgba(124,185,255,.15);color:var(--blue);}
|
||||
|
||||
19
static/ui.js
19
static/ui.js
@@ -465,15 +465,24 @@ function renderMd(raw){
|
||||
t=t.replace(/\*\*\*(.+?)\*\*\*/g,(_,x)=>`<strong><em>${esc(x)}</em></strong>`);
|
||||
t=t.replace(/\*\*(.+?)\*\*/g,(_,x)=>`<strong>${esc(x)}</strong>`);
|
||||
t=t.replace(/\*([^*\n]+)\*/g,(_,x)=>`<em>${esc(x)}</em>`);
|
||||
// #487: Image pass — runs while code stash is active so  inside
|
||||
// backticks stays protected as a \x00C token and is never rendered as <img>.
|
||||
// Must run before _code_stash restore and before _link_stash so the image
|
||||
// is not consumed by the [label](url) link regex.
|
||||
t=t.replace(/!\[([^\]]*)\]\((https?:\/\/[^\)]+)\)/g,(_,alt,url)=>`<img src="${url.replace(/"/g,'%22')}" alt="${esc(alt)}" class="msg-media-img" loading="lazy" onclick="this.classList.toggle('msg-media-img--full')">`);
|
||||
// Stash rendered <img> tags so autolink never matches URLs inside src=
|
||||
const _img_stash=[];
|
||||
t=t.replace(/(<img\b[^>]*>)/g,m=>{_img_stash.push(m);return `\x00G${_img_stash.length-1}\x00`;});
|
||||
t=t.replace(/\x00C(\d+)\x00/g,(_,i)=>_code_stash[+i]);
|
||||
// Stash [label](url) links before autolink so the URL in href= is not re-linked
|
||||
const _link_stash=[];
|
||||
t=t.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g,(_,lb,u)=>{_link_stash.push(`<a href="${u.replace(/"/g,'%22')}" target="_blank" rel="noopener">${esc(lb)}</a>`);return `\x00L${_link_stash.length-1}\x00`;});
|
||||
t=t.replace(/(https?:\/\/[^\s<>"')\]]+)/g,(url)=>{const trail=url.match(/[.,;:!?)]$/)?url.slice(-1):'';const clean=trail?url.slice(0,-1):url;return `<a href="${clean}" target="_blank" rel="noopener">${esc(clean)}</a>${trail}`;});
|
||||
t=t.replace(/\x00L(\d+)\x00/g,(_,i)=>_link_stash[+i]);
|
||||
t=t.replace(/\x00G(\d+)\x00/g,(_,i)=>_img_stash[+i]);
|
||||
// Escape any plain text that isn't already wrapped in a tag we produced
|
||||
// by escaping bare < > that aren't part of our own tags
|
||||
const SAFE_INLINE=/^<\/?(strong|em|code|a)([\s>]|$)/i;
|
||||
// by escaping bare < > that are not part of our own tags
|
||||
const SAFE_INLINE=/^<\/?(strong|em|code|a|img)([\s>]|$)/i;
|
||||
t=t.replace(/<\/?[a-z][^>]*>/gi,tag=>SAFE_INLINE.test(tag)?tag:esc(tag));
|
||||
return t;
|
||||
}
|
||||
@@ -523,6 +532,10 @@ function renderMd(raw){
|
||||
const body=rows.slice(2).map(r=>`<tr>${parseRow(r)}</tr>`).join('');
|
||||
return `<table><thead>${header}</thead><tbody>${body}</tbody></table>`;
|
||||
});
|
||||
// #487: Outer image pass — handles  in plain paragraphs (outside tables/lists).
|
||||
// Runs AFTER the table pass (images in table cells are handled by inlineMd() above).
|
||||
// Runs BEFORE the outer [label](url) link pass so the image is not consumed as a plain link.
|
||||
s=s.replace(/!\[([^\]]*)\]\((https?:\/\/[^\)]+)\)/g,(_,alt,url)=>`<img src="${url.replace(/"/g,'%22')}" alt="${esc(alt)}" class="msg-media-img" loading="lazy" onclick="this.classList.toggle('msg-media-img--full')">`);
|
||||
// Outer link pass for labeled links in plain paragraphs (outside table cells).
|
||||
// Runs AFTER the table pass so table cells are processed by inlineMd() only.
|
||||
// Stash existing <a> tags first to avoid re-linking already-linked URLs.
|
||||
@@ -534,7 +547,7 @@ function renderMd(raw){
|
||||
// Our pipeline only emits: <strong>,<em>,<code>,<pre>,<h1-6>,<ul>,<ol>,<li>,
|
||||
// <table>,<thead>,<tbody>,<tr>,<th>,<td>,<hr>,<blockquote>,<p>,<br>,<a>,
|
||||
// <div class="..."> (mermaid/pre-header). Everything else is untrusted input.
|
||||
const SAFE_TAGS=/^<\/?(strong|em|code|pre|h[1-6]|ul|ol|li|table|thead|tbody|tr|th|td|hr|blockquote|p|br|a|div|span)([\s>]|$)/i;
|
||||
const SAFE_TAGS=/^<\/?(strong|em|code|pre|h[1-6]|ul|ol|li|table|thead|tbody|tr|th|td|hr|blockquote|p|br|a|img|div|span)([\s>]|$)/i;
|
||||
s=s.replace(/<\/?[a-z][^>]*>/gi,tag=>SAFE_TAGS.test(tag)?tag:esc(tag));
|
||||
// Autolink: convert plain URLs to clickable links.
|
||||
// Stash existing <a> tags first so we never re-link a URL already inside href="...".
|
||||
|
||||
Reference in New Issue
Block a user