Files
blogflare/src/lib/markdown/rendering.ts
T

104 lines
2.6 KiB
TypeScript

import sanitizeHtml from 'sanitize-html';
type AdmonitionRenderer = (type: string, title: string, content: string) => string;
export function replaceGithubAdmonitions(markdown: string, render: AdmonitionRenderer): string {
const pattern =
/(?:^|\r?\n)> ?\[!(NOTE|ABSTRACT|INFO|TIP|SUCCESS|QUESTION|WARNING|FAILURE|DANGER|BUG|EXAMPLE|QUOTE|IMPORTANT|CAUTION)\][ \t]*(.*?)\r?\n((?:>.*(?:\r?\n|$))*)/gi;
return markdown.replace(pattern, (_match, type: string, title: string, content: string) =>
render(type, title, content.replace(/^> ?/gm, '').trimEnd())
);
}
export function createHeadingId(rawText: string): string {
const slug = rawText
.replace(/<[^>]*>?/g, '')
.normalize('NFKC')
.trim()
.toLocaleLowerCase()
.replace(/\s+/g, '-')
.replace(/[^\p{L}\p{N}_-]+/gu, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
return slug || 'section';
}
export function createUniqueHeadingId(rawText: string, seen: Map<string, number>): string {
const base = createHeadingId(rawText);
const count = seen.get(base) ?? 0;
seen.set(base, count + 1);
return count === 0 ? base : `${base}-${count + 1}`;
}
export function sanitizeMarkdownHtml(html: string): string {
return sanitizeHtml(html, {
allowedTags: [
'p',
'br',
'strong',
'em',
'del',
'blockquote',
'ul',
'ol',
'li',
'hr',
'a',
'img',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'table',
'thead',
'tbody',
'tr',
'th',
'td',
'figure',
'figcaption',
'time',
'svg',
'path',
'button',
'span',
'pre',
'code',
'div'
],
allowedAttributes: {
'*': ['class', 'id', 'title', 'role', 'aria-hidden'],
a: ['href', 'target', 'rel', 'data-fancybox', 'data-caption'],
img: ['src', 'alt', 'title', 'loading', 'width', 'height'],
button: ['type', 'title', 'data-code', 'aria-hidden'],
svg: ['viewBox', 'viewbox', 'fill', 'width', 'height', 'xmlns', 'focusable'],
path: ['d', 'fill'],
time: ['datetime']
},
allowedSchemes: ['http', 'https', 'mailto', 'tel'],
allowedSchemesByTag: {
img: ['http', 'https']
},
allowProtocolRelative: false,
enforceHtmlBoundary: true,
parser: {
lowerCaseAttributeNames: false
},
transformTags: {
a: (tagName, attribs) => {
if (attribs['data-fancybox'] || (attribs.href && attribs.href.startsWith('#'))) {
return { tagName, attribs };
}
const rel = new Set((attribs.rel || '').split(/\s+/).filter(Boolean));
rel.add('noopener');
rel.add('noreferrer');
return { tagName, attribs: { ...attribs, target: '_blank', rel: [...rel].join(' ') } };
}
}
});
}