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 | import {unreachable} from 'devlop'
|
85 | import {toJsxRuntime} from 'hast-util-to-jsx-runtime'
|
86 | import {urlAttributes} from 'html-url-attributes'
|
87 |
|
88 | import {Fragment, jsx, jsxs} from 'react/jsx-runtime'
|
89 | import remarkParse from 'remark-parse'
|
90 | import remarkRehype from 'remark-rehype'
|
91 | import {unified} from 'unified'
|
92 | import {visit} from 'unist-util-visit'
|
93 | import {VFile} from 'vfile'
|
94 |
|
95 | const changelog =
|
96 | 'https://github.com/remarkjs/react-markdown/blob/main/changelog.md'
|
97 |
|
98 |
|
99 | const emptyPlugins = []
|
100 |
|
101 | const emptyRemarkRehypeOptions = {allowDangerousHtml: true}
|
102 | const safeProtocol = /^(https?|ircs?|mailto|xmpp)$/i
|
103 |
|
104 |
|
105 |
|
106 | const deprecations = [
|
107 | {from: 'astPlugins', id: 'remove-buggy-html-in-markdown-parser'},
|
108 | {from: 'allowDangerousHtml', id: 'remove-buggy-html-in-markdown-parser'},
|
109 | {
|
110 | from: 'allowNode',
|
111 | id: 'replace-allownode-allowedtypes-and-disallowedtypes',
|
112 | to: 'allowElement'
|
113 | },
|
114 | {
|
115 | from: 'allowedTypes',
|
116 | id: 'replace-allownode-allowedtypes-and-disallowedtypes',
|
117 | to: 'allowedElements'
|
118 | },
|
119 | {
|
120 | from: 'disallowedTypes',
|
121 | id: 'replace-allownode-allowedtypes-and-disallowedtypes',
|
122 | to: 'disallowedElements'
|
123 | },
|
124 | {from: 'escapeHtml', id: 'remove-buggy-html-in-markdown-parser'},
|
125 | {from: 'includeElementIndex', id: '#remove-includeelementindex'},
|
126 | {
|
127 | from: 'includeNodeIndex',
|
128 | id: 'change-includenodeindex-to-includeelementindex'
|
129 | },
|
130 | {from: 'linkTarget', id: 'remove-linktarget'},
|
131 | {from: 'plugins', id: 'change-plugins-to-remarkplugins', to: 'remarkPlugins'},
|
132 | {from: 'rawSourcePos', id: '#remove-rawsourcepos'},
|
133 | {from: 'renderers', id: 'change-renderers-to-components', to: 'components'},
|
134 | {from: 'source', id: 'change-source-to-children', to: 'children'},
|
135 | {from: 'sourcePos', id: '#remove-sourcepos'},
|
136 | {from: 'transformImageUri', id: '#add-urltransform', to: 'urlTransform'},
|
137 | {from: 'transformLinkUri', id: '#add-urltransform', to: 'urlTransform'}
|
138 | ]
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 | export function Markdown(options) {
|
149 | const allowedElements = options.allowedElements
|
150 | const allowElement = options.allowElement
|
151 | const children = options.children || ''
|
152 | const className = options.className
|
153 | const components = options.components
|
154 | const disallowedElements = options.disallowedElements
|
155 | const rehypePlugins = options.rehypePlugins || emptyPlugins
|
156 | const remarkPlugins = options.remarkPlugins || emptyPlugins
|
157 | const remarkRehypeOptions = options.remarkRehypeOptions
|
158 | ? {...options.remarkRehypeOptions, ...emptyRemarkRehypeOptions}
|
159 | : emptyRemarkRehypeOptions
|
160 | const skipHtml = options.skipHtml
|
161 | const unwrapDisallowed = options.unwrapDisallowed
|
162 | const urlTransform = options.urlTransform || defaultUrlTransform
|
163 |
|
164 | const processor = unified()
|
165 | .use(remarkParse)
|
166 | .use(remarkPlugins)
|
167 | .use(remarkRehype, remarkRehypeOptions)
|
168 | .use(rehypePlugins)
|
169 |
|
170 | const file = new VFile()
|
171 |
|
172 | if (typeof children === 'string') {
|
173 | file.value = children
|
174 | } else {
|
175 | unreachable(
|
176 | 'Unexpected value `' +
|
177 | children +
|
178 | '` for `children` prop, expected `string`'
|
179 | )
|
180 | }
|
181 |
|
182 | if (allowedElements && disallowedElements) {
|
183 | unreachable(
|
184 | 'Unexpected combined `allowedElements` and `disallowedElements`, expected one or the other'
|
185 | )
|
186 | }
|
187 |
|
188 | for (const deprecation of deprecations) {
|
189 | if (Object.hasOwn(options, deprecation.from)) {
|
190 | unreachable(
|
191 | 'Unexpected `' +
|
192 | deprecation.from +
|
193 | '` prop, ' +
|
194 | (deprecation.to
|
195 | ? 'use `' + deprecation.to + '` instead'
|
196 | : 'remove it') +
|
197 | ' (see <' +
|
198 | changelog +
|
199 | '#' +
|
200 | deprecation.id +
|
201 | '> for more info)'
|
202 | )
|
203 | }
|
204 | }
|
205 |
|
206 | const mdastTree = processor.parse(file)
|
207 |
|
208 | let hastTree = processor.runSync(mdastTree, file)
|
209 |
|
210 |
|
211 | if (className) {
|
212 | hastTree = {
|
213 | type: 'element',
|
214 | tagName: 'div',
|
215 | properties: {className},
|
216 |
|
217 | children: (
|
218 | hastTree.type === 'root' ? hastTree.children : [hastTree]
|
219 | )
|
220 | }
|
221 | }
|
222 |
|
223 | visit(hastTree, transform)
|
224 |
|
225 | return toJsxRuntime(hastTree, {
|
226 | Fragment,
|
227 | components,
|
228 | ignoreInvalidStyle: true,
|
229 | jsx,
|
230 | jsxs,
|
231 | passKeys: true,
|
232 | passNode: true
|
233 | })
|
234 |
|
235 |
|
236 | function transform(node, index, parent) {
|
237 | if (node.type === 'raw' && parent && typeof index === 'number') {
|
238 | if (skipHtml) {
|
239 | parent.children.splice(index, 1)
|
240 | } else {
|
241 | parent.children[index] = {type: 'text', value: node.value}
|
242 | }
|
243 |
|
244 | return index
|
245 | }
|
246 |
|
247 | if (node.type === 'element') {
|
248 |
|
249 | let key
|
250 |
|
251 | for (key in urlAttributes) {
|
252 | if (
|
253 | Object.hasOwn(urlAttributes, key) &&
|
254 | Object.hasOwn(node.properties, key)
|
255 | ) {
|
256 | const value = node.properties[key]
|
257 | const test = urlAttributes[key]
|
258 | if (test === null || test.includes(node.tagName)) {
|
259 | node.properties[key] = urlTransform(String(value || ''), key, node)
|
260 | }
|
261 | }
|
262 | }
|
263 | }
|
264 |
|
265 | if (node.type === 'element') {
|
266 | let remove = allowedElements
|
267 | ? !allowedElements.includes(node.tagName)
|
268 | : disallowedElements
|
269 | ? disallowedElements.includes(node.tagName)
|
270 | : false
|
271 |
|
272 | if (!remove && allowElement && typeof index === 'number') {
|
273 | remove = !allowElement(node, index, parent)
|
274 | }
|
275 |
|
276 | if (remove && parent && typeof index === 'number') {
|
277 | if (unwrapDisallowed && node.children) {
|
278 | parent.children.splice(index, 1, ...node.children)
|
279 | } else {
|
280 | parent.children.splice(index, 1)
|
281 | }
|
282 |
|
283 | return index
|
284 | }
|
285 | }
|
286 | }
|
287 | }
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 | export function defaultUrlTransform(value) {
|
299 |
|
300 |
|
301 |
|
302 | const colon = value.indexOf(':')
|
303 | const questionMark = value.indexOf('?')
|
304 | const numberSign = value.indexOf('#')
|
305 | const slash = value.indexOf('/')
|
306 |
|
307 | if (
|
308 |
|
309 | colon < 0 ||
|
310 |
|
311 | (slash > -1 && colon > slash) ||
|
312 | (questionMark > -1 && colon > questionMark) ||
|
313 | (numberSign > -1 && colon > numberSign) ||
|
314 |
|
315 | safeProtocol.test(value.slice(0, colon))
|
316 | ) {
|
317 | return value
|
318 | }
|
319 |
|
320 | return ''
|
321 | }
|