104 lines
2.6 KiB
TypeScript
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(' ') } };
|
|
}
|
|
}
|
|
});
|
|
}
|