safe-mdx 0.0.6 → 0.2.0

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/src/safe-mdx.tsx CHANGED
@@ -3,6 +3,9 @@ import { htmlToJsx } from 'html-to-jsx-transform'
3
3
  import { Node, Parent, RootContent } from 'mdast'
4
4
  import remarkFrontmatter from 'remark-frontmatter'
5
5
 
6
+ import { collapseWhiteSpace } from 'collapse-white-space'
7
+ import { visit } from 'unist-util-visit'
8
+
6
9
  import { Root, Yaml } from 'mdast'
7
10
  import { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx-jsx'
8
11
  import { remark } from 'remark'
@@ -10,34 +13,45 @@ import remarkGfm from 'remark-gfm'
10
13
  import remarkMdx from 'remark-mdx'
11
14
 
12
15
  import { Fragment, ReactNode } from 'react'
13
- import { remarkMarkAndUnravel } from './plugins.js'
16
+ import { completeJsxTags } from './streaming.js'
14
17
 
15
18
  type MyRootContent = RootContent | Root
16
19
 
17
- const processor = remark()
18
- .use(remarkMdx)
19
- .use(remarkMarkAndUnravel)
20
- .use(remarkFrontmatter, ['yaml', 'toml'])
21
- .use(remarkGfm)
22
- .use(() => {
23
- return (tree, file) => {
24
- file.data.ast = tree
25
- }
26
- })
27
-
28
20
  export function mdxParse(code: string) {
29
- const file = processor.processSync(code)
21
+ const file = mdxProcessor.processSync(code)
30
22
  return file.data.ast as Root
31
23
  }
32
24
 
33
- void React
25
+ declare module 'mdast' {
26
+ export interface Data {
27
+ hProperties?: {
28
+ id?: string
29
+ }
30
+ }
31
+ }
32
+
33
+ export type CustomTransformer = (
34
+ node: MyRootContent,
35
+ transform: (node: MyRootContent) => ReactNode,
36
+ ) => ReactNode | undefined
34
37
 
35
38
  export function SafeMdxRenderer({
36
39
  components,
37
40
  code = '',
38
41
  mdast = null as any,
42
+ customTransformer,
43
+ }: {
44
+ components?: ComponentsMap
45
+ code?: string
46
+ mdast?: MyRootContent
47
+ customTransformer?: CustomTransformer
39
48
  }) {
40
- const visitor = new MdastToJsx({ code, mdast, components })
49
+ const visitor = new MdastToJsx({
50
+ code,
51
+ mdast,
52
+ components,
53
+ customTransformer,
54
+ })
41
55
  const result = visitor.run()
42
56
  return result
43
57
  }
@@ -48,13 +62,25 @@ export class MdastToJsx {
48
62
  jsxStr: string = ''
49
63
  c: ComponentsMap
50
64
  errors: { message: string }[] = []
65
+ customTransformer?: CustomTransformer
66
+
51
67
  constructor({
52
68
  code = '',
53
69
  mdast = undefined as any,
54
70
  components = {} as ComponentsMap,
71
+ customTransformer,
72
+ }: {
73
+ code?: string
74
+ mdast?: MyRootContent
75
+ components?: ComponentsMap
76
+ customTransformer?: (
77
+ node: MyRootContent,
78
+ transform: (node: MyRootContent) => ReactNode,
79
+ ) => ReactNode | undefined
55
80
  }) {
56
81
  this.str = code
57
82
  this.mdast = mdast || mdxParse(code)
83
+ this.customTransformer = customTransformer
58
84
 
59
85
  this.c = {
60
86
  ...Object.fromEntries(
@@ -150,6 +176,17 @@ export class MdastToJsx {
150
176
  return []
151
177
  }
152
178
 
179
+ // Check for custom transformer first, giving it higher priority
180
+ if (this.customTransformer) {
181
+ const customResult = this.customTransformer(
182
+ node,
183
+ this.mdastTransformer.bind(this),
184
+ )
185
+ if (customResult !== undefined) {
186
+ return customResult
187
+ }
188
+ }
189
+
153
190
  switch (node.type) {
154
191
  case 'mdxjsEsm': {
155
192
  const start = node.position?.start?.offset
@@ -192,16 +229,24 @@ export class MdastToJsx {
192
229
  }
193
230
  case 'heading': {
194
231
  const level = node.depth
195
-
196
232
  const Tag = this.c[`h${level}`] ?? `h${level}`
197
- return <Tag>{this.mapMdastChildren(node)}</Tag>
233
+
234
+ return (
235
+ <Tag {...node.data?.hProperties}>
236
+ {this.mapMdastChildren(node)}
237
+ </Tag>
238
+ )
198
239
  }
199
240
  case 'paragraph': {
200
- return <this.c.p>{this.mapMdastChildren(node)}</this.c.p>
241
+ return (
242
+ <this.c.p {...node.data?.hProperties}>
243
+ {this.mapMdastChildren(node)}
244
+ </this.c.p>
245
+ )
201
246
  }
202
247
  case 'blockquote': {
203
248
  return (
204
- <this.c.blockquote>
249
+ <this.c.blockquote {...node.data?.hProperties}>
205
250
  {this.mapMdastChildren(node)}
206
251
  </this.c.blockquote>
207
252
  )
@@ -216,7 +261,7 @@ export class MdastToJsx {
216
261
  const language = node.lang || ''
217
262
  const code = node.value
218
263
  const codeBlock = (className?: string) => (
219
- <this.c.pre>
264
+ <this.c.pre {...node.data?.hProperties}>
220
265
  <this.c.code className={className}>{code}</this.c.code>
221
266
  </this.c.pre>
222
267
  )
@@ -241,23 +286,37 @@ export class MdastToJsx {
241
286
  case 'list': {
242
287
  if (node.ordered) {
243
288
  return (
244
- <this.c.ol start={node.start!}>
289
+ <this.c.ol
290
+ start={node.start!}
291
+ {...node.data?.hProperties}
292
+ >
245
293
  {this.mapMdastChildren(node)}
246
294
  </this.c.ol>
247
295
  )
248
296
  }
249
- return <this.c.ul>{this.mapMdastChildren(node)}</this.c.ul>
297
+ return (
298
+ <this.c.ul {...node.data?.hProperties}>
299
+ {this.mapMdastChildren(node)}
300
+ </this.c.ul>
301
+ )
250
302
  }
251
303
  case 'listItem': {
252
304
  // https://github.com/syntax-tree/mdast-util-gfm-task-list-item#syntax-tree
253
305
  if (node?.checked != null) {
254
306
  return (
255
- <this.c.li data-checked={node.checked}>
307
+ <this.c.li
308
+ data-checked={node.checked}
309
+ {...node.data?.hProperties}
310
+ >
256
311
  {this.mapMdastChildren(node)}
257
312
  </this.c.li>
258
313
  )
259
314
  }
260
- return <this.c.li>{this.mapMdastChildren(node)}</this.c.li>
315
+ return (
316
+ <this.c.li {...node.data?.hProperties}>
317
+ {this.mapMdastChildren(node)}
318
+ </this.c.li>
319
+ )
261
320
  }
262
321
  case 'text': {
263
322
  if (!node.value) {
@@ -269,33 +328,54 @@ export class MdastToJsx {
269
328
  const src = node.url || ''
270
329
  const alt = node.alt || ''
271
330
  const title = node.title || ''
272
- return <this.c.img src={src} alt={alt} title={title} />
331
+ return (
332
+ <this.c.img
333
+ src={src}
334
+ alt={alt}
335
+ title={title}
336
+ {...node.data?.hProperties}
337
+ />
338
+ )
273
339
  }
274
340
  case 'link': {
275
341
  const href = node.url || ''
276
342
  const title = node.title || ''
277
343
  return (
278
- <this.c.a {...{ href, title }}>
344
+ <this.c.a {...{ href, title }} {...node.data?.hProperties}>
279
345
  {this.mapMdastChildren(node)}
280
346
  </this.c.a>
281
347
  )
282
348
  }
283
349
  case 'strong': {
284
350
  return (
285
- <this.c.strong>{this.mapMdastChildren(node)}</this.c.strong>
351
+ <this.c.strong {...node.data?.hProperties}>
352
+ {this.mapMdastChildren(node)}
353
+ </this.c.strong>
286
354
  )
287
355
  }
288
356
  case 'emphasis': {
289
- return <this.c.em>{this.mapMdastChildren(node)}</this.c.em>
357
+ return (
358
+ <this.c.em {...node.data?.hProperties}>
359
+ {this.mapMdastChildren(node)}
360
+ </this.c.em>
361
+ )
290
362
  }
291
363
  case 'delete': {
292
- return <this.c.del>{this.mapMdastChildren(node)}</this.c.del>
364
+ return (
365
+ <this.c.del {...node.data?.hProperties}>
366
+ {this.mapMdastChildren(node)}
367
+ </this.c.del>
368
+ )
293
369
  }
294
370
  case 'inlineCode': {
295
371
  if (!node.value) {
296
372
  return []
297
373
  }
298
- return <this.c.code>{node.value}</this.c.code>
374
+ return (
375
+ <this.c.code {...node.data?.hProperties}>
376
+ {node.value}
377
+ </this.c.code>
378
+ )
299
379
  }
300
380
  case 'break': {
301
381
  return <this.c.br />
@@ -308,7 +388,7 @@ export class MdastToJsx {
308
388
  this.mapMdastChildren(node),
309
389
  )
310
390
  return (
311
- <this.c.table>
391
+ <this.c.table {...node.data?.hProperties}>
312
392
  {head && <this.c.thead>{head}</this.c.thead>}
313
393
  {!!body?.length && <this.c.tbody>{body}</this.c.tbody>}
314
394
  </this.c.table>
@@ -316,15 +396,18 @@ export class MdastToJsx {
316
396
  }
317
397
  case 'tableRow': {
318
398
  return (
319
- <this.c.tr className=''>
399
+ <this.c.tr className='' {...node.data?.hProperties}>
320
400
  {this.mapMdastChildren(node)}
321
401
  </this.c.tr>
322
402
  )
323
403
  }
324
404
  case 'tableCell': {
325
405
  let content = this.mapMdastChildren(node)
326
-
327
- return <this.c.td className=''>{content}</this.c.td>
406
+ return (
407
+ <this.c.td className='' {...node.data?.hProperties}>
408
+ {content}
409
+ </this.c.td>
410
+ )
328
411
  }
329
412
  case 'definition': {
330
413
  return []
@@ -341,7 +424,7 @@ export class MdastToJsx {
341
424
  })
342
425
 
343
426
  return (
344
- <this.c.a href={href}>
427
+ <this.c.a href={href} {...node.data?.hProperties}>
345
428
  {this.mapMdastChildren(node)}
346
429
  </this.c.a>
347
430
  )
@@ -403,10 +486,9 @@ export function getJsxAttrs(
403
486
  .map((attr) => {
404
487
  if (attr.type === 'mdxJsxExpressionAttribute') {
405
488
  onError({
406
- message: `Expressions in jsx props are not supported (${attr.value.replace(
407
- /\n+/g,
408
- ' ',
409
- )})`,
489
+ message: `Expressions in jsx props are not supported (${attr.value
490
+ .replace(/\n+/g, ' ')
491
+ .replace(/ +/g, ' ')})`,
410
492
  })
411
493
  return
412
494
  }
@@ -553,6 +635,22 @@ const nativeTags = [
553
635
  'video',
554
636
  'code',
555
637
  'pre',
638
+ 'figure',
639
+ 'canvas',
640
+ 'details',
641
+ 'dl',
642
+ 'dt',
643
+ 'dd',
644
+ 'fieldset',
645
+ 'footer',
646
+ 'header',
647
+ 'legend',
648
+ 'main',
649
+ 'mark',
650
+ 'nav',
651
+ 'progress',
652
+ 'summary',
653
+ 'time',
556
654
  ] as const
557
655
 
558
656
  const supportedLanguages = [
@@ -836,4 +934,100 @@ const supportedLanguages = [
836
934
  ] as const
837
935
  const supportedLanguagesSet = new Set(supportedLanguages)
838
936
 
839
- type ComponentsMap = { [k in (typeof nativeTags)[number]]?: any }
937
+ type ComponentsMap = { [k in (typeof nativeTags)[number]]?: any } & {
938
+ [key: string]: any
939
+ }
940
+
941
+ /**
942
+ * https://github.com/mdx-js/mdx/blob/b3351fadcb6f78833a72757b7135dcfb8ab646fe/packages/mdx/lib/plugin/remark-mark-and-unravel.js
943
+ * A tiny plugin that unravels `<p><h1>x</h1></p>` but also
944
+ * `<p><Component /></p>` (so it has no knowledge of "HTML").
945
+ *
946
+ * It also marks JSX as being explicitly JSX, so when a user passes a `h1`
947
+ * component, it is used for `# heading` but not for `<h1>heading</h1>`.
948
+ *
949
+ */
950
+ export function remarkMarkAndUnravel() {
951
+ return function (tree: Root) {
952
+ visit(tree, function (node, index, parent) {
953
+ let offset = -1
954
+ let all = true
955
+ let oneOrMore = false
956
+
957
+ if (
958
+ parent &&
959
+ typeof index === 'number' &&
960
+ node.type === 'paragraph'
961
+ ) {
962
+ const children = node.children
963
+
964
+ while (++offset < children.length) {
965
+ const child = children[offset]
966
+
967
+ if (
968
+ child.type === 'mdxJsxTextElement' ||
969
+ child.type === 'mdxTextExpression'
970
+ ) {
971
+ oneOrMore = true
972
+ } else if (
973
+ child.type === 'text' &&
974
+ collapseWhiteSpace(child.value, {
975
+ style: 'html',
976
+ trim: true,
977
+ }) === ''
978
+ ) {
979
+ // Empty.
980
+ } else {
981
+ all = false
982
+ break
983
+ }
984
+ }
985
+
986
+ if (all && oneOrMore) {
987
+ offset = -1
988
+
989
+ const newChildren: RootContent[] = []
990
+
991
+ while (++offset < children.length) {
992
+ const child = children[offset]
993
+
994
+ if (child.type === 'mdxJsxTextElement') {
995
+ // @ts-expect-error: mutate because it is faster; content model is fine.
996
+ child.type = 'mdxJsxFlowElement'
997
+ }
998
+
999
+ if (child.type === 'mdxTextExpression') {
1000
+ // @ts-expect-error: mutate because it is faster; content model is fine.
1001
+ child.type = 'mdxFlowExpression'
1002
+ }
1003
+
1004
+ if (
1005
+ child.type === 'text' &&
1006
+ /^[\t\r\n ]+$/.test(String(child.value))
1007
+ ) {
1008
+ // Empty.
1009
+ } else {
1010
+ newChildren.push(child)
1011
+ }
1012
+ }
1013
+
1014
+ parent.children.splice(index, 1, ...newChildren)
1015
+ return index
1016
+ }
1017
+ }
1018
+ })
1019
+ }
1020
+ }
1021
+
1022
+ const mdxProcessor = remark()
1023
+ .use(remarkMdx)
1024
+ .use(remarkFrontmatter, ['yaml', 'toml'])
1025
+ .use(remarkGfm)
1026
+ .use(remarkMarkAndUnravel)
1027
+ .use(() => {
1028
+ return (tree, file) => {
1029
+ file.data.ast = tree
1030
+ }
1031
+ })
1032
+
1033
+ export { completeJsxTags }
@@ -0,0 +1,60 @@
1
+ // taken from https://www.prompt-kit.com/docs/jsx-preview
2
+
3
+ function matchJsxTag(code: string) {
4
+ if (code.trim() === '') {
5
+ return null
6
+ }
7
+
8
+ const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9]*)\s*([^>]*?)(\/)?>/
9
+ const match = code.match(tagRegex)
10
+
11
+ if (!match || typeof match.index === 'undefined') {
12
+ return null
13
+ }
14
+
15
+ const [fullMatch, tagName, attributes, selfClosing] = match
16
+
17
+ const type = selfClosing
18
+ ? 'self-closing'
19
+ : fullMatch.startsWith('</')
20
+ ? 'closing'
21
+ : 'opening'
22
+
23
+ return {
24
+ tag: fullMatch,
25
+ tagName,
26
+ type,
27
+ attributes: attributes.trim(),
28
+ startIndex: match.index,
29
+ endIndex: match.index + fullMatch.length,
30
+ }
31
+ }
32
+
33
+ export function completeJsxTags(code: string) {
34
+ const stack: string[] = []
35
+ let result = ''
36
+ let currentPosition = 0
37
+
38
+ while (currentPosition < code.length) {
39
+ const match = matchJsxTag(code.slice(currentPosition))
40
+ if (!match) break
41
+ const { tagName, type, endIndex } = match
42
+
43
+ if (type === 'opening') {
44
+ stack.push(tagName)
45
+ } else if (type === 'closing') {
46
+ stack.pop()
47
+ }
48
+
49
+ result += code.slice(currentPosition, currentPosition + endIndex)
50
+ currentPosition += endIndex
51
+ }
52
+
53
+ return (
54
+ result +
55
+ stack
56
+ .reverse()
57
+ .map((tag) => `</${tag}>`)
58
+ .join('')
59
+ )
60
+ }
package/src/plugins.ts DELETED
@@ -1,87 +0,0 @@
1
- import { Root, RootContent } from 'mdast'
2
- import { collapseWhiteSpace } from 'collapse-white-space'
3
- import { visit } from 'unist-util-visit'
4
-
5
-
6
- /**
7
- * https://github.com/mdx-js/mdx/blob/b3351fadcb6f78833a72757b7135dcfb8ab646fe/packages/mdx/lib/plugin/remark-mark-and-unravel.js
8
- * A tiny plugin that unravels `<p><h1>x</h1></p>` but also
9
- * `<p><Component /></p>` (so it has no knowledge of "HTML").
10
- *
11
- * It also marks JSX as being explicitly JSX, so when a user passes a `h1`
12
- * component, it is used for `# heading` but not for `<h1>heading</h1>`.
13
- *
14
- */
15
- export function remarkMarkAndUnravel() {
16
- return function (tree: Root) {
17
-
18
- visit(tree, function (node, index, parent) {
19
- let offset = -1
20
- let all = true
21
- let oneOrMore = false
22
-
23
-
24
- if (
25
- parent &&
26
- typeof index === 'number' &&
27
- node.type === 'paragraph'
28
- ) {
29
- const children = node.children
30
-
31
- while (++offset < children.length) {
32
- const child = children[offset]
33
-
34
- if (
35
- child.type === 'mdxJsxTextElement' ||
36
- child.type === 'mdxTextExpression'
37
- ) {
38
- oneOrMore = true
39
- } else if (
40
- child.type === 'text' &&
41
- collapseWhiteSpace(child.value, {
42
- style: 'html',
43
- trim: true,
44
- }) === ''
45
- ) {
46
- // Empty.
47
- } else {
48
- all = false
49
- break
50
- }
51
- }
52
-
53
- if (all && oneOrMore) {
54
- offset = -1
55
-
56
- const newChildren: RootContent[] = []
57
-
58
- while (++offset < children.length) {
59
- const child = children[offset]
60
-
61
- if (child.type === 'mdxJsxTextElement') {
62
- // @ts-expect-error: mutate because it is faster; content model is fine.
63
- child.type = 'mdxJsxFlowElement'
64
- }
65
-
66
- if (child.type === 'mdxTextExpression') {
67
- // @ts-expect-error: mutate because it is faster; content model is fine.
68
- child.type = 'mdxFlowExpression'
69
- }
70
-
71
- if (
72
- child.type === 'text' &&
73
- /^[\t\r\n ]+$/.test(String(child.value))
74
- ) {
75
- // Empty.
76
- } else {
77
- newChildren.push(child)
78
- }
79
- }
80
-
81
- parent.children.splice(index, 1, ...newChildren)
82
- return index
83
- }
84
- }
85
- })
86
- }
87
- }