@dfinity/gix-components
Version:
A UI kit developed by the GIX team
99 lines (98 loc) • 4.49 kB
JavaScript
import { isNullish } from "@dfinity/utils";
// eslint-disable-next-line local-rules/prefer-object-params
export const targetBlankLinkRenderer = (href, title, text) => `<a${href === null || href === undefined
? ""
: ` target="_blank" rel="noopener noreferrer" href="${href}"`}${title === null || title === undefined ? "" : ` title="${title}"`}>${text.length === 0 ? (href ?? title) : text}</a>`;
/**
* Based on https://github.com/markedjs/marked/blob/master/src/Renderer.js#L186
* @returns <a> tag to image
*/
// eslint-disable-next-line local-rules/prefer-object-params
export const imageToLinkRenderer = (src, title, alt) => {
if (src === undefined || src === null || src?.length === 0) {
return alt;
}
const fileExtention = src.includes(".")
? src.split(".").pop()
: "";
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-type
const typeProp = fileExtention === "" ? undefined : ` type="image/${fileExtention}"`;
const titleDefined = title !== undefined && title !== null;
const titleProp = titleDefined ? ` title="${title}"` : undefined;
const text = alt === "" ? (titleDefined ? title : src) : alt;
return `<a href="${src}" target="_blank" rel="noopener noreferrer"${typeProp ?? ""}${titleProp ?? ""}>${text}</a>`;
};
const escapeHtml = (html) => html.replace(/</g, "<").replace(/>/g, ">");
const escapeSvgs = (html) => {
// Early exit if no SVGs to process
if (!/<svg\b[^>]*>/i.test(html)) {
return html;
}
// Early exit if no code blocks - just escape all SVGs
if (!/```[\s\S]*?```|`[^`\n]+`/g.test(html)) {
return html.replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, escapeHtml);
}
// Find all code blocks (both inline and fenced) and their positions
const codeBlocks = [];
// Match fenced code blocks (```...```)
const fencedCodeRegex = /```[\s\S]*?```/g;
let match;
while ((match = fencedCodeRegex.exec(html)) !== null) {
codeBlocks.push({ start: match.index, end: match.index + match[0].length });
}
// Match inline code (`...`)
const inlineCodeRegex = /`[^`\n]+`/g;
while ((match = inlineCodeRegex.exec(html)) !== null) {
codeBlocks.push({ start: match.index, end: match.index + match[0].length });
}
// Helper function to check if a position is inside any code block
const isInsideCodeBlock = (position) => codeBlocks.some((block) => position >= block.start && position < block.end);
// Replace SVGs that are NOT inside code blocks
return html.replace(/<svg[^>]*>[\s\S]*?<\/svg>/gi, (svgMatch, offset) => isInsideCodeBlock(offset) ? svgMatch : escapeHtml(svgMatch));
};
/**
* Escape <img> tags or convert them to links
*/
const transformImg = (img) => {
const src = img.match(/src="([^"]+)"/)?.[1];
const alt = img.match(/alt="([^"]+)"/)?.[1] ?? "img";
const title = img.match(/title="([^"]+)"/)?.[1];
const shouldEscape = isNullish(src) || src.startsWith("data:image");
const imageHtml = shouldEscape
? escapeHtml(img)
: imageToLinkRenderer(src, title, alt);
return imageHtml;
};
/** Avoid <img> tags; instead, apply the same logic as for markdown images by either escaping them or converting them to links. */
export const htmlRenderer = (html) => /<img\s+[^>]*>/gi.test(html) ? transformImg(html) : html;
/**
* Marked.js renderer for proposal summary.
* Customized renderers
* - targetBlankLinkRenderer
* - imageToLinkRenderer
* - htmlRenderer
*
* @param marked
*/
const proposalSummaryRenderer = (marked) => {
const renderer = new marked.Renderer();
renderer.link = targetBlankLinkRenderer;
renderer.image = imageToLinkRenderer;
renderer.html = htmlRenderer;
return renderer;
};
/**
* Uses markedjs.
* Escape or transform to links some raw HTML tags (img, svg)
* @see {@link https://github.com/markedjs/marked}
*/
export const markdownToHTML = async (text) => {
// Replace the SVG elements in the HTML with their escaped versions to improve security.
// It's not possible to do it with html renderer because the svg consists of multiple tags.
const escapedText = escapeSvgs(text);
// The dynamic import cannot be analyzed by Vite. As it is intended, we use the /* @vite-ignore */ comment inside the import() call to suppress this warning.
const { marked } = await import("marked");
return marked(escapedText, {
renderer: proposalSummaryRenderer(marked),
});
};