Repository: mdx-js/mdx (JavaScript)

Issue:
Compatibility with Solid and other JSX flavors
### Initial checklist

- [X] I read the [support docs](https://mdxjs.com/community/support/)
- [X] I read the [contributing guide](https://mdxjs.com/community/contribute/)
- [X] I agree to follow the [code of conduct](https://github.com/mdx-js/.github/blob/HEAD/code-of-conduct.md)
- [X] I searched [issues](https://github.com/search?q=user%3Amdx-js&type=Issues) and couldn’t find anything (or linked relevant results below)

### Problem

Currently this package is only compatible with React-like JSX flavors, for example by outputting `className` for CSS classes, and camel cased CSS properties in the style object..

### Solution

The underlying utility used in the recma plugin to create the jsx is `hast-util-to-estree`. This util has recently updated to support options to customize how it get rendered (for example, by using `class` instead of `className`, which is necessary for Solid).

These new options should be made available and passed through by this package, so that users can take advantage of them.

Context: https://github.com/syntax-tree/hast-util-to-estree/issues/7#issuecomment-1421953456

### Alternatives

None :)


Discussion:

Patch:
diff --git a/packages/mdx/lib/core.js b/packages/mdx/lib/core.js
index f028dbfec..feed19d77 100644
--- a/packages/mdx/lib/core.js
+++ b/packages/mdx/lib/core.js
@@ -2,6 +2,7 @@
  * @typedef {import('remark-rehype').Options} RemarkRehypeOptions
  * @typedef {import('unified').PluggableList} PluggableList
  * @typedef {import('unified').Processor} Processor
+ * @typedef {import('./plugin/rehype-recma.js').Options} RehypeRecmaOptions
  * @typedef {import('./plugin/recma-document.js').RecmaDocumentOptions} RecmaDocumentOptions
  * @typedef {import('./plugin/recma-stringify.js').RecmaStringifyOptions} RecmaStringifyOptions
  * @typedef {import('./plugin/recma-jsx-rewrite.js').RecmaJsxRewriteOptions} RecmaJsxRewriteOptions
@@ -29,7 +30,7 @@
  * @property {RemarkRehypeOptions | null | undefined} [remarkRehypeOptions]
  *   Options to pass through to `remark-rehype`.
  *
- * @typedef {Omit<RecmaDocumentOptions & RecmaStringifyOptions & RecmaJsxRewriteOptions, 'outputFormat'>} PluginOptions
+ * @typedef {Omit<RehypeRecmaOptions & RecmaDocumentOptions & RecmaStringifyOptions & RecmaJsxRewriteOptions, 'outputFormat'>} PluginOptions
  *   Configuration for internal plugins.
  *
  * @typedef {BaseProcessorOptions & PluginOptions} ProcessorOptions
@@ -82,6 +83,8 @@ export function createProcessor(options) {
     rehypePlugins,
     remarkPlugins,
     remarkRehypeOptions,
+    elementAttributeNameCase,
+    stylePropertyNameCase,
     SourceMapGenerator,
     ...rest
   } = options || {}
@@ -136,7 +139,7 @@ export function createProcessor(options) {
   }
 
   pipeline
-    .use(rehypeRecma)
+    .use(rehypeRecma, {elementAttributeNameCase, stylePropertyNameCase})
     .use(recmaDocument, {...rest, outputFormat})
     .use(recmaJsxRewrite, {
       development: dev,
diff --git a/packages/mdx/lib/plugin/rehype-recma.js b/packages/mdx/lib/plugin/rehype-recma.js
index a55abbc31..e8279369d 100644
--- a/packages/mdx/lib/plugin/rehype-recma.js
+++ b/packages/mdx/lib/plugin/rehype-recma.js
@@ -3,14 +3,41 @@
  * @typedef {import('hast').Root} Root
  */
 
+/**
+ * @typedef {'html' | 'react'} ElementAttributeNameCase
+ *   Specify casing to use for attribute names.
+ *
+ *   HTML casing is for example `class`, `stroke-linecap`, `xml:lang`.
+ *   React casing is for example `className`, `strokeLinecap`, `xmlLang`.
+ *
+ * @typedef {'css' | 'dom'} StylePropertyNameCase
+ *   Casing to use for property names in `style` objects.
+ *
+ *   CSS casing is for example `background-color` and `-webkit-line-clamp`.
+ *   DOM casing is for example `backgroundColor` and `WebkitLineClamp`.
+ *
+ * @typedef Options
+ *   Configuration for internal plugin `rehype-recma`.
+ * @property {ElementAttributeNameCase | null | undefined} [elementAttributeNameCase='react']
+ *   Specify casing to use for attribute names.
+ *
+ *   This casing is used for hast elements, not for embedded MDX JSX nodes
+ *   (components that someone authored manually).
+ * @property {StylePropertyNameCase | null | undefined} [stylePropertyNameCase='dom']
+ *   Specify casing to use for property names in `style` objects.
+ *
+ *   This casing is used for hast elements, not for embedded MDX JSX nodes
+ *   (components that someone authored manually).
+ */
+
 import {toEstree} from 'hast-util-to-estree'
 
 /**
  * A plugin to transform an HTML (hast) tree to a JS (estree).
  * `hast-util-to-estree` does all the work for us!
  *
- * @type {import('unified').Plugin<[], Root, Program>}
+ * @type {import('unified').Plugin<[Options | null | undefined] | [], Root, Program>}
  */
-export function rehypeRecma() {
-  return (tree) => toEstree(tree)
+export function rehypeRecma(options) {
+  return (tree) => toEstree(tree, options)
 }
diff --git a/packages/mdx/readme.md b/packages/mdx/readme.md
index 23d98d4be..826ed66fe 100644
--- a/packages/mdx/readme.md
+++ b/packages/mdx/readme.md
@@ -702,6 +702,22 @@ is `'c'` this following will be generated: `import a from 'c'`.
 
 See `options.pragma` for an example.
 
+###### `options.elementAttributeNameCase`
+
+Specify casing to use for attribute names (`'html' | 'react`, default:
+`'react'`).
+
+This casing is used for hast elements, not for embedded MDX JSX nodes
+(components that someone authored manually).
+
+###### `options.stylePropertyNameCase`
+
+Specify casing to use for property names in `style` objects (`'css' | 'dom`,
+default: `'dom'`).
+
+This casing is used for hast elements, not for embedded MDX JSX nodes
+(components that someone authored manually).
+
 ###### Returns
 
 `Promise<VFile>` — Promise that resolves to the compiled JS as a [vfile][].


Test patch:
diff --git a/packages/mdx/test/compile.js b/packages/mdx/test/compile.js
index 65c5ed128..5920b414f 100644
--- a/packages/mdx/test/compile.js
+++ b/packages/mdx/test/compile.js
@@ -943,6 +943,60 @@ test('jsx', async () => {
     /a & b &#123; c &lt; d/,
     'should serialize `<` and `{` in JSX text'
   )
+
+  assert.match(
+    String(
+      compileSync('', {
+        rehypePlugins: [
+          /** @type {import('unified').Plugin<[], import('hast').Root>} */
+          function () {
+            return function (tree) {
+              tree.children.push({
+                type: 'element',
+                tagName: 'a',
+                properties: {
+                  className: 'b',
+                  style: '-webkit-box-shadow: 0 0 1px 0 red'
+                },
+                children: []
+              })
+            }
+          }
+        ],
+        jsx: true
+      })
+    ),
+    /className="b"/,
+    'should use React props and DOM styles by default'
+  )
+
+  assert.match(
+    String(
+      compileSync('', {
+        rehypePlugins: [
+          /** @type {import('unified').Plugin<[], import('hast').Root>} */
+          function () {
+            return function (tree) {
+              tree.children.push({
+                type: 'element',
+                tagName: 'a',
+                properties: {
+                  className: 'b',
+                  style: '-webkit-box-shadow: 0 0 1px 0 red'
+                },
+                children: []
+              })
+            }
+          }
+        ],
+        elementAttributeNameCase: 'html',
+        stylePropertyNameCase: 'css',
+        jsx: true
+      })
+    ),
+    /class="b"/,
+    'should support `elementAttributeNameCase` and `stylePropertyNameCase`'
+  )
 })
 
 test('markdown (CM)', async () => {