markdown-to-jsx 9.3.5 → 9.4.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/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,14 +479,105 @@ 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
- 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:
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). The `renderRule` function always executes before any other rendering code, giving you full control over how nodes are rendered, including normally-skipped nodes like `ref`, `footnote`, and `frontmatter`.
579
+
580
+ 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:
488
581
 
489
582
  ````tsx
490
583
  import Markdown, { RuleType } from 'markdown-to-jsx'
@@ -513,17 +606,41 @@ function App() {
513
606
  }
514
607
  ````
515
608
 
609
+ **Accessing parsed HTML content:** For HTML blocks marked as `verbatim` (like `<script>`, `<style>`, `<pre>`), default renderers use `rawText` for CommonMark compliance, but `renderRule` can access the fully parsed AST in `children`:
610
+
611
+ ```tsx
612
+ <Markdown
613
+ options={{
614
+ renderRule(next, node, renderChildren) {
615
+ if (node.type === RuleType.htmlBlock && node.tag === 'script') {
616
+ // Access parsed children even for verbatim blocks
617
+ const parsedContent = node.children || []
618
+ // Or use rawText for original content
619
+ const rawContent = node.rawText || ''
620
+
621
+ // Custom rendering logic here
622
+ return <CustomScript content={parsedContent} raw={rawContent} />
623
+ }
624
+ return next()
625
+ },
626
+ }}
627
+ >
628
+ <script>Hello **world**</script>
629
+ </Markdown>
630
+ ```
631
+
516
632
  #### options.sanitizer
517
633
 
518
634
  By default a lightweight URL sanitizer function is provided to avoid common attack vectors that might be placed into the `href` of an anchor tag, for example. The sanitizer receives the input, the HTML tag being targeted, and the attribute name. The original function is available as a library export called `sanitizer`.
519
635
 
520
636
  This can be overridden and replaced with a custom sanitizer if desired via `options.sanitizer`:
521
637
 
638
+ <!-- prettier-ignore -->
522
639
  ```tsx
523
640
  // sanitizer in this situation would receive:
524
641
  // ('javascript:alert("foo")', 'a', 'href')
525
642
 
526
- ;<Markdown options={{ sanitizer: (value, tag, attribute) => value }}>
643
+ <Markdown options={{ sanitizer: (value, tag, attribute) => value }}>
527
644
  {`[foo](javascript:alert("foo"))`}
528
645
  </Markdown>
529
646
 
@@ -538,8 +655,9 @@ compiler('[foo](javascript:alert("foo"))', {
538
655
 
539
656
  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
657
 
658
+ <!-- prettier-ignore -->
541
659
  ```tsx
542
- ;<Markdown options={{ slugify: str => str }}># 中文</Markdown>
660
+ <Markdown options={{ slugify: str => str }}># 中文</Markdown>
543
661
  compiler('# 中文', { slugify: str => str })
544
662
  ```
545
663
 
@@ -722,11 +840,25 @@ The AST consists of the following node types (use `RuleType` to check node types
722
840
  ```tsx
723
841
  { type: RuleType.table, header: [...], cells: [[...]], align: [...] }
724
842
  ```
725
- - `RuleType.htmlBlock` - HTML blocks
843
+ - `RuleType.htmlBlock` - HTML blocks and JSX components
844
+
726
845
  ```tsx
727
- { type: RuleType.htmlBlock, tag: "div", attrs: {}, children: [...] }
846
+ {
847
+ type: RuleType.htmlBlock,
848
+ tag: "div",
849
+ attrs: {},
850
+ rawAttrs?: string,
851
+ children?: ASTNode[],
852
+ verbatim?: boolean,
853
+ rawText?: string,
854
+ text?: string // @deprecated - use rawText instead
855
+ }
728
856
  ```
729
857
 
858
+ **Note (v9.1+):** JSX components with blank lines between opening/closing tags now properly nest children instead of creating sibling nodes.
859
+
860
+ **HTML Block Parsing (v9.2+):** HTML blocks are always fully parsed into the `children` property, even when marked as `verbatim`. The `verbatim` flag acts as a rendering hint (default renderers use `rawText` for verbatim blocks to maintain CommonMark compliance), but `renderRule` implementations can access the fully parsed AST in `children` for all HTML blocks. The `rawText` field contains the original raw HTML content for verbatim blocks, while `rawAttrs` contains the original attribute string.
861
+
730
862
  **Inline nodes:**
731
863
 
732
864
  - `RuleType.text` - Plain text
@@ -772,6 +904,15 @@ The AST consists of the following node types (use `RuleType` to check node types
772
904
  { type: RuleType.htmlSelfClosing, tag: "img", attrs: { src: "image.png" } }
773
905
  ```
774
906
 
907
+ **JSX Prop Parsing (v9.1+):**
908
+
909
+ The parser intelligently parses JSX prop values:
910
+
911
+ - Arrays/objects are parsed via `JSON.parse()`: `rows={[["a", "b"]]}` → `attrs.rows = [["a", "b"]]`
912
+ - Functions are kept as strings for security: `onClick={() => ...}` → `attrs.onClick = "() => ..."`
913
+ - Booleans are parsed: `enabled={true}` → `attrs.enabled = true`
914
+ - The original raw attribute string is preserved in `rawAttrs` field
915
+
775
916
  #### Example AST Structure
776
917
 
777
918
  ````tsx
@@ -852,16 +993,36 @@ if (node.type === RuleType.heading) {
852
993
 
853
994
  ### Gotchas
854
995
 
855
- **Props are stringified:** When using `overrides` with React components, props are passed as strings. Parse them in your component:
996
+ **JSX prop parsing (v9.1+):** Arrays and objects in JSX props are automatically parsed:
856
997
 
998
+ <!-- prettier-ignore -->
857
999
  ```tsx
858
- const Table = ({ columns, dataSource, ...props }) => {
859
- const parsedColumns = JSON.parse(columns)
860
- const parsedData = JSON.parse(dataSource)
861
- // ... use parsed data
1000
+ // In markdown:
1001
+ <Table
1002
+ columns={['Name', 'Age']}
1003
+ data={[
1004
+ ['Alice', 30],
1005
+ ['Bob', 25],
1006
+ ]}
1007
+ />
1008
+
1009
+ // In your component (v9.1+):
1010
+ const Table = ({ columns, data, ...props }) => {
1011
+ // columns is already an array: ["Name", "Age"]
1012
+ // data is already an array: [["Alice", 30], ["Bob", 25]]
1013
+ // No JSON.parse needed!
1014
+ }
1015
+
1016
+ // For backwards compatibility, check types:
1017
+ const Table = ({ columns, data, ...props }) => {
1018
+ const parsedColumns =
1019
+ typeof columns === 'string' ? JSON.parse(columns) : columns
1020
+ const parsedData = typeof data === 'string' ? JSON.parse(data) : data
862
1021
  }
863
1022
  ```
864
1023
 
1024
+ **Function props are kept as strings** for security. Use [renderRule](#optionsrenderrule) for case-by-case handling, or see [evalUnserializableExpressions](#optionsevalunserializableexpressions) for opt-in eval.
1025
+
865
1026
  **HTML indentation:** Leading whitespace in HTML blocks is auto-trimmed based on the first line's indentation to avoid markdown syntax conflicts.
866
1027
 
867
1028
  **Code in HTML:** Don't put code directly in HTML divs. Use fenced code blocks instead: