feat(web): implement robust inline message formatting parser
All checks were successful
CI / test (push) Successful in 25s
All checks were successful
CI / test (push) Successful in 25s
This commit is contained in:
@@ -9,25 +9,104 @@ function escapeHtml(input: string): string {
|
||||
|
||||
function sanitizeHref(input: string): string {
|
||||
const normalized = input.trim();
|
||||
if (/^https?:\/\//i.test(normalized)) {
|
||||
if (/^(https?:\/\/|mailto:)/i.test(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
return "#";
|
||||
}
|
||||
|
||||
export function formatMessageHtml(text: string): string {
|
||||
let html = escapeHtml(text);
|
||||
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, label: string, href: string) => {
|
||||
const safeHref = sanitizeHref(href);
|
||||
return `<a href="${safeHref}" target="_blank" rel="noreferrer" class="underline text-sky-300">${label}</a>`;
|
||||
});
|
||||
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
||||
html = html.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
||||
html = html.replace(/__([^_]+)__/g, "<u>$1</u>");
|
||||
html = html.replace(/~~([^~]+)~~/g, "<s>$1</s>");
|
||||
html = html.replace(/`([^`]+)`/g, "<code class=\"rounded bg-slate-700/60 px-1 py-0.5 text-[12px]\">$1</code>");
|
||||
html = html.replace(/\|\|([^|]+)\|\|/g, "<span class=\"rounded bg-slate-700/80 px-1 text-transparent hover:text-inherit\">$1</span>");
|
||||
html = html.replace(/\n/g, "<br/>");
|
||||
return html;
|
||||
function renderLink(labelHtml: string, href: string): string {
|
||||
const safeHref = sanitizeHref(href);
|
||||
return `<a href="${safeHref}" target="_blank" rel="noreferrer" class="underline text-sky-300">${labelHtml}</a>`;
|
||||
}
|
||||
|
||||
function findClosing(text: string, marker: string, start: number): number {
|
||||
let index = text.indexOf(marker, start);
|
||||
while (index !== -1) {
|
||||
if (index > start) {
|
||||
return index;
|
||||
}
|
||||
index = text.indexOf(marker, index + marker.length);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function parseInline(text: string): string {
|
||||
let i = 0;
|
||||
let out = "";
|
||||
|
||||
while (i < text.length) {
|
||||
if (text[i] === "\n") {
|
||||
out += "<br/>";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const mdLink = text.slice(i).match(/^\[([^\]]+)\]\(([^)]+)\)/);
|
||||
if (mdLink) {
|
||||
const [, label, href] = mdLink;
|
||||
out += renderLink(parseInline(label), href);
|
||||
i += mdLink[0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
const bareUrl = text.slice(i).match(/^https?:\/\/[^\s<>()]+/i);
|
||||
if (bareUrl) {
|
||||
const raw = bareUrl[0];
|
||||
const trimmed = raw.replace(/[),.;!?]+$/g, "");
|
||||
const trailing = raw.slice(trimmed.length);
|
||||
out += renderLink(escapeHtml(trimmed), trimmed);
|
||||
out += escapeHtml(trailing);
|
||||
i += raw.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (text.startsWith("`", i)) {
|
||||
const end = findClosing(text, "`", i + 1);
|
||||
if (end !== -1) {
|
||||
const value = escapeHtml(text.slice(i + 1, end));
|
||||
out += `<code class="rounded bg-slate-700/60 px-1 py-0.5 text-[12px]">${value}</code>`;
|
||||
i = end + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const tokenDefs: Array<{ marker: string; open: string; close: string }> = [
|
||||
{ marker: "||", open: '<span class="rounded bg-slate-700/80 px-1 text-transparent hover:text-inherit">', close: "</span>" },
|
||||
{ marker: "**", open: "<strong>", close: "</strong>" },
|
||||
{ marker: "__", open: "<u>", close: "</u>" },
|
||||
{ marker: "~~", open: "<s>", close: "</s>" },
|
||||
{ marker: "*", open: "<em>", close: "</em>" },
|
||||
];
|
||||
let matched = false;
|
||||
for (const def of tokenDefs) {
|
||||
if (!text.startsWith(def.marker, i)) {
|
||||
continue;
|
||||
}
|
||||
const end = findClosing(text, def.marker, i + def.marker.length);
|
||||
if (end === -1) {
|
||||
continue;
|
||||
}
|
||||
const value = text.slice(i + def.marker.length, end);
|
||||
if (!value.trim()) {
|
||||
continue;
|
||||
}
|
||||
out += `${def.open}${parseInline(value)}${def.close}`;
|
||||
i = end + def.marker.length;
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
if (matched) {
|
||||
continue;
|
||||
}
|
||||
|
||||
out += escapeHtml(text[i]);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
export function formatMessageHtml(text: string): string {
|
||||
return parseInline(text);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user