markdown-to-jsx 9.3.4 → 9.4.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/README.md CHANGED
@@ -35,6 +35,7 @@ Some special features of the library:
35
35
  - [options.createElement](#optionscreateelement)
36
36
  - [options.forceWrapper](#optionsforcewrapper)
37
37
  - [options.overrides](#optionsoverrides)
38
+ - [options.evalUnserializableExpressions](#optionsevalunserializableexpressions)
38
39
  - [options.renderRule](#optionsrenderrule)
39
40
  - [options.sanitizer](#optionssanitizer)
40
41
  - [options.slugify](#optionsslugify)
@@ -378,23 +379,24 @@ const normalizedMarkdown2 = astToMarkdown(ast)
378
379
 
379
380
  #### All Options
380
381
 
381
- | Option | Type | Default | Description |
382
- | ----------------------- | ----------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
383
- | `createElement` | `function` | - | Custom createElement behavior (React/React Native/SolidJS/Vue only). See [createElement](#optionscreateelement) for details. |
384
- | `disableAutoLink` | `boolean` | `false` | Disable automatic conversion of bare URLs to anchor tags. |
385
- | `disableParsingRawHTML` | `boolean` | `false` | Disable parsing of raw HTML into JSX. |
386
- | `enforceAtxHeadings` | `boolean` | `false` | Require space between `#` and header text (GFM spec compliance). |
387
- | `forceBlock` | `boolean` | `false` | Force all content to be treated as block-level. |
388
- | `forceInline` | `boolean` | `false` | Force all content to be treated as inline. |
389
- | `forceWrapper` | `boolean` | `false` | Force wrapper even with single child (React/React Native/Vue only). See [forceWrapper](#optionsforcewrapper) for details. |
390
- | `overrides` | `object` | - | Override HTML tag rendering. See [overrides](#optionsoverrides) for details. |
391
- | `preserveFrontmatter` | `boolean` | `false` | Include frontmatter in rendered output (as `<pre>` for HTML/JSX, included in markdown). Behavior varies by compiler type. |
392
- | `renderRule` | `function` | - | Custom rendering for AST rules. See [renderRule](#optionsrenderrule) for details. |
393
- | `sanitizer` | `function` | built-in | Custom URL sanitizer function. See [sanitizer](#optionssanitizer) for details. |
394
- | `slugify` | `function` | built-in | Custom slug generation for heading IDs. See [slugify](#optionsslugify) for details. |
395
- | `tagfilter` | `boolean` | `true` | Escape dangerous HTML tags (`script`, `iframe`, `style`, etc.) to prevent XSS. |
396
- | `wrapper` | `string \| component \| null` | `'div'` | Wrapper element for multiple children (React/React Native/Vue only). See [wrapper](#optionswrapper) for details. |
397
- | `wrapperProps` | `object` | - | Props for wrapper element (React/React Native/Vue only). See [wrapperProps](#optionswrapperprops) for details. |
382
+ | Option | Type | Default | Description |
383
+ | ------------------------------- | ----------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
384
+ | `createElement` | `function` | - | Custom createElement behavior (React/React Native/SolidJS/Vue only). See [createElement](#optionscreateelement) for details. |
385
+ | `disableAutoLink` | `boolean` | `false` | Disable automatic conversion of bare URLs to anchor tags. |
386
+ | `disableParsingRawHTML` | `boolean` | `false` | Disable parsing of raw HTML into JSX. |
387
+ | `enforceAtxHeadings` | `boolean` | `false` | Require space between `#` and header text (GFM spec compliance). |
388
+ | `evalUnserializableExpressions` | `boolean` | `false` | ⚠️ Eval unserializable props (DANGEROUS). See [evalUnserializableExpressions](#optionsevalunserializableexpressions) for details. |
389
+ | `forceBlock` | `boolean` | `false` | Force all content to be treated as block-level. |
390
+ | `forceInline` | `boolean` | `false` | Force all content to be treated as inline. |
391
+ | `forceWrapper` | `boolean` | `false` | Force wrapper even with single child (React/React Native/Vue only). See [forceWrapper](#optionsforcewrapper) for details. |
392
+ | `overrides` | `object` | - | Override HTML tag rendering. See [overrides](#optionsoverrides) for details. |
393
+ | `preserveFrontmatter` | `boolean` | `false` | Include frontmatter in rendered output (as `<pre>` for HTML/JSX, included in markdown). Behavior varies by compiler type. |
394
+ | `renderRule` | `function` | - | Custom rendering for AST rules. See [renderRule](#optionsrenderrule) for details. |
395
+ | `sanitizer` | `function` | built-in | Custom URL sanitizer function. See [sanitizer](#optionssanitizer) for details. |
396
+ | `slugify` | `function` | built-in | Custom slug generation for heading IDs. See [slugify](#optionsslugify) for details. |
397
+ | `tagfilter` | `boolean` | `true` | Escape dangerous HTML tags (`script`, `iframe`, `style`, etc.) to prevent XSS. |
398
+ | `wrapper` | `string \| component \| null` | `'div'` | Wrapper element for multiple children (React/React Native/Vue only). See [wrapper](#optionswrapper) for details. |
399
+ | `wrapperProps` | `object` | - | Props for wrapper element (React/React Native/Vue only). See [wrapperProps](#optionswrapperprops) for details. |
398
400
 
399
401
  #### options.createElement
400
402
 
@@ -477,11 +479,100 @@ const md = `<DatePicker timezone="UTC+5" startTime={1514579720511} />`
477
479
 
478
480
  **Important notes:**
479
481
 
480
- - Props are passed as strings; parse them in your component (e.g., `JSON.parse(columns)`)
481
- - JSX interpolations (`{value}`) are passed as raw strings
482
+ - **JSX props are intelligently parsed** (v9.1+):
483
+ - Arrays and objects: `data={[1, 2, 3]}` parsed as `[1, 2, 3]`
484
+ - Booleans: `enabled={true}` → parsed as `true`
485
+ - Functions: `onClick={() => ...}` → kept as string for security (use [renderRule](#optionsrenderrule) for case-by-case handling, or see [evalUnserializableExpressions](#optionsevalunserializableexpressions))
486
+ - Complex expressions: `value={someVar}` → kept as string
487
+ - The original raw attribute string is available in `node.rawAttrs` when using `parser()`
482
488
  - Some props are preserved: `a` (`href`, `title`), `img` (`src`, `alt`, `title`), `input[type="checkbox"]` (`checked`, `readonly`), `ol` (`start`), `td`/`th` (`style`)
483
489
  - Element mappings: `span` for inline text, `code` for inline code, `pre > code` for code blocks
484
490
 
491
+ #### options.evalUnserializableExpressions
492
+
493
+ **⚠️ SECURITY WARNING: STRONGLY DISCOURAGED FOR USER INPUTS**
494
+
495
+ When enabled, attempts to eval expressions in JSX props that cannot be serialized as JSON (functions, variables, complex expressions). This uses `eval()` which can execute arbitrary code.
496
+
497
+ **By default (recommended)**, unserializable expressions are kept as strings for security:
498
+
499
+ ```tsx
500
+ import { parser } from 'markdown-to-jsx'
501
+
502
+ const ast = parser('<Button onClick={() => alert("hi")} />')
503
+ // ast[0].attrs.onClick === "() => alert(\"hi\")" (string, safe)
504
+
505
+ // Arrays and objects are automatically parsed (no eval needed):
506
+ const ast2 = parser('<Table data={[1, 2, 3]} />')
507
+ // ast2[0].attrs.data === [1, 2, 3] (parsed via JSON.parse)
508
+ ```
509
+
510
+ **ONLY enable this option when:**
511
+
512
+ - The markdown source is completely trusted (e.g., your own documentation)
513
+ - You control all JSX components and their props
514
+ - The content is NOT user-generated or user-editable
515
+
516
+ **DO NOT enable this option when:**
517
+
518
+ - Processing user-submitted markdown
519
+ - Rendering untrusted content
520
+ - Building public-facing applications with user content
521
+
522
+ **Example of the danger:**
523
+
524
+ ```tsx
525
+ // User-submitted markdown with malicious code
526
+ const userMarkdown = '<Component onClick={() => fetch("/admin/delete-all")} />'
527
+
528
+ // ❌ DANGEROUS - function will be executable
529
+ parser(userMarkdown, { evalUnserializableExpressions: true })
530
+
531
+ // ✅ SAFE - function kept as string
532
+ parser(userMarkdown) // default behavior
533
+ ```
534
+
535
+ **Safe alternative: Use renderRule for case-by-case handling:**
536
+
537
+ ```tsx
538
+ // Instead of eval'ing arbitrary expressions, handle them selectively in renderRule:
539
+ const handlers = {
540
+ handleClick: () => console.log('clicked'),
541
+ handleSubmit: () => console.log('submitted'),
542
+ }
543
+
544
+ compiler(markdown, {
545
+ renderRule(next, node) {
546
+ if (
547
+ node.type === RuleType.htmlBlock &&
548
+ typeof node.attrs?.onClick === 'string'
549
+ ) {
550
+ // Option 1: Named handler lookup (safest)
551
+ const handler = handlers[node.attrs.onClick]
552
+ if (handler) {
553
+ return <button onClick={handler}>{/* ... */}</button>
554
+ }
555
+
556
+ // Option 2: Selective eval with allowlist (still risky)
557
+ if (
558
+ node.tag === 'TrustedComponent' &&
559
+ node.attrs.onClick.startsWith('() =>')
560
+ ) {
561
+ try {
562
+ const fn = eval(`(${node.attrs.onClick})`)
563
+ return <button onClick={fn}>{/* ... */}</button>
564
+ } catch (e) {
565
+ // Handle error
566
+ }
567
+ }
568
+ }
569
+ return next()
570
+ },
571
+ })
572
+ ```
573
+
574
+ This approach gives you full control over which expressions are evaluated and under what conditions.
575
+
485
576
  #### options.renderRule
486
577
 
487
578
  Supply your own rendering function that can selectively override how _rules_ are rendered (note, this is different than _`options.overrides`_ which operates at the HTML tag level and is more general). You can use this functionality to do pretty much anything with an established AST node; here's an example of selectively overriding the "codeBlock" rule to process LaTeX syntax using the `@matejmazur/react-katex` library:
@@ -519,11 +610,12 @@ By default a lightweight URL sanitizer function is provided to avoid common atta
519
610
 
520
611
  This can be overridden and replaced with a custom sanitizer if desired via `options.sanitizer`:
521
612
 
613
+ <!-- prettier-ignore -->
522
614
  ```tsx
523
615
  // sanitizer in this situation would receive:
524
616
  // ('javascript:alert("foo")', 'a', 'href')
525
617
 
526
- ;<Markdown options={{ sanitizer: (value, tag, attribute) => value }}>
618
+ <Markdown options={{ sanitizer: (value, tag, attribute) => value }}>
527
619
  {`[foo](javascript:alert("foo"))`}
528
620
  </Markdown>
529
621
 
@@ -538,8 +630,9 @@ compiler('[foo](javascript:alert("foo"))', {
538
630
 
539
631
  By default, a [lightweight deburring function](https://github.com/quantizor/markdown-to-jsx/blob/bc2f57412332dc670f066320c0f38d0252e0f057/index.js#L261-L275) is used to generate an HTML id from headings. You can override this by passing a function to `options.slugify`. This is helpful when you are using non-alphanumeric characters (e.g. Chinese or Japanese characters) in headings. For example:
540
632
 
633
+ <!-- prettier-ignore -->
541
634
  ```tsx
542
- ;<Markdown options={{ slugify: str => str }}># 中文</Markdown>
635
+ <Markdown options={{ slugify: str => str }}># 中文</Markdown>
543
636
  compiler('# 中文', { slugify: str => str })
544
637
  ```
545
638
 
@@ -722,10 +815,11 @@ The AST consists of the following node types (use `RuleType` to check node types
722
815
  ```tsx
723
816
  { type: RuleType.table, header: [...], cells: [[...]], align: [...] }
724
817
  ```
725
- - `RuleType.htmlBlock` - HTML blocks
818
+ - `RuleType.htmlBlock` - HTML blocks and JSX components
726
819
  ```tsx
727
820
  { type: RuleType.htmlBlock, tag: "div", attrs: {}, children: [...] }
728
821
  ```
822
+ **Note (v9.1+):** JSX components with blank lines between opening/closing tags now properly nest children instead of creating sibling nodes.
729
823
 
730
824
  **Inline nodes:**
731
825
 
@@ -772,6 +866,15 @@ The AST consists of the following node types (use `RuleType` to check node types
772
866
  { type: RuleType.htmlSelfClosing, tag: "img", attrs: { src: "image.png" } }
773
867
  ```
774
868
 
869
+ **JSX Prop Parsing (v9.1+):**
870
+
871
+ The parser intelligently parses JSX prop values:
872
+
873
+ - Arrays/objects are parsed via `JSON.parse()`: `rows={[["a", "b"]]}` → `attrs.rows = [["a", "b"]]`
874
+ - Functions are kept as strings for security: `onClick={() => ...}` → `attrs.onClick = "() => ..."`
875
+ - Booleans are parsed: `enabled={true}` → `attrs.enabled = true`
876
+ - The original raw attribute string is preserved in `rawAttrs` field
877
+
775
878
  #### Example AST Structure
776
879
 
777
880
  ````tsx
@@ -852,16 +955,36 @@ if (node.type === RuleType.heading) {
852
955
 
853
956
  ### Gotchas
854
957
 
855
- **Props are stringified:** When using `overrides` with React components, props are passed as strings. Parse them in your component:
958
+ **JSX prop parsing (v9.1+):** Arrays and objects in JSX props are automatically parsed:
856
959
 
960
+ <!-- prettier-ignore -->
857
961
  ```tsx
858
- const Table = ({ columns, dataSource, ...props }) => {
859
- const parsedColumns = JSON.parse(columns)
860
- const parsedData = JSON.parse(dataSource)
861
- // ... use parsed data
962
+ // In markdown:
963
+ <Table
964
+ columns={['Name', 'Age']}
965
+ data={[
966
+ ['Alice', 30],
967
+ ['Bob', 25],
968
+ ]}
969
+ />
970
+
971
+ // In your component (v9.1+):
972
+ const Table = ({ columns, data, ...props }) => {
973
+ // columns is already an array: ["Name", "Age"]
974
+ // data is already an array: [["Alice", 30], ["Bob", 25]]
975
+ // No JSON.parse needed!
976
+ }
977
+
978
+ // For backwards compatibility, check types:
979
+ const Table = ({ columns, data, ...props }) => {
980
+ const parsedColumns =
981
+ typeof columns === 'string' ? JSON.parse(columns) : columns
982
+ const parsedData = typeof data === 'string' ? JSON.parse(data) : data
862
983
  }
863
984
  ```
864
985
 
986
+ **Function props are kept as strings** for security. Use [renderRule](#optionsrenderrule) for case-by-case handling, or see [evalUnserializableExpressions](#optionsevalunserializableexpressions) for opt-in eval.
987
+
865
988
  **HTML indentation:** Leading whitespace in HTML blocks is auto-trimmed based on the first line's indentation to avoid markdown syntax conflicts.
866
989
 
867
990
  **Code in HTML:** Don't put code directly in HTML divs. Use fenced code blocks instead: