aboutsummaryrefslogtreecommitdiff
path: root/scripts/apidocs/utils/markdown.ts
blob: 2787a611bf47e1f0e6a755453bececfba39ca390 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import sanitizeHtml from 'sanitize-html';
import type { MarkdownRenderer } from 'vitepress';
import { createMarkdownRenderer } from 'vitepress';
import vitepressConfig from '../../../docs/.vitepress/config';
import { FILE_PATH_API_DOCS } from './paths';

let markdown: MarkdownRenderer;

export async function initMarkdownRenderer(): Promise<void> {
  markdown = await createMarkdownRenderer(
    FILE_PATH_API_DOCS,
    vitepressConfig.markdown,
    '/'
  );
}

const htmlSanitizeOptions: sanitizeHtml.IOptions = {
  allowedTags: [
    'a',
    'button',
    'code',
    'div',
    'li',
    'p',
    'pre',
    'span',
    'strong',
    'ul',
  ],
  allowedAttributes: {
    a: ['href', 'target', 'rel'],
    button: ['class', 'title'],
    div: ['class'],
    pre: ['class', 'v-pre', 'tabindex'],
    span: ['class', 'style'],
  },
  selfClosing: [],
};

function comparableSanitizedHtml(html: string): string {
  return html
    .replaceAll(/&#x[0-9A-F]{2};/g, (x) =>
      String.fromCodePoint(Number.parseInt(x.slice(3, -1), 16))
    )
    .replaceAll('&gt;', '>')
    .replaceAll('&lt;', '<')
    .replaceAll('&amp;', '&')
    .replaceAll('=""', '')
    .replaceAll(' ', '');
}

/**
 * Converts a Typescript code block to an HTML string and sanitizes it.
 *
 * @param code The code to convert.
 *
 * @returns The converted HTML string.
 */
export function codeToHtml(code: string): string {
  const delimiter = '```';
  return mdToHtml(`${delimiter}ts\n${code}\n${delimiter}`);
}

/**
 * Converts Markdown to an HTML string and sanitizes it.
 *
 * @param md The markdown to convert.
 * @param inline Whether to render the markdown as inline, without a wrapping `<p>` tag. Defaults to `false`.
 *
 * @returns The converted HTML string.
 */
export function mdToHtml(md: string, inline?: boolean): string;
/**
 * Converts Markdown to an HTML string and sanitizes it.
 *
 * @param md The markdown to convert.
 * @param inline Whether to render the markdown as inline, without a wrapping `<p>` tag. Defaults to `false`.
 *
 * @returns The converted HTML string.
 */
export function mdToHtml(
  md: string | undefined,
  inline?: boolean
): string | undefined;
export function mdToHtml(
  md: string | undefined,
  inline: boolean = false
): string | undefined {
  if (md == null) {
    return undefined;
  }

  const rawHtml = inline ? markdown.renderInline(md) : markdown.render(md);

  const safeHtml: string = sanitizeHtml(rawHtml, htmlSanitizeOptions);
  // Revert some escaped characters for comparison.
  if (comparableSanitizedHtml(rawHtml) === comparableSanitizedHtml(safeHtml)) {
    return adjustUrls(safeHtml);
  }

  console.debug('Rejected unsafe md:\n', md);
  console.error('Rejected unsafe html:\n', rawHtml);
  console.error('Clean unsafe html:\n', comparableSanitizedHtml(rawHtml));
  console.error('Clean safe html:\n', comparableSanitizedHtml(safeHtml));
  console.log('-'.repeat(80));
  throw new Error('Found unsafe html');
}

export function adjustUrls(description: string): string {
  return description.replaceAll(/https:\/\/(next.)?fakerjs.dev\//g, '/');
}