recma-mdx-html-override 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ipikuka
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,201 @@
1
+ # recma-mdx-html-override
2
+
3
+ [![npm version][badge-npm-version]][url-npm-package]
4
+ [![npm downloads][badge-npm-download]][url-npm-package]
5
+ [![publish to npm][badge-publish-to-npm]][url-publish-github-actions]
6
+ [![code-coverage][badge-codecov]][url-codecov]
7
+ [![type-coverage][badge-type-coverage]][url-github-package]
8
+ [![typescript][badge-typescript]][url-typescript]
9
+ [![license][badge-license]][url-license]
10
+
11
+ This package is a **[unified][unified]** (**[recma][recma]**) plugin **that ensures selected html raw elements overridable via MDXComponents in MDX**.
12
+
13
+ **[unified][unified]** is a project that transforms content with abstract syntax trees (ASTs) using the new parser **[micromark][micromark]**. **[recma][recma]** adds support for producing a javascript code by transforming **[esast][esast]** which stands for Ecma Script Abstract Syntax Tree (AST) that is used in production of compiled source for the **[MDX][MDX]**.
14
+
15
+ ## When should I use this?
16
+
17
+ **Use this plugin to be able to override selected html raw elements via MDXComponents**.
18
+
19
+ You can find the keys (JSX identifiers, `wrapper` and html tags) can be passed in MDXComponents and the rules whether a key is a `Literal` or a `Reference to an Identifier` in [@mdx-js/mdx docs](https://mdxjs.com/docs/using-mdx/#components).
20
+
21
+ **`recma-mdx-html-override`** focuses the `Literal` ones (the names that start with a lowercase or is not a valid JS identifier) to make them overridable via MDXComponents.
22
+
23
+ Basically, **`recma-mdx-html-override`** changes the `Literal` parameters into **`_components.[literal]`** in the `jsx`/`jsxs` call expressions making approppriate changes in the compiled source; and ensures them overridable via mdx components.
24
+
25
+ ## Installation
26
+
27
+ This package is suitable for ESM only. In Node.js (version 18+), install with npm:
28
+
29
+ ```bash
30
+ npm install recma-mdx-html-override
31
+ ```
32
+
33
+ or
34
+
35
+ ```bash
36
+ yarn add recma-mdx-html-override
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ Say we have the following file, `example.mdx`,
42
+
43
+ ```mdx
44
+ # Hi
45
+
46
+ <img src="image.png" alt="picture" />
47
+ ```
48
+
49
+ And our module, `example.js`, looks as follows:
50
+
51
+ ```javascript
52
+ import { read } from "to-vfile";
53
+ import { compile } from "@mdx-js/mdx";
54
+ import recmaMdxHtmlOverride from "recma-mdx-html-override";
55
+
56
+ main();
57
+
58
+ async function main() {
59
+ const source = await read("example.mdx");
60
+
61
+ const compiledSource = await compile(source, {
62
+ recmaPlugins: [[recmaMdxHtmlOverride, {tags: "img"}]],
63
+ });
64
+
65
+ return String(compiledSource);
66
+ }
67
+ ```
68
+
69
+ Now, running `node example.js` produces the `compiled source` like below:
70
+
71
+ ```js
72
+ function _createMdxContent(props) {
73
+ const _components = {
74
+ h1: "h1",
75
+ + img: "img",
76
+ ...props.components
77
+ };
78
+ return _jsxs(_Fragment, {
79
+ children: [_jsx(_components.h1, {
80
+ children: "Hi"
81
+ - }), "\\n", _jsx("img", {
82
+ + }), "\\n", _jsx(_components.img, {
83
+ src: "image.png",
84
+ alt: "picture"
85
+ })]
86
+ });
87
+ }
88
+ ```
89
+
90
+ And, this provides us to override **`img`** components via mdx components `{ img: () => {/* */} }`
91
+
92
+ ## Options
93
+
94
+ There is one option, which is `undefined` by default.
95
+
96
+ ```typescript
97
+ export type HtmlOverrideOptions = {
98
+ tags?: string | string[]; // default is undefined
99
+ };
100
+ ```
101
+
102
+ ### Tags
103
+
104
+ It is a **`string | string[]`** option to set the tag names of html raw elements to be made overridable in MDX.
105
+
106
+ ```javascript
107
+ use(recmaMdxHtmlOverride, {tags: "video"} as HtmlOverrideOptions);
108
+ ```
109
+
110
+ Now, `<video />` html elements in MDX will be overridable via mdx components.
111
+
112
+ ## Syntax tree
113
+
114
+ This plugin only modifies the ESAST (Ecma Script Abstract Syntax Tree) as explained.
115
+
116
+ ## Types
117
+
118
+ This package is fully typed with [TypeScript][url-typescript]. The plugin options is exported as `HtmlOverrideOptions`.
119
+
120
+ ## Compatibility
121
+
122
+ This plugin works with `unified` version 6+. It is compatible with `mdx` version 3+.
123
+
124
+ ## Security
125
+
126
+ Use of `recma-mdx-html-override` does not involve user content so there are no openings for cross-site scripting (XSS) attacks.
127
+
128
+ ## My Plugins
129
+
130
+ I like to contribute the Unified / Remark / MDX ecosystem, so I recommend you to have a look my plugins.
131
+
132
+ ### My Remark Plugins
133
+
134
+ - [`remark-flexible-code-titles`](https://www.npmjs.com/package/remark-flexible-code-titles)
135
+ – Remark plugin to add titles or/and containers for the code blocks with customizable properties
136
+ - [`remark-flexible-containers`](https://www.npmjs.com/package/remark-flexible-containers)
137
+ – Remark plugin to add custom containers with customizable properties in markdown
138
+ - [`remark-ins`](https://www.npmjs.com/package/remark-ins)
139
+ – Remark plugin to add `ins` element in markdown
140
+ - [`remark-flexible-paragraphs`](https://www.npmjs.com/package/remark-flexible-paragraphs)
141
+ – Remark plugin to add custom paragraphs with customizable properties in markdown
142
+ - [`remark-flexible-markers`](https://www.npmjs.com/package/remark-flexible-markers)
143
+ – Remark plugin to add custom `mark` element with customizable properties in markdown
144
+ - [`remark-flexible-toc`](https://www.npmjs.com/package/remark-flexible-toc)
145
+ – Remark plugin to expose the table of contents via `vfile.data` or via an option reference
146
+ - [`remark-mdx-remove-esm`](https://www.npmjs.com/package/remark-mdx-remove-esm)
147
+ – Remark plugin to remove import and/or export statements (mdxjsEsm)
148
+
149
+ ### My Rehype Plugins
150
+
151
+ - [`rehype-pre-language`](https://www.npmjs.com/package/rehype-pre-language)
152
+ – Rehype plugin to add language information as a property to `pre` element
153
+ - [`rehype-highlight-code-lines`](https://www.npmjs.com/package/rehype-highlight-code-lines)
154
+ – Rehype plugin to add line numbers to code blocks and allow highlighting of desired code lines
155
+ - [`rehype-code-meta`](https://www.npmjs.com/package/rehype-code-meta)
156
+ – Rehype plugin to copy `code.data.meta` to `code.properties.metastring`
157
+
158
+ ### My Recma Plugins
159
+
160
+ - [`recma-mdx-escape-missing-components`](https://www.npmjs.com/package/recma-mdx-escape-missing-components)
161
+ – Recma plugin to set the default value `() => null` for the Components in MDX in case of missing or not provided so as not to throw an error
162
+ - [`recma-mdx-change-props`](https://www.npmjs.com/package/recma-mdx-change-props)
163
+ – Recma plugin to change the `props` parameter into the `_props` in the `function _createMdxContent(props) {/* */}` in the compiled source in order to be able to use `{props.foo}` like expressions. It is useful for the `next-mdx-remote` or `next-mdx-remote-client` users in `nextjs` applications.
164
+ - [`recma-mdx-change-imports`](https://www.npmjs.com/package/recma-mdx-change-imports)
165
+ – Recma plugin to convert import declarations for assets and media with relative links into variable declarations with string URLs, enabling direct asset URL resolution in compiled MDX.
166
+ - [`recma-mdx-import-media`](https://www.npmjs.com/package/recma-mdx-import-media)
167
+ – Recma plugin to turn media relative paths into import declarations for both markdown and html syntax in MDX.
168
+ - [`recma-mdx-import-react`](https://www.npmjs.com/package/recma-mdx-import-react)
169
+ – Recma plugin to ensure getting `React` instance from the arguments and to make the runtime props `{React, jsx, jsxs, jsxDev, Fragment}` is available in the dynamically imported components in the compiled source of MDX.
170
+ - [`recma-mdx-html-override`](https://www.npmjs.com/package/recma-mdx-html-override)
171
+ – Recma plugin to ensure selected html raw elements overridable via mdx components in MDX.
172
+
173
+ ## License
174
+
175
+ [MIT License](./LICENSE) © ipikuka
176
+
177
+ [unified]: https://github.com/unifiedjs/unified
178
+ [micromark]: https://github.com/micromark/micromark
179
+ [recma]: https://mdxjs.com/docs/extending-mdx/#list-of-plugins
180
+ [esast]: https://github.com/syntax-tree/esast
181
+ [estree]: https://github.com/estree/estree
182
+ [MDX]: https://mdxjs.com/
183
+
184
+ [badge-npm-version]: https://img.shields.io/npm/v/recma-mdx-html-override
185
+ [badge-npm-download]:https://img.shields.io/npm/dt/recma-mdx-html-override
186
+ [url-npm-package]: https://www.npmjs.com/package/recma-mdx-html-override
187
+ [url-github-package]: https://github.com/ipikuka/recma-mdx-html-override
188
+
189
+ [badge-license]: https://img.shields.io/github/license/ipikuka/recma-mdx-html-override
190
+ [url-license]: https://github.com/ipikuka/recma-mdx-html-override/blob/main/LICENSE
191
+
192
+ [badge-publish-to-npm]: https://github.com/ipikuka/recma-mdx-html-override/actions/workflows/publish.yml/badge.svg
193
+ [url-publish-github-actions]: https://github.com/ipikuka/recma-mdx-html-override/actions/workflows/publish.yml
194
+
195
+ [badge-typescript]: https://img.shields.io/npm/types/recma-mdx-html-override
196
+ [url-typescript]: https://www.typescriptlang.org/
197
+
198
+ [badge-codecov]: https://codecov.io/gh/ipikuka/recma-mdx-html-override/graph/badge.svg?token=6UIKn4z8lc
199
+ [url-codecov]: https://codecov.io/gh/ipikuka/recma-mdx-html-override
200
+
201
+ [badge-type-coverage]: https://img.shields.io/badge/dynamic/json.svg?label=type-coverage&prefix=%E2%89%A5&suffix=%&query=$.typeCoverage.atLeast&uri=https%3A%2F%2Fraw.githubusercontent.com%2Fipikuka%2Frecma-mdx-html-override%2Fmaster%2Fpackage.json
@@ -0,0 +1,12 @@
1
+ import type { Plugin } from "unified";
2
+ import type { Program } from "estree";
3
+ export type HtmlOverrideOptions = {
4
+ tags?: string | string[];
5
+ };
6
+ /**
7
+ *
8
+ * It is a recma plugin which makes selected html raw elements overridable.
9
+ *
10
+ */
11
+ declare const plugin: Plugin<[HtmlOverrideOptions?], Program>;
12
+ export default plugin;
@@ -0,0 +1,186 @@
1
+ import { CONTINUE, EXIT, SKIP, visit } from "estree-util-visit";
2
+ const DEFAULT_SETTINGS = {
3
+ tags: undefined,
4
+ };
5
+ /**
6
+ *
7
+ * It is a recma plugin which makes selected html raw elements overridable.
8
+ *
9
+ */
10
+ const plugin = (options = {}) => {
11
+ const settings = Object.assign({}, DEFAULT_SETTINGS, options);
12
+ const componentMap = {};
13
+ let functionNode;
14
+ let functionPropsName = "props";
15
+ let targetVariableDeclarator;
16
+ function containsHyphen(name) {
17
+ return name.includes("-");
18
+ }
19
+ return (tree) => {
20
+ // console.dir(tree, { depth: 16 });
21
+ if (!settings.tags)
22
+ return;
23
+ // finds the function _createMdxContent(){}
24
+ visit(tree, (node, _, index) => {
25
+ if (index === undefined)
26
+ return;
27
+ if (node.type !== "FunctionDeclaration")
28
+ return SKIP;
29
+ if (node.id.name === "_createMdxContent") {
30
+ functionNode = node;
31
+ const param = node.params[0];
32
+ if (param.type === "Identifier") {
33
+ functionPropsName = param.name;
34
+ }
35
+ return EXIT;
36
+ }
37
+ /* istanbul ignore next */
38
+ return CONTINUE;
39
+ });
40
+ /* istanbul ignore next */
41
+ if (!functionNode)
42
+ return;
43
+ // trace call expressions to change _jsx("xxx", {}) to _jsx(_components.xxx, {})
44
+ visit(functionNode, (node) => {
45
+ if (node.type !== "CallExpression")
46
+ return CONTINUE;
47
+ if ("name" in node.callee) {
48
+ if (node.callee.name !== "_jsx" &&
49
+ node.callee.name !== "_jsxDEV" &&
50
+ node.callee.name !== "_jsxs") {
51
+ return;
52
+ }
53
+ }
54
+ // First child of a CallExpression is a Literal or Identifier to a reference
55
+ const firstArgument = node.arguments[0];
56
+ if (firstArgument.type === "Literal" &&
57
+ typeof firstArgument.value === "string" &&
58
+ ((typeof settings.tags === "string" && firstArgument.value === settings.tags) ||
59
+ settings.tags.includes(firstArgument.value))) {
60
+ node.arguments[0] = {
61
+ type: "MemberExpression",
62
+ object: { type: "Identifier", name: "_components" },
63
+ property: {
64
+ type: "Identifier",
65
+ name: containsHyphen(firstArgument.value)
66
+ ? '"' + firstArgument.value + '"'
67
+ : firstArgument.value,
68
+ },
69
+ computed: containsHyphen(firstArgument.value),
70
+ optional: false,
71
+ };
72
+ if (!componentMap[firstArgument.value]) {
73
+ componentMap[firstArgument.value] = firstArgument.value;
74
+ }
75
+ }
76
+ return CONTINUE;
77
+ });
78
+ // trace jsx elements to change <xxx /> to <__components.xxx />
79
+ visit(functionNode, (node) => {
80
+ if (node.type !== "JSXElement")
81
+ return CONTINUE;
82
+ // First child of a CallExpression is a Literal or Identifier to a reference
83
+ const openingElement = node.openingElement;
84
+ if (openingElement.name.type === "JSXIdentifier") {
85
+ const jsxIdentifier = openingElement.name;
86
+ if ((typeof settings.tags === "string" && jsxIdentifier.name === settings.tags) ||
87
+ settings.tags.includes(jsxIdentifier.name)) {
88
+ node.openingElement.name = {
89
+ type: "JSXMemberExpression",
90
+ object: { type: "JSXIdentifier", name: "_components" },
91
+ property: {
92
+ type: "JSXIdentifier",
93
+ name: jsxIdentifier.name,
94
+ // TODO: fix <_components["hypened-name"]></>
95
+ // name: containsHyphen(jsxIdentifier.name)
96
+ // ? '"' + jsxIdentifier.name + '"'
97
+ // : jsxIdentifier.name,
98
+ // computed: containsHyphen(jsxIdentifier.name) // proposal to "estree-jsx" for JSXMemberExpression
99
+ },
100
+ };
101
+ if (!componentMap[jsxIdentifier.name]) {
102
+ componentMap[jsxIdentifier.name] = jsxIdentifier.name;
103
+ }
104
+ }
105
+ }
106
+ return CONTINUE;
107
+ });
108
+ if (!Object.keys(componentMap).length)
109
+ return;
110
+ // find "const _components = {}" variable declarator; and add the components inside, if not exist.
111
+ visit(functionNode, (node) => {
112
+ if (node.type !== "VariableDeclarator")
113
+ return CONTINUE;
114
+ if (node.id.type === "Identifier" && node.id.name === "_components") {
115
+ targetVariableDeclarator = node;
116
+ if (node.init?.type === "ObjectExpression") {
117
+ const properties = node.init.properties;
118
+ const existingComponentMap = {};
119
+ for (const property of properties) {
120
+ if (property.type === "Property") {
121
+ if (property.key.type === "Identifier" &&
122
+ property.value.type === "Literal" &&
123
+ typeof property.value.value === "string") {
124
+ existingComponentMap[property.key.name] = property.value.value;
125
+ }
126
+ }
127
+ }
128
+ const diffComponentMap = Object.entries(componentMap).filter(([key]) => !existingComponentMap[key]);
129
+ if (diffComponentMap.length) {
130
+ node.init.properties.splice(node.init.properties.length - 1, 0, ...diffComponentMap.map(([key, value]) => ({
131
+ type: "Property",
132
+ kind: "init",
133
+ key: containsHyphen(key)
134
+ ? { type: "Literal", value: key }
135
+ : { type: "Identifier", name: key },
136
+ value: { type: "Literal", value },
137
+ method: false,
138
+ shorthand: false,
139
+ computed: false,
140
+ })));
141
+ }
142
+ }
143
+ }
144
+ return CONTINUE;
145
+ });
146
+ if (targetVariableDeclarator)
147
+ return;
148
+ // There is no "_components" declarator; so we will add VariableDeclaration ourself
149
+ functionNode.body.body.unshift({
150
+ type: "VariableDeclaration",
151
+ kind: "const",
152
+ declarations: [
153
+ {
154
+ type: "VariableDeclarator",
155
+ id: { type: "Identifier", name: "_components" },
156
+ init: {
157
+ type: "ObjectExpression",
158
+ properties: [
159
+ ...Object.entries(componentMap).map(([key, value]) => ({
160
+ type: "Property",
161
+ kind: "init",
162
+ key: { type: "Identifier", name: key },
163
+ value: { type: "Literal", value },
164
+ method: false,
165
+ shorthand: false,
166
+ computed: false,
167
+ })),
168
+ {
169
+ type: "SpreadElement",
170
+ argument: {
171
+ type: "MemberExpression",
172
+ object: { type: "Identifier", name: functionPropsName },
173
+ property: { type: "Identifier", name: "components" },
174
+ computed: false,
175
+ optional: false,
176
+ },
177
+ },
178
+ ],
179
+ },
180
+ },
181
+ ],
182
+ });
183
+ };
184
+ };
185
+ export default plugin;
186
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAMhE,MAAM,gBAAgB,GAAwB;IAC5C,IAAI,EAAE,SAAS;CAChB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,GAA4C,CAAC,OAAO,GAAG,EAAE,EAAE,EAAE;IACvE,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAC5B,EAAE,EACF,gBAAgB,EAChB,OAAO,CACyB,CAAC;IAEnC,MAAM,YAAY,GAA2B,EAAE,CAAC;IAChD,IAAI,YAA6C,CAAC;IAClD,IAAI,iBAAiB,GAAW,OAAO,CAAC;IACxC,IAAI,wBAA4C,CAAC;IAEjD,SAAS,cAAc,CAAC,IAAY;QAClC,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;IAED,OAAO,CAAC,IAAU,EAAE,EAAE;QACpB,oCAAoC;QACpC,IAAI,CAAC,QAAQ,CAAC,IAAI;YAAE,OAAO;QAE3B,2CAA2C;QAC3C,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE;YAC7B,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO;YAEhC,IAAI,IAAI,CAAC,IAAI,KAAK,qBAAqB;gBAAE,OAAO,IAAI,CAAC;YAErD,IAAI,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,mBAAmB,EAAE,CAAC;gBACzC,YAAY,GAAG,IAAI,CAAC;gBAEpB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBAC7B,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;oBAChC,iBAAiB,GAAG,KAAK,CAAC,IAAI,CAAC;gBACjC,CAAC;gBAED,OAAO,IAAI,CAAC;YACd,CAAC;YAED,0BAA0B;YAC1B,OAAO,QAAQ,CAAC;QAClB,CAAC,CAAC,CAAC;QAEH,0BAA0B;QAC1B,IAAI,CAAC,YAAY;YAAE,OAAO;QAE1B,gFAAgF;QAChF,KAAK,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE;YAC3B,IAAI,IAAI,CAAC,IAAI,KAAK,gBAAgB;gBAAE,OAAO,QAAQ,CAAC;YAEpD,IAAI,MAAM,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBAC1B,IACE,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM;oBAC3B,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS;oBAC9B,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,OAAO,EAC5B,CAAC;oBACD,OAAO;gBACT,CAAC;YACH,CAAC;YAED,4EAA4E;YAC5E,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YAExC,IACE,aAAa,CAAC,IAAI,KAAK,SAAS;gBAChC,OAAO,aAAa,CAAC,KAAK,KAAK,QAAQ;gBACvC,CAAC,CAAC,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ,IAAI,aAAa,CAAC,KAAK,KAAK,QAAQ,CAAC,IAAI,CAAC;oBAC3E,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,EAC9C,CAAC;gBACD,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG;oBAClB,IAAI,EAAE,kBAAkB;oBACxB,MAAM,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,EAAE;oBACnD,QAAQ,EAAE;wBACR,IAAI,EAAE,YAAY;wBAClB,IAAI,EAAE,cAAc,CAAC,aAAa,CAAC,KAAK,CAAC;4BACvC,CAAC,CAAC,GAAG,GAAG,aAAa,CAAC,KAAK,GAAG,GAAG;4BACjC,CAAC,CAAC,aAAa,CAAC,KAAK;qBACxB;oBACD,QAAQ,EAAE,cAAc,CAAC,aAAa,CAAC,KAAK,CAAC;oBAC7C,QAAQ,EAAE,KAAK;iBAChB,CAAC;gBAEF,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;oBACvC,YAAY,CAAC,aAAa,CAAC,KAAK,CAAC,GAAG,aAAa,CAAC,KAAK,CAAC;gBAC1D,CAAC;YACH,CAAC;YAED,OAAO,QAAQ,CAAC;QAClB,CAAC,CAAC,CAAC;QAEH,+DAA+D;QAC/D,KAAK,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE;YAC3B,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY;gBAAE,OAAO,QAAQ,CAAC;YAEhD,4EAA4E;YAC5E,MAAM,cAAc,GAAsB,IAAI,CAAC,cAAc,CAAC;YAE9D,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;gBACjD,MAAM,aAAa,GAAkB,cAAc,CAAC,IAAI,CAAC;gBAEzD,IACE,CAAC,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ,IAAI,aAAa,CAAC,IAAI,KAAK,QAAQ,CAAC,IAAI,CAAC;oBAC3E,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,EAC1C,CAAC;oBACD,IAAI,CAAC,cAAc,CAAC,IAAI,GAAG;wBACzB,IAAI,EAAE,qBAAqB;wBAC3B,MAAM,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,aAAa,EAAE;wBACtD,QAAQ,EAAE;4BACR,IAAI,EAAE,eAAe;4BACrB,IAAI,EAAE,aAAa,CAAC,IAAI;4BACxB,6CAA6C;4BAC7C,2CAA2C;4BAC3C,qCAAqC;4BACrC,0BAA0B;4BAC1B,mGAAmG;yBACpG;qBACF,CAAC;oBAEF,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;wBACtC,YAAY,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,IAAI,CAAC;oBACxD,CAAC;gBACH,CAAC;YACH,CAAC;YAED,OAAO,QAAQ,CAAC;QAClB,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM;YAAE,OAAO;QAE9C,kGAAkG;QAClG,KAAK,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE;YAC3B,IAAI,IAAI,CAAC,IAAI,KAAK,oBAAoB;gBAAE,OAAO,QAAQ,CAAC;YAExD,IAAI,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,EAAE,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;gBACpE,wBAAwB,GAAG,IAAI,CAAC;gBAEhC,IAAI,IAAI,CAAC,IAAI,EAAE,IAAI,KAAK,kBAAkB,EAAE,CAAC;oBAC3C,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC;oBAExC,MAAM,oBAAoB,GAA2B,EAAE,CAAC;oBAExD,KAAK,MAAM,QAAQ,IAAI,UAAU,EAAE,CAAC;wBAClC,IAAI,QAAQ,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;4BACjC,IACE,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,YAAY;gCAClC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS;gCACjC,OAAO,QAAQ,CAAC,KAAK,CAAC,KAAK,KAAK,QAAQ,EACxC,CAAC;gCACD,oBAAoB,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC;4BACjE,CAAC;wBACH,CAAC;oBACH,CAAC;oBAED,MAAM,gBAAgB,GAAG,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,MAAM,CAC1D,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,oBAAoB,CAAC,GAAG,CAAC,CACtC,CAAC;oBAEF,IAAI,gBAAgB,CAAC,MAAM,EAAE,CAAC;wBAC5B,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CACzB,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,EAC/B,CAAC,EACD,GAAG,gBAAgB,CAAC,GAAG,CACrB,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CACf,CAAC;4BACC,IAAI,EAAE,UAAU;4BAChB,IAAI,EAAE,MAAM;4BACZ,GAAG,EAAE,cAAc,CAAC,GAAG,CAAC;gCACtB,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE;gCACjC,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,GAAG,EAAE;4BACrC,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE;4BACjC,MAAM,EAAE,KAAK;4BACb,SAAS,EAAE,KAAK;4BAChB,QAAQ,EAAE,KAAK;yBAChB,CAAa,CACjB,CACF,CAAC;oBACJ,CAAC;gBACH,CAAC;YACH,CAAC;YAED,OAAO,QAAQ,CAAC;QAClB,CAAC,CAAC,CAAC;QAEH,IAAI,wBAAwB;YAAE,OAAO;QAErC,mFAAmF;QACnF,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC;YAC7B,IAAI,EAAE,qBAAqB;YAC3B,IAAI,EAAE,OAAO;YACb,YAAY,EAAE;gBACZ;oBACE,IAAI,EAAE,oBAAoB;oBAC1B,EAAE,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,aAAa,EAAE;oBAC/C,IAAI,EAAE;wBACJ,IAAI,EAAE,kBAAkB;wBACxB,UAAU,EAAE;4BACV,GAAG,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,GAAG,CACjC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CACf,CAAC;gCACC,IAAI,EAAE,UAAU;gCAChB,IAAI,EAAE,MAAM;gCACZ,GAAG,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,GAAG,EAAE;gCACtC,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE;gCACjC,MAAM,EAAE,KAAK;gCACb,SAAS,EAAE,KAAK;gCAChB,QAAQ,EAAE,KAAK;6BAChB,CAAa,CACjB;4BACD;gCACE,IAAI,EAAE,eAAe;gCACrB,QAAQ,EAAE;oCACR,IAAI,EAAE,kBAAkB;oCACxB,MAAM,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,iBAAiB,EAAE;oCACvD,QAAQ,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE;oCACpD,QAAQ,EAAE,KAAK;oCACf,QAAQ,EAAE,KAAK;iCAChB;6BACF;yBACF;qBACF;iBACF;aACF;SACF,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC,CAAC;AAEF,eAAe,MAAM,CAAC"}
@@ -0,0 +1 @@
1
+ {"root":["../../src/index.ts"],"version":"5.8.2"}
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "recma-mdx-html-override",
3
+ "version": "1.0.0",
4
+ "description": "Recma plugin to ensure selected html raw elements overridable via mdx components in MDX.",
5
+ "type": "module",
6
+ "exports": "./dist/esm/index.js",
7
+ "main": "./dist/esm/index.js",
8
+ "types": "./dist/esm/index.d.ts",
9
+ "scripts": {
10
+ "build": "rimraf dist && tsc --build && type-coverage",
11
+ "format": "npm run prettier && npm run lint",
12
+ "prettier": "prettier --write .",
13
+ "lint": "eslint .",
14
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest --config ./jest.config.cjs --coverage",
15
+ "test:file": "NODE_OPTIONS=--experimental-vm-modules jest --config ./jest.config.cjs test1.jsx.spec.ts",
16
+ "prepack": "npm run build",
17
+ "prepublishOnly": "npm test && npm run format"
18
+ },
19
+ "files": [
20
+ "dist/",
21
+ "src/",
22
+ "LICENSE",
23
+ "README.md"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/ipikuka/recma-mdx-html-override.git"
28
+ },
29
+ "keywords": [
30
+ "unified",
31
+ "estree",
32
+ "esast",
33
+ "mdx",
34
+ "mdxjs",
35
+ "plugin",
36
+ "recma",
37
+ "recma plugin",
38
+ "recma mdx",
39
+ "html override",
40
+ "html raw",
41
+ "recma mdx html override"
42
+ ],
43
+ "author": "ipikuka <talatkuyuk@gmail.com>",
44
+ "license": "MIT",
45
+ "homepage": "https://github.com/ipikuka/recma-mdx-html-override#readme",
46
+ "bugs": {
47
+ "url": "https://github.com/ipikuka/recma-mdx-html-override/issues"
48
+ },
49
+ "devDependencies": {
50
+ "@eslint/js": "^9.20.0",
51
+ "@mdx-js/mdx": "^3.1.0",
52
+ "@types/dedent": "^0.7.2",
53
+ "@types/jest": "^29.5.14",
54
+ "@types/node": "^22.13.9",
55
+ "dedent": "^1.5.3",
56
+ "eslint": "^9.21.0",
57
+ "eslint-config-prettier": "^10.0.2",
58
+ "eslint-plugin-jest": "^28.11.0",
59
+ "eslint-plugin-prettier": "^5.2.3",
60
+ "jest": "^29.7.0",
61
+ "prettier": "^3.5.3",
62
+ "prettier-2": "npm:prettier@^2.8.8",
63
+ "rimraf": "^5.0.10",
64
+ "ts-jest": "^29.2.6",
65
+ "type-coverage": "^2.29.7",
66
+ "typescript": "^5.8.2",
67
+ "typescript-eslint": "^8.26.0",
68
+ "unified": "^11.0.5"
69
+ },
70
+ "dependencies": {
71
+ "@types/estree": "^1.0.6",
72
+ "@types/estree-jsx": "^1.0.5",
73
+ "estree-util-visit": "^2.0.0"
74
+ },
75
+ "peerDependencies": {
76
+ "unified": "^11"
77
+ },
78
+ "sideEffects": false,
79
+ "typeCoverage": {
80
+ "atLeast": 100,
81
+ "detail": true,
82
+ "ignoreAsAssertion": true,
83
+ "strict": true
84
+ }
85
+ }
package/src/index.ts ADDED
@@ -0,0 +1,243 @@
1
+ import type { Plugin } from "unified";
2
+ import type { FunctionDeclaration, Node, Program, Property, VariableDeclarator } from "estree";
3
+ import type { JSXIdentifier, JSXOpeningElement } from "estree-jsx";
4
+ import { CONTINUE, EXIT, SKIP, visit } from "estree-util-visit";
5
+
6
+ export type HtmlOverrideOptions = {
7
+ tags?: string | string[];
8
+ };
9
+
10
+ const DEFAULT_SETTINGS: HtmlOverrideOptions = {
11
+ tags: undefined,
12
+ };
13
+
14
+ /**
15
+ *
16
+ * It is a recma plugin which makes selected html raw elements overridable.
17
+ *
18
+ */
19
+ const plugin: Plugin<[HtmlOverrideOptions?], Program> = (options = {}) => {
20
+ const settings = Object.assign(
21
+ {},
22
+ DEFAULT_SETTINGS,
23
+ options,
24
+ ) as Required<HtmlOverrideOptions>;
25
+
26
+ const componentMap: Record<string, string> = {};
27
+ let functionNode: FunctionDeclaration | undefined;
28
+ let functionPropsName: string = "props";
29
+ let targetVariableDeclarator: VariableDeclarator;
30
+
31
+ function containsHyphen(name: string): boolean {
32
+ return name.includes("-");
33
+ }
34
+
35
+ return (tree: Node) => {
36
+ // console.dir(tree, { depth: 16 });
37
+ if (!settings.tags) return;
38
+
39
+ // finds the function _createMdxContent(){}
40
+ visit(tree, (node, _, index) => {
41
+ if (index === undefined) return;
42
+
43
+ if (node.type !== "FunctionDeclaration") return SKIP;
44
+
45
+ if (node.id.name === "_createMdxContent") {
46
+ functionNode = node;
47
+
48
+ const param = node.params[0];
49
+ if (param.type === "Identifier") {
50
+ functionPropsName = param.name;
51
+ }
52
+
53
+ return EXIT;
54
+ }
55
+
56
+ /* istanbul ignore next */
57
+ return CONTINUE;
58
+ });
59
+
60
+ /* istanbul ignore next */
61
+ if (!functionNode) return;
62
+
63
+ // trace call expressions to change _jsx("xxx", {}) to _jsx(_components.xxx, {})
64
+ visit(functionNode, (node) => {
65
+ if (node.type !== "CallExpression") return CONTINUE;
66
+
67
+ if ("name" in node.callee) {
68
+ if (
69
+ node.callee.name !== "_jsx" &&
70
+ node.callee.name !== "_jsxDEV" &&
71
+ node.callee.name !== "_jsxs"
72
+ ) {
73
+ return;
74
+ }
75
+ }
76
+
77
+ // First child of a CallExpression is a Literal or Identifier to a reference
78
+ const firstArgument = node.arguments[0];
79
+
80
+ if (
81
+ firstArgument.type === "Literal" &&
82
+ typeof firstArgument.value === "string" &&
83
+ ((typeof settings.tags === "string" && firstArgument.value === settings.tags) ||
84
+ settings.tags.includes(firstArgument.value))
85
+ ) {
86
+ node.arguments[0] = {
87
+ type: "MemberExpression",
88
+ object: { type: "Identifier", name: "_components" },
89
+ property: {
90
+ type: "Identifier",
91
+ name: containsHyphen(firstArgument.value)
92
+ ? '"' + firstArgument.value + '"'
93
+ : firstArgument.value,
94
+ },
95
+ computed: containsHyphen(firstArgument.value),
96
+ optional: false,
97
+ };
98
+
99
+ if (!componentMap[firstArgument.value]) {
100
+ componentMap[firstArgument.value] = firstArgument.value;
101
+ }
102
+ }
103
+
104
+ return CONTINUE;
105
+ });
106
+
107
+ // trace jsx elements to change <xxx /> to <__components.xxx />
108
+ visit(functionNode, (node) => {
109
+ if (node.type !== "JSXElement") return CONTINUE;
110
+
111
+ // First child of a CallExpression is a Literal or Identifier to a reference
112
+ const openingElement: JSXOpeningElement = node.openingElement;
113
+
114
+ if (openingElement.name.type === "JSXIdentifier") {
115
+ const jsxIdentifier: JSXIdentifier = openingElement.name;
116
+
117
+ if (
118
+ (typeof settings.tags === "string" && jsxIdentifier.name === settings.tags) ||
119
+ settings.tags.includes(jsxIdentifier.name)
120
+ ) {
121
+ node.openingElement.name = {
122
+ type: "JSXMemberExpression",
123
+ object: { type: "JSXIdentifier", name: "_components" },
124
+ property: {
125
+ type: "JSXIdentifier",
126
+ name: jsxIdentifier.name,
127
+ // TODO: fix <_components["hypened-name"]></>
128
+ // name: containsHyphen(jsxIdentifier.name)
129
+ // ? '"' + jsxIdentifier.name + '"'
130
+ // : jsxIdentifier.name,
131
+ // computed: containsHyphen(jsxIdentifier.name) // proposal to "estree-jsx" for JSXMemberExpression
132
+ },
133
+ };
134
+
135
+ if (!componentMap[jsxIdentifier.name]) {
136
+ componentMap[jsxIdentifier.name] = jsxIdentifier.name;
137
+ }
138
+ }
139
+ }
140
+
141
+ return CONTINUE;
142
+ });
143
+
144
+ if (!Object.keys(componentMap).length) return;
145
+
146
+ // find "const _components = {}" variable declarator; and add the components inside, if not exist.
147
+ visit(functionNode, (node) => {
148
+ if (node.type !== "VariableDeclarator") return CONTINUE;
149
+
150
+ if (node.id.type === "Identifier" && node.id.name === "_components") {
151
+ targetVariableDeclarator = node;
152
+
153
+ if (node.init?.type === "ObjectExpression") {
154
+ const properties = node.init.properties;
155
+
156
+ const existingComponentMap: Record<string, string> = {};
157
+
158
+ for (const property of properties) {
159
+ if (property.type === "Property") {
160
+ if (
161
+ property.key.type === "Identifier" &&
162
+ property.value.type === "Literal" &&
163
+ typeof property.value.value === "string"
164
+ ) {
165
+ existingComponentMap[property.key.name] = property.value.value;
166
+ }
167
+ }
168
+ }
169
+
170
+ const diffComponentMap = Object.entries(componentMap).filter(
171
+ ([key]) => !existingComponentMap[key],
172
+ );
173
+
174
+ if (diffComponentMap.length) {
175
+ node.init.properties.splice(
176
+ node.init.properties.length - 1,
177
+ 0,
178
+ ...diffComponentMap.map(
179
+ ([key, value]) =>
180
+ ({
181
+ type: "Property",
182
+ kind: "init",
183
+ key: containsHyphen(key)
184
+ ? { type: "Literal", value: key }
185
+ : { type: "Identifier", name: key },
186
+ value: { type: "Literal", value },
187
+ method: false,
188
+ shorthand: false,
189
+ computed: false,
190
+ }) as Property,
191
+ ),
192
+ );
193
+ }
194
+ }
195
+ }
196
+
197
+ return CONTINUE;
198
+ });
199
+
200
+ if (targetVariableDeclarator) return;
201
+
202
+ // There is no "_components" declarator; so we will add VariableDeclaration ourself
203
+ functionNode.body.body.unshift({
204
+ type: "VariableDeclaration",
205
+ kind: "const",
206
+ declarations: [
207
+ {
208
+ type: "VariableDeclarator",
209
+ id: { type: "Identifier", name: "_components" },
210
+ init: {
211
+ type: "ObjectExpression",
212
+ properties: [
213
+ ...Object.entries(componentMap).map(
214
+ ([key, value]) =>
215
+ ({
216
+ type: "Property",
217
+ kind: "init",
218
+ key: { type: "Identifier", name: key },
219
+ value: { type: "Literal", value },
220
+ method: false,
221
+ shorthand: false,
222
+ computed: false,
223
+ }) as Property,
224
+ ),
225
+ {
226
+ type: "SpreadElement",
227
+ argument: {
228
+ type: "MemberExpression",
229
+ object: { type: "Identifier", name: functionPropsName },
230
+ property: { type: "Identifier", name: "components" },
231
+ computed: false,
232
+ optional: false,
233
+ },
234
+ },
235
+ ],
236
+ },
237
+ },
238
+ ],
239
+ });
240
+ };
241
+ };
242
+
243
+ export default plugin;