react-markdown-canvas 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ast-to-react.js +451 -0
- package/lib/react-markdown.js +184 -0
- package/lib/rehype-filter.js +66 -0
- package/lib/uri-transformer.js +45 -0
- package/package.json +14 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @template T
|
|
3
|
+
* @typedef {import('react').ComponentType<T>} ComponentType<T>
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @template {import('react').ElementType} T
|
|
8
|
+
* @typedef {import('react').ComponentPropsWithoutRef<T>} ComponentPropsWithoutRef<T>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import('react').ReactNode} ReactNode
|
|
13
|
+
* @typedef {import('unist').Position} Position
|
|
14
|
+
* @typedef {import('hast').Element} Element
|
|
15
|
+
* @typedef {import('hast').ElementContent} ElementContent
|
|
16
|
+
* @typedef {import('hast').Root} Root
|
|
17
|
+
* @typedef {import('hast').Text} Text
|
|
18
|
+
* @typedef {import('hast').Comment} Comment
|
|
19
|
+
* @typedef {import('hast').DocType} Doctype
|
|
20
|
+
* @typedef {import('property-information').Info} Info
|
|
21
|
+
* @typedef {import('property-information').Schema} Schema
|
|
22
|
+
* @typedef {import('./complex-types.js').ReactMarkdownProps} ReactMarkdownProps
|
|
23
|
+
*
|
|
24
|
+
* @typedef Raw
|
|
25
|
+
* @property {'raw'} type
|
|
26
|
+
* @property {string} value
|
|
27
|
+
*
|
|
28
|
+
* @typedef Context
|
|
29
|
+
* @property {Options} options
|
|
30
|
+
* @property {Schema} schema
|
|
31
|
+
* @property {number} listDepth
|
|
32
|
+
*
|
|
33
|
+
* @callback TransformLink
|
|
34
|
+
* @param {string} href
|
|
35
|
+
* @param {Array<ElementContent>} children
|
|
36
|
+
* @param {string?} title
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*
|
|
39
|
+
* @callback TransformImage
|
|
40
|
+
* @param {string} src
|
|
41
|
+
* @param {string} alt
|
|
42
|
+
* @param {string?} title
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*
|
|
45
|
+
* @typedef {import('react').HTMLAttributeAnchorTarget} TransformLinkTargetType
|
|
46
|
+
*
|
|
47
|
+
* @callback TransformLinkTarget
|
|
48
|
+
* @param {string} href
|
|
49
|
+
* @param {Array<ElementContent>} children
|
|
50
|
+
* @param {string?} title
|
|
51
|
+
* @returns {TransformLinkTargetType|undefined}
|
|
52
|
+
*
|
|
53
|
+
* @typedef {keyof JSX.IntrinsicElements} ReactMarkdownNames
|
|
54
|
+
*
|
|
55
|
+
* To do: is `data-sourcepos` typeable?
|
|
56
|
+
*
|
|
57
|
+
* @typedef {ComponentPropsWithoutRef<'code'> & ReactMarkdownProps & {inline?: boolean}} CodeProps
|
|
58
|
+
* @typedef {ComponentPropsWithoutRef<'h1'> & ReactMarkdownProps & {level: number}} HeadingProps
|
|
59
|
+
* @typedef {ComponentPropsWithoutRef<'li'> & ReactMarkdownProps & {checked: boolean|null, index: number, ordered: boolean}} LiProps
|
|
60
|
+
* @typedef {ComponentPropsWithoutRef<'ol'> & ReactMarkdownProps & {depth: number, ordered: true}} OrderedListProps
|
|
61
|
+
* @typedef {ComponentPropsWithoutRef<'td'> & ReactMarkdownProps & {style?: Record<string, unknown>, isHeader: false}} TableDataCellProps
|
|
62
|
+
* @typedef {ComponentPropsWithoutRef<'th'> & ReactMarkdownProps & {style?: Record<string, unknown>, isHeader: true}} TableHeaderCellProps
|
|
63
|
+
* @typedef {ComponentPropsWithoutRef<'tr'> & ReactMarkdownProps & {isHeader: boolean}} TableRowProps
|
|
64
|
+
* @typedef {ComponentPropsWithoutRef<'ul'> & ReactMarkdownProps & {depth: number, ordered: false}} UnorderedListProps
|
|
65
|
+
*
|
|
66
|
+
* @typedef {ComponentType<CodeProps>} CodeComponent
|
|
67
|
+
* @typedef {ComponentType<HeadingProps>} HeadingComponent
|
|
68
|
+
* @typedef {ComponentType<LiProps>} LiComponent
|
|
69
|
+
* @typedef {ComponentType<OrderedListProps>} OrderedListComponent
|
|
70
|
+
* @typedef {ComponentType<TableDataCellProps>} TableDataCellComponent
|
|
71
|
+
* @typedef {ComponentType<TableHeaderCellProps>} TableHeaderCellComponent
|
|
72
|
+
* @typedef {ComponentType<TableRowProps>} TableRowComponent
|
|
73
|
+
* @typedef {ComponentType<UnorderedListProps>} UnorderedListComponent
|
|
74
|
+
*
|
|
75
|
+
* @typedef SpecialComponents
|
|
76
|
+
* @property {CodeComponent|ReactMarkdownNames} code
|
|
77
|
+
* @property {HeadingComponent|ReactMarkdownNames} h1
|
|
78
|
+
* @property {HeadingComponent|ReactMarkdownNames} h2
|
|
79
|
+
* @property {HeadingComponent|ReactMarkdownNames} h3
|
|
80
|
+
* @property {HeadingComponent|ReactMarkdownNames} h4
|
|
81
|
+
* @property {HeadingComponent|ReactMarkdownNames} h5
|
|
82
|
+
* @property {HeadingComponent|ReactMarkdownNames} h6
|
|
83
|
+
* @property {LiComponent|ReactMarkdownNames} li
|
|
84
|
+
* @property {OrderedListComponent|ReactMarkdownNames} ol
|
|
85
|
+
* @property {TableDataCellComponent|ReactMarkdownNames} td
|
|
86
|
+
* @property {TableHeaderCellComponent|ReactMarkdownNames} th
|
|
87
|
+
* @property {TableRowComponent|ReactMarkdownNames} tr
|
|
88
|
+
* @property {UnorderedListComponent|ReactMarkdownNames} ul
|
|
89
|
+
*
|
|
90
|
+
* @typedef {Partial<Omit<import('./complex-types.js').NormalComponents, keyof SpecialComponents> & SpecialComponents>} Components
|
|
91
|
+
*
|
|
92
|
+
* @typedef Options
|
|
93
|
+
* @property {boolean} [sourcePos=false]
|
|
94
|
+
* @property {boolean} [rawSourcePos=false]
|
|
95
|
+
* @property {boolean} [skipHtml=false]
|
|
96
|
+
* @property {boolean} [includeElementIndex=false]
|
|
97
|
+
* @property {null|false|TransformLink} [transformLinkUri]
|
|
98
|
+
* @property {TransformImage} [transformImageUri]
|
|
99
|
+
* @property {TransformLinkTargetType|TransformLinkTarget} [linkTarget]
|
|
100
|
+
* @property {Components} [components]
|
|
101
|
+
*/
|
|
102
|
+
|
|
103
|
+
import React from 'react'
|
|
104
|
+
import ReactIs from 'react-is'
|
|
105
|
+
import {whitespace} from 'hast-util-whitespace'
|
|
106
|
+
import {svg, find, hastToReact} from 'property-information'
|
|
107
|
+
import {stringify as spaces} from 'space-separated-tokens'
|
|
108
|
+
import {stringify as commas} from 'comma-separated-tokens'
|
|
109
|
+
import style from 'style-to-object'
|
|
110
|
+
import {uriTransformer} from './uri-transformer.js'
|
|
111
|
+
|
|
112
|
+
const own = {}.hasOwnProperty
|
|
113
|
+
|
|
114
|
+
// The table-related elements that must not contain whitespace text according
|
|
115
|
+
// to React.
|
|
116
|
+
const tableElements = new Set(['table', 'thead', 'tbody', 'tfoot', 'tr'])
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {Context} context
|
|
120
|
+
* @param {Element|Root} node
|
|
121
|
+
*/
|
|
122
|
+
export function childrenToReact(context, node) {
|
|
123
|
+
/** @type {Array<ReactNode>} */
|
|
124
|
+
const children = []
|
|
125
|
+
let childIndex = -1
|
|
126
|
+
/** @type {Comment|Doctype|Element|Raw|Text} */
|
|
127
|
+
let child
|
|
128
|
+
|
|
129
|
+
while (++childIndex < node.children.length) {
|
|
130
|
+
child = node.children[childIndex]
|
|
131
|
+
|
|
132
|
+
if (child.type === 'element') {
|
|
133
|
+
children.push(toReact(context, child, childIndex, node))
|
|
134
|
+
} else if (child.type === 'text') {
|
|
135
|
+
// Currently, a warning is triggered by react for *any* white space in
|
|
136
|
+
// tables.
|
|
137
|
+
// So we drop it.
|
|
138
|
+
// See: <https://github.com/facebook/react/pull/7081>.
|
|
139
|
+
// See: <https://github.com/facebook/react/pull/7515>.
|
|
140
|
+
// See: <https://github.com/remarkjs/remark-react/issues/64>.
|
|
141
|
+
// See: <https://github.com/remarkjs/react-markdown/issues/576>.
|
|
142
|
+
if (
|
|
143
|
+
node.type !== 'element' ||
|
|
144
|
+
!tableElements.has(node.tagName) ||
|
|
145
|
+
!whitespace(child)
|
|
146
|
+
) {
|
|
147
|
+
children.push(child.value)
|
|
148
|
+
}
|
|
149
|
+
} else if (child.type === 'raw' && !context.options.skipHtml) {
|
|
150
|
+
// Default behavior is to show (encoded) HTML.
|
|
151
|
+
children.push(child.value)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return children
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {Context} context
|
|
160
|
+
* @param {Element} node
|
|
161
|
+
* @param {number} index
|
|
162
|
+
* @param {Element|Root} parent
|
|
163
|
+
*/
|
|
164
|
+
function toReact(context, node, index, parent) {
|
|
165
|
+
const options = context.options
|
|
166
|
+
const transform =
|
|
167
|
+
options.transformLinkUri === undefined
|
|
168
|
+
? uriTransformer
|
|
169
|
+
: options.transformLinkUri
|
|
170
|
+
const parentSchema = context.schema
|
|
171
|
+
/** @type {ReactMarkdownNames} */
|
|
172
|
+
// @ts-expect-error assume a known HTML/SVG element.
|
|
173
|
+
const name = node.tagName
|
|
174
|
+
/** @type {Record<string, unknown>} */
|
|
175
|
+
const properties = {}
|
|
176
|
+
let schema = parentSchema
|
|
177
|
+
/** @type {string} */
|
|
178
|
+
let property
|
|
179
|
+
|
|
180
|
+
if (parentSchema.space === 'html' && name === 'svg') {
|
|
181
|
+
schema = svg
|
|
182
|
+
context.schema = schema
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (node.properties) {
|
|
186
|
+
for (property in node.properties) {
|
|
187
|
+
if (own.call(node.properties, property)) {
|
|
188
|
+
addProperty(properties, property, node.properties[property], context)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (name === 'ol' || name === 'ul') {
|
|
194
|
+
context.listDepth++
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const children = childrenToReact(context, node)
|
|
198
|
+
|
|
199
|
+
if (name === 'ol' || name === 'ul') {
|
|
200
|
+
context.listDepth--
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Restore parent schema.
|
|
204
|
+
context.schema = parentSchema
|
|
205
|
+
|
|
206
|
+
// Nodes created by plugins do not have positional info, in which case we use
|
|
207
|
+
// an object that matches the position interface.
|
|
208
|
+
const position = node.position || {
|
|
209
|
+
start: {line: null, column: null, offset: null},
|
|
210
|
+
end: {line: null, column: null, offset: null}
|
|
211
|
+
}
|
|
212
|
+
const component =
|
|
213
|
+
options.components && own.call(options.components, name)
|
|
214
|
+
? options.components[name]
|
|
215
|
+
: name
|
|
216
|
+
const basic = typeof component === 'string' || component === React.Fragment
|
|
217
|
+
|
|
218
|
+
if (!ReactIs.isValidElementType(component)) {
|
|
219
|
+
throw new TypeError(
|
|
220
|
+
`Component for name \`${name}\` not defined or is not renderable`
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
properties.key = index
|
|
225
|
+
|
|
226
|
+
if (name === 'a' && options.linkTarget) {
|
|
227
|
+
properties.target =
|
|
228
|
+
typeof options.linkTarget === 'function'
|
|
229
|
+
? options.linkTarget(
|
|
230
|
+
String(properties.href || ''),
|
|
231
|
+
node.children,
|
|
232
|
+
typeof properties.title === 'string' ? properties.title : null
|
|
233
|
+
)
|
|
234
|
+
: options.linkTarget
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (name === 'a' && transform) {
|
|
238
|
+
properties.href = transform(
|
|
239
|
+
String(properties.href || ''),
|
|
240
|
+
node.children,
|
|
241
|
+
typeof properties.title === 'string' ? properties.title : null
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (
|
|
246
|
+
!basic &&
|
|
247
|
+
name === 'code' &&
|
|
248
|
+
parent.type === 'element' &&
|
|
249
|
+
parent.tagName !== 'pre'
|
|
250
|
+
) {
|
|
251
|
+
properties.inline = true
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (
|
|
255
|
+
!basic &&
|
|
256
|
+
(name === 'h1' ||
|
|
257
|
+
name === 'h2' ||
|
|
258
|
+
name === 'h3' ||
|
|
259
|
+
name === 'h4' ||
|
|
260
|
+
name === 'h5' ||
|
|
261
|
+
name === 'h6')
|
|
262
|
+
) {
|
|
263
|
+
properties.level = Number.parseInt(name.charAt(1), 10)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (name === 'img' && options.transformImageUri) {
|
|
267
|
+
properties.src = options.transformImageUri(
|
|
268
|
+
String(properties.src || ''),
|
|
269
|
+
String(properties.alt || ''),
|
|
270
|
+
typeof properties.title === 'string' ? properties.title : null
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!basic && name === 'li' && parent.type === 'element') {
|
|
275
|
+
const input = getInputElement(node)
|
|
276
|
+
properties.checked =
|
|
277
|
+
input && input.properties ? Boolean(input.properties.checked) : null
|
|
278
|
+
properties.index = getElementsBeforeCount(parent, node)
|
|
279
|
+
properties.ordered = parent.tagName === 'ol'
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!basic && (name === 'ol' || name === 'ul')) {
|
|
283
|
+
properties.ordered = name === 'ol'
|
|
284
|
+
properties.depth = context.listDepth
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (name === 'td' || name === 'th') {
|
|
288
|
+
if (properties.align) {
|
|
289
|
+
if (!properties.style) properties.style = {}
|
|
290
|
+
// @ts-expect-error assume `style` is an object
|
|
291
|
+
properties.style.textAlign = properties.align
|
|
292
|
+
delete properties.align
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!basic) {
|
|
296
|
+
properties.isHeader = name === 'th'
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!basic && name === 'tr' && parent.type === 'element') {
|
|
301
|
+
properties.isHeader = Boolean(parent.tagName === 'thead')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// If `sourcePos` is given, pass source information (line/column info from markdown source).
|
|
305
|
+
if (options.sourcePos) {
|
|
306
|
+
properties['data-sourcepos'] = flattenPosition(position)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!basic && options.rawSourcePos) {
|
|
310
|
+
properties.sourcePosition = node.position
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// If `includeElementIndex` is given, pass node index info to components.
|
|
314
|
+
if (!basic && options.includeElementIndex) {
|
|
315
|
+
properties.index = getElementsBeforeCount(parent, node)
|
|
316
|
+
properties.siblingCount = getElementsBeforeCount(parent)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (!basic) {
|
|
320
|
+
properties.node = node
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Ensure no React warnings are emitted for void elements w/ children.
|
|
324
|
+
return children.length > 0
|
|
325
|
+
? React.createElement(component, properties, children)
|
|
326
|
+
: React.createElement(component, properties)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @param {Element|Root} node
|
|
331
|
+
* @returns {Element?}
|
|
332
|
+
*/
|
|
333
|
+
function getInputElement(node) {
|
|
334
|
+
let index = -1
|
|
335
|
+
|
|
336
|
+
while (++index < node.children.length) {
|
|
337
|
+
const child = node.children[index]
|
|
338
|
+
|
|
339
|
+
if (child.type === 'element' && child.tagName === 'input') {
|
|
340
|
+
return child
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return null
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* @param {Element|Root} parent
|
|
349
|
+
* @param {Element} [node]
|
|
350
|
+
* @returns {number}
|
|
351
|
+
*/
|
|
352
|
+
function getElementsBeforeCount(parent, node) {
|
|
353
|
+
let index = -1
|
|
354
|
+
let count = 0
|
|
355
|
+
|
|
356
|
+
while (++index < parent.children.length) {
|
|
357
|
+
if (parent.children[index] === node) break
|
|
358
|
+
if (parent.children[index].type === 'element') count++
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return count
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* @param {Record<string, unknown>} props
|
|
366
|
+
* @param {string} prop
|
|
367
|
+
* @param {unknown} value
|
|
368
|
+
* @param {Context} ctx
|
|
369
|
+
*/
|
|
370
|
+
function addProperty(props, prop, value, ctx) {
|
|
371
|
+
const info = find(ctx.schema, prop)
|
|
372
|
+
let result = value
|
|
373
|
+
|
|
374
|
+
// Ignore nullish and `NaN` values.
|
|
375
|
+
// eslint-disable-next-line no-self-compare
|
|
376
|
+
if (result === null || result === undefined || result !== result) {
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Accept `array`.
|
|
381
|
+
// Most props are space-separated.
|
|
382
|
+
if (Array.isArray(result)) {
|
|
383
|
+
result = info.commaSeparated ? commas(result) : spaces(result)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (info.property === 'style' && typeof result === 'string') {
|
|
387
|
+
result = parseStyle(result)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (info.space && info.property) {
|
|
391
|
+
props[
|
|
392
|
+
own.call(hastToReact, info.property)
|
|
393
|
+
? hastToReact[info.property]
|
|
394
|
+
: info.property
|
|
395
|
+
] = result
|
|
396
|
+
} else if (info.attribute) {
|
|
397
|
+
props[info.attribute] = result
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* @param {string} value
|
|
403
|
+
* @returns {Record<string, string>}
|
|
404
|
+
*/
|
|
405
|
+
function parseStyle(value) {
|
|
406
|
+
/** @type {Record<string, string>} */
|
|
407
|
+
const result = {}
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
style(value, iterator)
|
|
411
|
+
} catch {
|
|
412
|
+
// Silent.
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return result
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* @param {string} name
|
|
419
|
+
* @param {string} v
|
|
420
|
+
*/
|
|
421
|
+
function iterator(name, v) {
|
|
422
|
+
const k = name.slice(0, 4) === '-ms-' ? `ms-${name.slice(4)}` : name
|
|
423
|
+
result[k.replace(/-([a-z])/g, styleReplacer)] = v
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* @param {unknown} _
|
|
429
|
+
* @param {string} $1
|
|
430
|
+
*/
|
|
431
|
+
function styleReplacer(_, $1) {
|
|
432
|
+
return $1.toUpperCase()
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* @param {Position|{start: {line: null, column: null, offset: null}, end: {line: null, column: null, offset: null}}} pos
|
|
437
|
+
* @returns {string}
|
|
438
|
+
*/
|
|
439
|
+
function flattenPosition(pos) {
|
|
440
|
+
return [
|
|
441
|
+
pos.start.line,
|
|
442
|
+
':',
|
|
443
|
+
pos.start.column,
|
|
444
|
+
'-',
|
|
445
|
+
pos.end.line,
|
|
446
|
+
':',
|
|
447
|
+
pos.end.column
|
|
448
|
+
]
|
|
449
|
+
.map(String)
|
|
450
|
+
.join('')
|
|
451
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('react').ReactNode} ReactNode
|
|
3
|
+
* @typedef {import('react').ReactElement<{}>} ReactElement
|
|
4
|
+
* @typedef {import('unified').PluggableList} PluggableList
|
|
5
|
+
* @typedef {import('hast').Root} Root
|
|
6
|
+
* @typedef {import('./rehype-filter.js').Options} FilterOptions
|
|
7
|
+
* @typedef {import('./ast-to-react.js').Options} TransformOptions
|
|
8
|
+
*
|
|
9
|
+
* @typedef CoreOptions
|
|
10
|
+
* @property {string} children
|
|
11
|
+
*
|
|
12
|
+
* @typedef PluginOptions
|
|
13
|
+
* @property {PluggableList} [remarkPlugins=[]]
|
|
14
|
+
* @property {PluggableList} [rehypePlugins=[]]
|
|
15
|
+
* @property {import('remark-rehype').Options | undefined} [remarkRehypeOptions={}]
|
|
16
|
+
*
|
|
17
|
+
* @typedef LayoutOptions
|
|
18
|
+
* @property {string} [className]
|
|
19
|
+
*
|
|
20
|
+
* @typedef {CoreOptions & PluginOptions & LayoutOptions & FilterOptions & TransformOptions} ReactMarkdownOptions
|
|
21
|
+
*
|
|
22
|
+
* @typedef Deprecation
|
|
23
|
+
* @property {string} id
|
|
24
|
+
* @property {string} [to]
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import React from 'react'
|
|
28
|
+
import {VFile} from 'vfile'
|
|
29
|
+
import {unified} from 'unified'
|
|
30
|
+
import remarkParse from 'remark-parse'
|
|
31
|
+
import remarkRehype from 'remark-rehype'
|
|
32
|
+
import PropTypes from 'prop-types'
|
|
33
|
+
import {html} from 'property-information'
|
|
34
|
+
import rehypeFilter from './rehype-filter.js'
|
|
35
|
+
import {childrenToReact} from './ast-to-react.js'
|
|
36
|
+
|
|
37
|
+
const own = {}.hasOwnProperty
|
|
38
|
+
const changelog =
|
|
39
|
+
'https://github.com/remarkjs/react-markdown/blob/main/changelog.md'
|
|
40
|
+
|
|
41
|
+
/** @type {Record<string, Deprecation>} */
|
|
42
|
+
const deprecated = {
|
|
43
|
+
plugins: {to: 'remarkPlugins', id: 'change-plugins-to-remarkplugins'},
|
|
44
|
+
renderers: {to: 'components', id: 'change-renderers-to-components'},
|
|
45
|
+
astPlugins: {id: 'remove-buggy-html-in-markdown-parser'},
|
|
46
|
+
allowDangerousHtml: {id: 'remove-buggy-html-in-markdown-parser'},
|
|
47
|
+
escapeHtml: {id: 'remove-buggy-html-in-markdown-parser'},
|
|
48
|
+
source: {to: 'children', id: 'change-source-to-children'},
|
|
49
|
+
allowNode: {
|
|
50
|
+
to: 'allowElement',
|
|
51
|
+
id: 'replace-allownode-allowedtypes-and-disallowedtypes'
|
|
52
|
+
},
|
|
53
|
+
allowedTypes: {
|
|
54
|
+
to: 'allowedElements',
|
|
55
|
+
id: 'replace-allownode-allowedtypes-and-disallowedtypes'
|
|
56
|
+
},
|
|
57
|
+
disallowedTypes: {
|
|
58
|
+
to: 'disallowedElements',
|
|
59
|
+
id: 'replace-allownode-allowedtypes-and-disallowedtypes'
|
|
60
|
+
},
|
|
61
|
+
includeNodeIndex: {
|
|
62
|
+
to: 'includeElementIndex',
|
|
63
|
+
id: 'change-includenodeindex-to-includeelementindex'
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* React component to render markdown.
|
|
69
|
+
*
|
|
70
|
+
* @param {ReactMarkdownOptions} options
|
|
71
|
+
* @returns {ReactElement}
|
|
72
|
+
*/
|
|
73
|
+
export function ReactMarkdown(options) {
|
|
74
|
+
for (const key in deprecated) {
|
|
75
|
+
if (own.call(deprecated, key) && own.call(options, key)) {
|
|
76
|
+
const deprecation = deprecated[key]
|
|
77
|
+
console.warn(
|
|
78
|
+
`[react-markdown] Warning: please ${
|
|
79
|
+
deprecation.to ? `use \`${deprecation.to}\` instead of` : 'remove'
|
|
80
|
+
} \`${key}\` (see <${changelog}#${deprecation.id}> for more info)`
|
|
81
|
+
)
|
|
82
|
+
delete deprecated[key]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const processor = unified()
|
|
87
|
+
.use(remarkParse)
|
|
88
|
+
.use(options.remarkPlugins || [])
|
|
89
|
+
.use(remarkRehype, {
|
|
90
|
+
...options.remarkRehypeOptions,
|
|
91
|
+
allowDangerousHtml: true
|
|
92
|
+
})
|
|
93
|
+
.use(options.rehypePlugins || [])
|
|
94
|
+
.use(rehypeFilter, options)
|
|
95
|
+
|
|
96
|
+
const file = new VFile()
|
|
97
|
+
|
|
98
|
+
if (typeof options.children === 'string') {
|
|
99
|
+
file.value = options.children
|
|
100
|
+
} else if (options.children !== undefined && options.children !== null) {
|
|
101
|
+
console.warn(
|
|
102
|
+
`[react-markdown] Warning: please pass a string as \`children\` (not: \`${options.children}\`)`
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const hastNode = processor.runSync(processor.parse(file), file)
|
|
107
|
+
|
|
108
|
+
if (hastNode.type !== 'root') {
|
|
109
|
+
throw new TypeError('Expected a `root` node')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @type {ReactElement} */
|
|
113
|
+
let result = React.createElement(
|
|
114
|
+
React.Fragment,
|
|
115
|
+
{},
|
|
116
|
+
childrenToReact({options, schema: html, listDepth: 0}, hastNode)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if (options.className) {
|
|
120
|
+
result = React.createElement('div', {className: options.className}, result)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
ReactMarkdown.propTypes = {
|
|
127
|
+
// Core options:
|
|
128
|
+
children: PropTypes.string,
|
|
129
|
+
// Layout options:
|
|
130
|
+
className: PropTypes.string,
|
|
131
|
+
// Filter options:
|
|
132
|
+
allowElement: PropTypes.func,
|
|
133
|
+
allowedElements: PropTypes.arrayOf(PropTypes.string),
|
|
134
|
+
disallowedElements: PropTypes.arrayOf(PropTypes.string),
|
|
135
|
+
unwrapDisallowed: PropTypes.bool,
|
|
136
|
+
// Plugin options:
|
|
137
|
+
remarkPlugins: PropTypes.arrayOf(
|
|
138
|
+
PropTypes.oneOfType([
|
|
139
|
+
PropTypes.object,
|
|
140
|
+
PropTypes.func,
|
|
141
|
+
PropTypes.arrayOf(
|
|
142
|
+
PropTypes.oneOfType([
|
|
143
|
+
PropTypes.bool,
|
|
144
|
+
PropTypes.string,
|
|
145
|
+
PropTypes.object,
|
|
146
|
+
PropTypes.func,
|
|
147
|
+
PropTypes.arrayOf(
|
|
148
|
+
// prettier-ignore
|
|
149
|
+
// type-coverage:ignore-next-line
|
|
150
|
+
PropTypes.any
|
|
151
|
+
)
|
|
152
|
+
])
|
|
153
|
+
)
|
|
154
|
+
])
|
|
155
|
+
),
|
|
156
|
+
rehypePlugins: PropTypes.arrayOf(
|
|
157
|
+
PropTypes.oneOfType([
|
|
158
|
+
PropTypes.object,
|
|
159
|
+
PropTypes.func,
|
|
160
|
+
PropTypes.arrayOf(
|
|
161
|
+
PropTypes.oneOfType([
|
|
162
|
+
PropTypes.bool,
|
|
163
|
+
PropTypes.string,
|
|
164
|
+
PropTypes.object,
|
|
165
|
+
PropTypes.func,
|
|
166
|
+
PropTypes.arrayOf(
|
|
167
|
+
// prettier-ignore
|
|
168
|
+
// type-coverage:ignore-next-line
|
|
169
|
+
PropTypes.any
|
|
170
|
+
)
|
|
171
|
+
])
|
|
172
|
+
)
|
|
173
|
+
])
|
|
174
|
+
),
|
|
175
|
+
// Transform options:
|
|
176
|
+
sourcePos: PropTypes.bool,
|
|
177
|
+
rawSourcePos: PropTypes.bool,
|
|
178
|
+
skipHtml: PropTypes.bool,
|
|
179
|
+
includeElementIndex: PropTypes.bool,
|
|
180
|
+
transformLinkUri: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
|
|
181
|
+
linkTarget: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
|
|
182
|
+
transformImageUri: PropTypes.func,
|
|
183
|
+
components: PropTypes.object
|
|
184
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {visit} from 'unist-util-visit'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('unist').Node} Node
|
|
5
|
+
* @typedef {import('hast').Root} Root
|
|
6
|
+
* @typedef {import('hast').Element} Element
|
|
7
|
+
*
|
|
8
|
+
* @callback AllowElement
|
|
9
|
+
* @param {Element} element
|
|
10
|
+
* @param {number} index
|
|
11
|
+
* @param {Element|Root} parent
|
|
12
|
+
* @returns {boolean|undefined}
|
|
13
|
+
*
|
|
14
|
+
* @typedef Options
|
|
15
|
+
* @property {Array<string>} [allowedElements]
|
|
16
|
+
* @property {Array<string>} [disallowedElements=[]]
|
|
17
|
+
* @property {AllowElement} [allowElement]
|
|
18
|
+
* @property {boolean} [unwrapDisallowed=false]
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @type {import('unified').Plugin<[Options], Root>}
|
|
23
|
+
*/
|
|
24
|
+
export default function rehypeFilter(options) {
|
|
25
|
+
if (options.allowedElements && options.disallowedElements) {
|
|
26
|
+
throw new TypeError(
|
|
27
|
+
'Only one of `allowedElements` and `disallowedElements` should be defined'
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
options.allowedElements ||
|
|
33
|
+
options.disallowedElements ||
|
|
34
|
+
options.allowElement
|
|
35
|
+
) {
|
|
36
|
+
return (tree) => {
|
|
37
|
+
visit(tree, 'element', (node, index, parent_) => {
|
|
38
|
+
const parent = /** @type {Element|Root} */ (parent_)
|
|
39
|
+
/** @type {boolean|undefined} */
|
|
40
|
+
let remove
|
|
41
|
+
|
|
42
|
+
if (options.allowedElements) {
|
|
43
|
+
remove = !options.allowedElements.includes(node.tagName)
|
|
44
|
+
} else if (options.disallowedElements) {
|
|
45
|
+
remove = options.disallowedElements.includes(node.tagName)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!remove && options.allowElement && typeof index === 'number') {
|
|
49
|
+
remove = !options.allowElement(node, index, parent)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (remove && typeof index === 'number') {
|
|
53
|
+
if (options.unwrapDisallowed && node.children) {
|
|
54
|
+
parent.children.splice(index, 1, ...node.children)
|
|
55
|
+
} else {
|
|
56
|
+
parent.children.splice(index, 1)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return index
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return undefined
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const protocols = ['http', 'https', 'mailto', 'tel']
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} uri
|
|
5
|
+
* @returns {string}
|
|
6
|
+
*/
|
|
7
|
+
export function uriTransformer(uri) {
|
|
8
|
+
const url = (uri || '').trim()
|
|
9
|
+
const first = url.charAt(0)
|
|
10
|
+
|
|
11
|
+
if (first === '#' || first === '/') {
|
|
12
|
+
return url
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const colon = url.indexOf(':')
|
|
16
|
+
if (colon === -1) {
|
|
17
|
+
return url
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let index = -1
|
|
21
|
+
|
|
22
|
+
while (++index < protocols.length) {
|
|
23
|
+
const protocol = protocols[index]
|
|
24
|
+
|
|
25
|
+
if (
|
|
26
|
+
colon === protocol.length &&
|
|
27
|
+
url.slice(0, protocol.length).toLowerCase() === protocol
|
|
28
|
+
) {
|
|
29
|
+
return url
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
index = url.indexOf('?')
|
|
34
|
+
if (index !== -1 && colon > index) {
|
|
35
|
+
return url
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
index = url.indexOf('#')
|
|
39
|
+
if (index !== -1 && colon > index) {
|
|
40
|
+
return url
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// eslint-disable-next-line no-script-url
|
|
44
|
+
return 'javascript:void(0)'
|
|
45
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-markdown-canvas",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "react-markdown-canvas",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"directories": {
|
|
7
|
+
"lib": "lib"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"author": "busf4ctor",
|
|
13
|
+
"license": "ISC"
|
|
14
|
+
}
|