@markuplint/mdx-parser 5.2.0 → 5.3.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/CHANGELOG.md CHANGED
@@ -3,6 +3,20 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [5.3.0](https://github.com/markuplint/markuplint/compare/v5.2.0...v5.3.0) (2026-10-21)
7
+
8
+ ### Bug Fixes
9
+
10
+ * make published packages loadable from require() and put types first ([723106f](https://github.com/markuplint/markuplint/commit/723106f51aac4e60dc02db2c1a894617368b0253)), closes [#4198](https://github.com/markuplint/markuplint/issues/4198)
11
+ * **mdx-parser:** read a tag whose name starts with `$`, `_` or a non-ASCII letter ([4ecfe45](https://github.com/markuplint/markuplint/commit/4ecfe455a83cb4f0c9ef496964d91c817e03e5c1))
12
+
13
+ ### Features
14
+
15
+ * **mdx-parser:** answer what a component name is by the rule of JSX ([c29c5af](https://github.com/markuplint/markuplint/commit/c29c5af5455d3253bee0915075f09a49a68b2ebe))
16
+ * **mdx-parser:** declare attribute names as case-sensitive ([f5a6f21](https://github.com/markuplint/markuplint/commit/f5a6f214bb229cdb306f0bc49b394b1b32aea283)), closes [#4229](https://github.com/markuplint/markuplint/issues/4229)
17
+ * **parser-utils:** let a parser say how an attribute name gets into the DOM ([abd81e5](https://github.com/markuplint/markuplint/commit/abd81e5d20f4ffeb0ba878e77fd4122c1a803a9f))
18
+
19
+
6
20
  # [5.2.0](https://github.com/markuplint/markuplint/compare/v5.1.1...v5.2.0) (2026-10-13)
7
21
 
8
22
  ### Bug Fixes
package/lib/parser.d.ts CHANGED
@@ -66,12 +66,32 @@ declare class MDXParser extends MarkdownAwareParser {
66
66
  __rightText?: string;
67
67
  });
68
68
  /**
69
- * MDX components use uppercase or dot notation following React's convention.
69
+ * An MDX element is `authored` when its name is a component's (`isComponentName` below: a name that
70
+ * does not start with a lowercase ASCII letter, or a member expression), a web component when it
71
+ * is a custom element name, and `html` otherwise.
70
72
  *
71
73
  * @param nodeName - The element name to classify.
72
- * @returns The element type (`'html'` or `'authored'`).
74
+ * @returns The element type (`'html'`, `'web-component'` or `'authored'`).
73
75
  */
74
76
  detectElementType(nodeName: string): import("@markuplint/ml-ast").ElementType;
77
+ /**
78
+ * An MDX element name starts with an ECMAScript `IdentifierStart` as a JSX one does (see
79
+ * `JSXParser#isTagNameStart` in `@markuplint/jsx-parser`): keep the two in step.
80
+ *
81
+ * @param char - The character that follows `<`
82
+ * @returns Whether it starts a tag name
83
+ */
84
+ isTagNameStart(char: string): boolean;
85
+ /**
86
+ * An MDX component is a JSX element, so a name is a component unless it starts with a lowercase
87
+ * ASCII letter or has a `-`; a `.` makes it a member expression. The rule is the one of
88
+ * `@markuplint/jsx-parser` (see `JSXParser#isComponentName` there for the grounds), which this
89
+ * package does not depend on: keep the two in step.
90
+ *
91
+ * @param name - A tag name as written, case kept
92
+ * @returns Whether the name is a component's
93
+ */
94
+ isComponentName(name: string): boolean;
75
95
  }
76
96
  export declare const parser: MDXParser;
77
97
  export {};
package/lib/parser.js CHANGED
@@ -1,9 +1,15 @@
1
1
  import { MarkdownAwareParser } from '@markuplint/markdown-parser';
2
+ import { isPotentialCustomElementName } from '@markuplint/parser-utils';
2
3
  import remarkFrontmatter from 'remark-frontmatter';
3
4
  import remarkGfm from 'remark-gfm';
4
5
  import remarkMdx from 'remark-mdx';
5
6
  import remarkParse from 'remark-parse';
6
7
  import { unified } from 'unified';
8
+ /**
9
+ * ECMAScript `IdentifierStart`, as `JSXIdentifier` starts (https://facebook.github.io/jsx/#prod-JSXIdentifier).
10
+ * `@markuplint/jsx-parser` has the same constant: keep the two in step.
11
+ */
12
+ const JSX_IDENTIFIER_START = /^[\p{ID_Start}$_]/u;
7
13
  /**
8
14
  * Parser for MDX files that maps JSX elements to markuplint's AST.
9
15
  *
@@ -30,6 +36,8 @@ class MDXParser extends MarkdownAwareParser {
30
36
  endTagType: 'xml',
31
37
  booleanish: true,
32
38
  tagNameCaseSensitive: true,
39
+ attrNameCaseSensitive: true,
40
+ attrNameRoute: 'dom-api',
33
41
  });
34
42
  }
35
43
  /**
@@ -158,13 +166,40 @@ class MDXParser extends MarkdownAwareParser {
158
166
  return attr;
159
167
  }
160
168
  /**
161
- * MDX components use uppercase or dot notation following React's convention.
169
+ * An MDX element is `authored` when its name is a component's (`isComponentName` below: a name that
170
+ * does not start with a lowercase ASCII letter, or a member expression), a web component when it
171
+ * is a custom element name, and `html` otherwise.
162
172
  *
163
173
  * @param nodeName - The element name to classify.
164
- * @returns The element type (`'html'` or `'authored'`).
174
+ * @returns The element type (`'html'`, `'web-component'` or `'authored'`).
165
175
  */
166
176
  detectElementType(nodeName) {
167
- return super.detectElementType(nodeName, /^[A-Z]|\./);
177
+ return super.detectElementType(nodeName, (name) => this.isComponentName(name));
178
+ }
179
+ /**
180
+ * An MDX element name starts with an ECMAScript `IdentifierStart` as a JSX one does (see
181
+ * `JSXParser#isTagNameStart` in `@markuplint/jsx-parser`): keep the two in step.
182
+ *
183
+ * @param char - The character that follows `<`
184
+ * @returns Whether it starts a tag name
185
+ */
186
+ isTagNameStart(char) {
187
+ return JSX_IDENTIFIER_START.test(char);
188
+ }
189
+ /**
190
+ * An MDX component is a JSX element, so a name is a component unless it starts with a lowercase
191
+ * ASCII letter or has a `-`; a `.` makes it a member expression. The rule is the one of
192
+ * `@markuplint/jsx-parser` (see `JSXParser#isComponentName` there for the grounds), which this
193
+ * package does not depend on: keep the two in step.
194
+ *
195
+ * @param name - A tag name as written, case kept
196
+ * @returns Whether the name is a component's
197
+ */
198
+ isComponentName(name) {
199
+ if (isPotentialCustomElementName(name)) {
200
+ return false;
201
+ }
202
+ return name.includes('.') || (JSX_IDENTIFIER_START.test(name) && !/^[a-z]/.test(name) && !name.includes('-'));
168
203
  }
169
204
  #visitJsxElement(
170
205
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/mdx-parser",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "MDX parser for markuplint",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,8 +15,8 @@
15
15
  "type": "module",
16
16
  "exports": {
17
17
  ".": {
18
- "import": "./lib/index.js",
19
- "types": "./lib/index.d.ts"
18
+ "types": "./lib/index.d.ts",
19
+ "default": "./lib/index.js"
20
20
  }
21
21
  },
22
22
  "publishConfig": {
@@ -28,9 +28,9 @@
28
28
  "clean": "tsc --build --clean tsconfig.build.json"
29
29
  },
30
30
  "dependencies": {
31
- "@markuplint/markdown-parser": "5.2.0",
32
- "@markuplint/ml-ast": "5.1.1",
33
- "@markuplint/parser-utils": "5.2.0",
31
+ "@markuplint/markdown-parser": "5.3.0",
32
+ "@markuplint/ml-ast": "5.3.0",
33
+ "@markuplint/parser-utils": "5.3.0",
34
34
  "remark-frontmatter": "^5.0.0",
35
35
  "remark-gfm": "^4.0.1",
36
36
  "remark-mdx": "^3.1.1",
@@ -40,5 +40,5 @@
40
40
  "devDependencies": {
41
41
  "@types/mdast": "4.0.4"
42
42
  },
43
- "gitHead": "d8b2feab77c9c862513615bea6b80d2817431878"
43
+ "gitHead": "f587cf0206c8426e57369a48d6d5fb22306eafde"
44
44
  }
@@ -0,0 +1,28 @@
1
+ import { describe, expect, test } from 'vitest';
2
+
3
+ import { parser } from './parser.js';
4
+
5
+ // An MDX component is a JSX element: the same rule as `@markuplint/jsx-parser`, which names it a
6
+ // component unless it starts with a lowercase ASCII letter (a `-` makes it an intrinsic element).
7
+ describe('isComponentName', () => {
8
+ test.each(['Foo', 'RouterLink', 'Tabs.Root', 'ns.Item', 'motion.div', '_Layout', '$Ui', 'Ärger', 'Éa'])(
9
+ '"%s" is a component name',
10
+ name => {
11
+ expect(parser.isComponentName(name)).toBe(true);
12
+ },
13
+ );
14
+
15
+ test.each([
16
+ 'div',
17
+ 'svg',
18
+ 'foreignObject',
19
+ 'x-foo',
20
+ 'my-widget.v2',
21
+ 'annotation-xml',
22
+ 'Foo-bar',
23
+ '#jsx-fragment',
24
+ '',
25
+ ])('"%s" is not a component name', name => {
26
+ expect(parser.isComponentName(name)).toBe(false);
27
+ });
28
+ });