@takazudo/mdx-formatter 0.5.0-next.2 → 0.5.0-next.3

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
@@ -4,15 +4,16 @@ AST-based markdown and MDX formatter with Japanese text support. Built on top of
4
4
 
5
5
  ## Features
6
6
 
7
- - **AST-based formatting** - Uses remark's AST for reliable formatting
8
- - **MDX support** - Full support for MDX syntax including JSX components
9
- - **Japanese text formatting** - Special handling for Japanese punctuation and URLs
10
- - **Docusaurus support** - Preserves Docusaurus admonitions (:::note, :::tip, etc.)
11
- - **HTML block formatting** - Proper indentation for HTML blocks (dl, table, ul, div, etc.)
12
- - **GFM features** - Tables, strikethrough, task lists
13
- - **Frontmatter preservation** - YAML frontmatter support
14
- - **CLI and API** - Use as command-line tool or import as library
15
- - **Configurable** - Customize component lists and rules via config file or API
7
+ - **AST-based formatting** Uses remark's AST for reliable, structural formatting
8
+ - **MDX support** Full support for MDX syntax including JSX components, imports, exports
9
+ - **Japanese text handling** Preserves Japanese punctuation and text formatting
10
+ - **Docusaurus admonitions** Preserves `:::note`, `:::tip`, `:::warning` etc. syntax
11
+ - **HTML block formatting** Proper indentation for HTML blocks (dl, table, ul, div, etc.) via Prettier
12
+ - **GFM features** Tables, strikethrough, task lists
13
+ - **YAML frontmatter** Formatting with safe value pre-processing
14
+ - **CLI and API** Use as command-line tool or import as library
15
+ - **Browser export** — `@takazudo/mdx-formatter/browser` for Vite/WebView/Tauri builds
16
+ - **Configurable** — 10 independently toggleable rules via config file or API
16
17
 
17
18
  ## Installation
18
19
 
@@ -26,6 +27,12 @@ Or use directly with npx:
26
27
  npx @takazudo/mdx-formatter --write "**/*.md"
27
28
  ```
28
29
 
30
+ ### Prerelease (next)
31
+
32
+ ```bash
33
+ npm install @takazudo/mdx-formatter@next
34
+ ```
35
+
29
36
  ## Usage
30
37
 
31
38
  ### CLI
@@ -36,6 +43,9 @@ mdx-formatter --check "**/*.{md,mdx}"
36
43
 
37
44
  # Format files in place
38
45
  mdx-formatter --write "**/*.{md,mdx}"
46
+
47
+ # With config file
48
+ mdx-formatter --config .mdx-formatter.json --write "**/*.mdx"
39
49
  ```
40
50
 
41
51
  ### API
@@ -47,24 +57,48 @@ const formatted = await format('# Hello\nWorld');
47
57
  console.log(formatted); // '# Hello\n\nWorld'
48
58
  ```
49
59
 
60
+ ### Browser / WebView
61
+
62
+ ```javascript
63
+ import { format } from '@takazudo/mdx-formatter/browser';
64
+
65
+ const formatted = await format('# Hello\nWorld');
66
+ ```
67
+
68
+ The browser export avoids Node.js `fs`/`path` dependencies. See [Browser Usage](https://takazudomodular.com/pj/mdx-formatter/docs/overview/browser-usage) for details.
69
+
50
70
  ## Documentation
51
71
 
52
- For full documentation including configuration, options reference, formatting rules, and API reference, visit the [documentation site](https://takazudomodular.com/pj/mdx-formatter/).
72
+ Full documentation at **[takazudomodular.com/pj/mdx-formatter](https://takazudomodular.com/pj/mdx-formatter/)**:
73
+
74
+ - [Overview](https://takazudomodular.com/pj/mdx-formatter/docs/overview) — Installation, usage, API, configuration
75
+ - [Formatting Rules](https://takazudomodular.com/pj/mdx-formatter/docs/formatting) — How the formatter handles each construct
76
+ - [Options](https://takazudomodular.com/pj/mdx-formatter/docs/options) — Per-rule configuration reference
77
+ - [Architecture](https://takazudomodular.com/pj/mdx-formatter/docs/architecture) — Hybrid formatter approach, Rust rewrite strategy
78
+ - [Changelog](https://takazudomodular.com/pj/mdx-formatter/docs/changelog) — Release history
79
+
80
+ ## Rust Rewrite (Experimental)
81
+
82
+ An experimental Rust implementation using [markdown-rs](https://github.com/wooorm/markdown-rs) and [napi-rs](https://napi.rs/) is in progress at `crates/`. See [Architecture: Rust Rewrite](https://takazudomodular.com/pj/mdx-formatter/docs/architecture/rust-rewrite) for details.
53
83
 
54
84
  ## Development
55
85
 
56
86
  ```bash
57
- # Install dependencies
58
- pnpm install
59
-
60
- # Run tests
61
- pnpm test
87
+ pnpm install # Install dependencies
88
+ pnpm build # Compile TypeScript
89
+ pnpm test # Run tests (274 tests)
90
+ pnpm test:watch # Watch mode
91
+ pnpm test:coverage # Coverage report
92
+ pnpm lint # ESLint check
93
+ pnpm check # Prettier + ESLint check
94
+ ```
62
95
 
63
- # Run tests in watch mode
64
- pnpm test:watch
96
+ ### Doc Site
65
97
 
66
- # Run tests with coverage
67
- pnpm test:coverage
98
+ ```bash
99
+ pnpm --dir doc dev # Dev server on port 3518
100
+ pnpm --dir doc dev:network # Network accessible (0.0.0.0:3518)
101
+ pnpm --dir doc build # Production build
68
102
  ```
69
103
 
70
104
  ## License
package/dist/browser.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * that rule depends on prettier, which is a Node.js dependency. If the code
11
11
  * path is never reached, bundlers can tree-shake the prettier import away.
12
12
  */
13
- import { HybridFormatter } from './hybrid-formatter.js';
13
+ import { MdxFormatter } from './mdx-formatter.js';
14
14
  import { detectMdx } from './detect-mdx.js';
15
15
  import { formatterSettings } from './settings.js';
16
16
  import { deepCloneSettings, deepMerge } from './utils.js';
@@ -35,7 +35,7 @@ export async function format(content, settings = {}) {
35
35
  try {
36
36
  let result = content;
37
37
  for (let i = 0; i < MAX_ITERATIONS; i++) {
38
- const formatter = new HybridFormatter(result, merged);
38
+ const formatter = new MdxFormatter(result, merged);
39
39
  const formatted = await formatter.format();
40
40
  if (formatted === result)
41
41
  break;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Main entry point for the markdown formatter
3
- * Uses HybridFormatter for AST-based formatting
3
+ * Uses MdxFormatter for AST-based formatting
4
4
  */
5
5
  import { detectMdx } from './detect-mdx.js';
6
6
  import type { FormatOptions } from './types.js';
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Main entry point for the markdown formatter
3
- * Uses HybridFormatter for AST-based formatting
3
+ * Uses MdxFormatter for AST-based formatting
4
4
  */
5
5
  import { promises as fs } from 'fs';
6
- import { HybridFormatter } from './hybrid-formatter.js';
6
+ import { MdxFormatter } from './mdx-formatter.js';
7
7
  import { loadConfig } from './load-config.js';
8
8
  import { detectMdx } from './detect-mdx.js';
9
9
  export { detectMdx };
@@ -19,7 +19,7 @@ export async function format(content, options = {}) {
19
19
  // cases (most files converge in 1, edge cases in 2).
20
20
  const MAX_ITERATIONS = 3;
21
21
  for (let i = 0; i < MAX_ITERATIONS; i++) {
22
- const formatter = new HybridFormatter(result, settings);
22
+ const formatter = new MdxFormatter(result, settings);
23
23
  const formatted = await formatter.format();
24
24
  if (formatted === result)
25
25
  break;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * LegacyLineFormatter - Applies only specific formatting rules to MDX content
3
+ * Does NOT reformat everything, only applies targeted fixes
4
+ */
5
+ import type { FormatterSettings } from './types.js';
6
+ export declare class LegacyLineFormatter {
7
+ private content;
8
+ private lines;
9
+ settings: FormatterSettings;
10
+ constructor(content: string, settings?: FormatterSettings | null);
11
+ format(): Promise<string>;
12
+ /**
13
+ * Rule 1: Add 1 empty line between elements
14
+ */
15
+ addEmptyLineBetweenElements(content: string): string;
16
+ shouldAddEmptyLine(currentLine: string, nextLine: string): boolean;
17
+ /**
18
+ * Combined JSX formatting - handles structure and content indentation together
19
+ */
20
+ formatJsxStructure(content: string): string;
21
+ /**
22
+ * Rule 4: Expand single-line JSX with 2+ props
23
+ */
24
+ expandSingleLineJsx(content: string): string;
25
+ parseJsxProps(propsString: string): string[];
26
+ /**
27
+ * Rule 7: Validate MDX and throw errors on invalid content
28
+ */
29
+ validateMdx(content: string): boolean;
30
+ }
@@ -0,0 +1,469 @@
1
+ /**
2
+ * LegacyLineFormatter - Applies only specific formatting rules to MDX content
3
+ * Does NOT reformat everything, only applies targeted fixes
4
+ */
5
+ import { formatterSettings } from './settings.js';
6
+ import { HtmlBlockFormatter } from './html-block-formatter.js';
7
+ import { deepCloneSettings } from './utils.js';
8
+ export class LegacyLineFormatter {
9
+ content;
10
+ lines;
11
+ settings;
12
+ constructor(content, settings = null) {
13
+ // Normalize line endings to \n
14
+ this.content = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
15
+ this.lines = this.content.split('\n');
16
+ this.settings = settings ? deepCloneSettings(settings) : deepCloneSettings(formatterSettings);
17
+ }
18
+ async format() {
19
+ let result = this.content;
20
+ // Apply rules in specific order to avoid conflicts
21
+ // HTML block formatting
22
+ if (this.settings.formatHtmlBlocksInMdx && this.settings.formatHtmlBlocksInMdx.enabled) {
23
+ const htmlFormatter = new HtmlBlockFormatter(this.settings.formatHtmlBlocksInMdx);
24
+ result = await htmlFormatter.format(result);
25
+ }
26
+ if (this.settings.expandSingleLineJsx.enabled) {
27
+ result = this.expandSingleLineJsx(result);
28
+ }
29
+ // Combined JSX formatting - do multi-line first, then content indenting
30
+ if (this.settings.formatMultiLineJsx.enabled || this.settings.indentJsxContent.enabled) {
31
+ result = this.formatJsxStructure(result);
32
+ }
33
+ if (this.settings.addEmptyLineBetweenElements.enabled) {
34
+ result = this.addEmptyLineBetweenElements(result);
35
+ }
36
+ if (this.settings.preserveAdmonitions.enabled) {
37
+ // Admonitions are preserved by not touching them
38
+ // This is handled in other methods by detecting and skipping them
39
+ }
40
+ if (this.settings.errorHandling.throwOnError) {
41
+ this.validateMdx(result);
42
+ }
43
+ return result;
44
+ }
45
+ /**
46
+ * Rule 1: Add 1 empty line between elements
47
+ */
48
+ addEmptyLineBetweenElements(content) {
49
+ const lines = content.split('\n');
50
+ const result = [];
51
+ let inCodeBlock = false;
52
+ let inJsxBlock = false;
53
+ let inAdmonition = false;
54
+ let inFrontmatter = false;
55
+ let jsxDepth = 0;
56
+ for (let i = 0; i < lines.length; i++) {
57
+ const line = lines[i];
58
+ const nextLine = lines[i + 1] || '';
59
+ const trimmedLine = line.trim();
60
+ const trimmedNext = nextLine.trim();
61
+ // Track frontmatter (YAML between --- markers at start of file)
62
+ if (i === 0 && line === '---') {
63
+ inFrontmatter = true;
64
+ }
65
+ else if (inFrontmatter && line === '---') {
66
+ inFrontmatter = false;
67
+ }
68
+ // Track code blocks
69
+ if (line.startsWith('```')) {
70
+ inCodeBlock = !inCodeBlock;
71
+ }
72
+ // Track admonitions
73
+ if (line.match(/^:::(note|tip|info|warning|danger|caution)/)) {
74
+ inAdmonition = true;
75
+ }
76
+ else if (inAdmonition && line === ':::') {
77
+ inAdmonition = false;
78
+ }
79
+ // Track JSX blocks
80
+ if (!inCodeBlock && !inAdmonition && !inFrontmatter) {
81
+ const openTags = (line.match(/<[A-Z][^>]*(?<!\/\s*)>/g) || []).length;
82
+ const closeTags = (line.match(/<\/[A-Z][^>]*>/g) || []).length;
83
+ jsxDepth += openTags - closeTags;
84
+ inJsxBlock = jsxDepth > 0;
85
+ }
86
+ result.push(line);
87
+ // Don't add empty lines inside code blocks, JSX blocks, admonitions, or frontmatter
88
+ if (inCodeBlock || inJsxBlock || inAdmonition || inFrontmatter) {
89
+ continue;
90
+ }
91
+ // Check if we need to add an empty line
92
+ if (i < lines.length - 1 && trimmedLine && trimmedNext && nextLine !== '') {
93
+ const needsEmptyLine = this.shouldAddEmptyLine(line, nextLine);
94
+ // Only add if there isn't already an empty line
95
+ if (needsEmptyLine && lines[i + 1] !== '') {
96
+ result.push('');
97
+ }
98
+ }
99
+ }
100
+ // Remove multiple consecutive empty lines (normalize to 1)
101
+ return result.join('\n').replace(/\n{3,}/g, '\n\n');
102
+ }
103
+ shouldAddEmptyLine(currentLine, nextLine) {
104
+ const current = currentLine.trim();
105
+ const next = nextLine.trim();
106
+ // Heading followed by anything
107
+ if (current.match(/^#{1,6}\s/))
108
+ return true;
109
+ // Anything followed by heading
110
+ if (next.match(/^#{1,6}\s/))
111
+ return true;
112
+ // List followed by paragraph
113
+ if (current.match(/^[-*+]\s/) && !next.match(/^[-*+]\s/))
114
+ return true;
115
+ // Paragraph followed by list
116
+ if (!current.match(/^[-*+]\s/) && next.match(/^[-*+]\s/))
117
+ return true;
118
+ // Code block boundaries
119
+ if (current.startsWith('```'))
120
+ return true;
121
+ if (next.startsWith('```'))
122
+ return true;
123
+ // JSX component boundaries (both single line and multi-line)
124
+ if (current.match(/^<[A-Z].*\/>$/))
125
+ return true;
126
+ if (next.match(/^<[A-Z].*\/>$/))
127
+ return true;
128
+ if (current.match(/^<\/[A-Z]/))
129
+ return true; // Closing JSX tag
130
+ if (next.match(/^<[A-Z]/))
131
+ return true; // Opening JSX tag
132
+ // Admonition boundaries
133
+ if (current === ':::')
134
+ return true;
135
+ if (next.match(/^:::(note|tip|info|warning|danger|caution)/))
136
+ return true;
137
+ return false;
138
+ }
139
+ /**
140
+ * Combined JSX formatting - handles structure and content indentation together
141
+ */
142
+ formatJsxStructure(content) {
143
+ const lines = content.split('\n');
144
+ const result = [];
145
+ const jsxStack = [];
146
+ let inCodeBlock = false;
147
+ const containerComponents = this.settings.indentJsxContent.containerComponents;
148
+ for (let i = 0; i < lines.length; i++) {
149
+ const line = lines[i];
150
+ const trimmed = line.trim();
151
+ // Handle code blocks - but still apply indentation if inside JSX
152
+ if (trimmed.startsWith('```')) {
153
+ if (jsxStack.length > 0) {
154
+ // Code block inside JSX - apply indentation
155
+ const baseIndent = ' '.repeat(jsxStack.length);
156
+ result.push(baseIndent + trimmed);
157
+ inCodeBlock = !inCodeBlock;
158
+ }
159
+ else {
160
+ // Code block outside JSX - no indentation
161
+ result.push(line);
162
+ inCodeBlock = !inCodeBlock;
163
+ }
164
+ continue;
165
+ }
166
+ if (inCodeBlock) {
167
+ if (jsxStack.length > 0) {
168
+ // Inside code block within JSX - apply indentation
169
+ const baseIndent = ' '.repeat(jsxStack.length);
170
+ result.push(baseIndent + trimmed);
171
+ }
172
+ else {
173
+ // Inside code block outside JSX - preserve as is
174
+ result.push(line);
175
+ }
176
+ continue;
177
+ }
178
+ // Handle JSX tags
179
+ if (trimmed.match(/^<([A-Z][A-Za-z0-9]*)(?:\s[^>]*)?>$/)) {
180
+ // Opening tag
181
+ const tagMatch = trimmed.match(/^<([A-Z][A-Za-z0-9]*)/);
182
+ const tagName = tagMatch[1];
183
+ // Add appropriate indentation
184
+ const indent = ' '.repeat(jsxStack.length);
185
+ result.push(indent + trimmed);
186
+ // Push to stack if not self-closing
187
+ if (!trimmed.endsWith('/>')) {
188
+ jsxStack.push({
189
+ name: tagName,
190
+ isContainer: containerComponents.includes(tagName),
191
+ hasContent: false,
192
+ });
193
+ }
194
+ }
195
+ else if (trimmed.match(/^<\/([A-Z][A-Za-z0-9]*)>$/)) {
196
+ // Closing tag
197
+ jsxStack.pop();
198
+ const indent = ' '.repeat(jsxStack.length);
199
+ result.push(indent + trimmed);
200
+ }
201
+ else if (jsxStack.length > 0) {
202
+ // Content inside JSX
203
+ const currentTag = jsxStack[jsxStack.length - 1];
204
+ const baseIndent = ' '.repeat(jsxStack.length);
205
+ // Add spacing between heading and content if needed
206
+ if (trimmed.match(/^#{1,6}\s/) && currentTag.hasContent) {
207
+ // This is a heading after some content, add blank line before
208
+ result.push('');
209
+ }
210
+ // If the line is empty, just push empty line (no indent)
211
+ if (trimmed === '') {
212
+ result.push('');
213
+ }
214
+ else {
215
+ result.push(baseIndent + trimmed);
216
+ }
217
+ // Check if we need to add blank line after heading
218
+ if (trimmed.match(/^#{1,6}\s/)) {
219
+ // Look ahead to see if next line is content
220
+ if (i + 1 < lines.length &&
221
+ lines[i + 1].trim() &&
222
+ !lines[i + 1].trim().match(/^#{1,6}\s/)) {
223
+ result.push('');
224
+ }
225
+ }
226
+ if (trimmed !== '') {
227
+ currentTag.hasContent = true;
228
+ }
229
+ }
230
+ else {
231
+ // Regular content outside JSX
232
+ result.push(line);
233
+ }
234
+ }
235
+ return result.join('\n');
236
+ }
237
+ /**
238
+ * Rule 4: Expand single-line JSX with 2+ props
239
+ */
240
+ expandSingleLineJsx(content) {
241
+ const threshold = this.settings.expandSingleLineJsx.propsThreshold;
242
+ // First, handle self-closing tags - use more careful parsing
243
+ const lines = content.split('\n');
244
+ const processedLines = lines.map((line) => {
245
+ // Look for JSX components
246
+ if (line.includes('<') && line.includes('/>')) {
247
+ // Parse more carefully
248
+ const startMatch = line.match(/<([A-Z][A-Za-z0-9]*)\s+/);
249
+ if (startMatch) {
250
+ const componentName = startMatch[1];
251
+ const startIndex = line.indexOf(startMatch[0]);
252
+ const endIndex = line.lastIndexOf('/>');
253
+ if (endIndex > startIndex) {
254
+ const fullTag = line.substring(startIndex, endIndex + 2);
255
+ const propsStart = startMatch[0].length;
256
+ const propsString = fullTag.substring(propsStart, fullTag.length - 2).trim();
257
+ // Parse and count props
258
+ const props = this.parseJsxProps(propsString);
259
+ if (props.length >= threshold) {
260
+ // Format as multi-line
261
+ let result = `<${componentName}`;
262
+ props.forEach((prop) => {
263
+ result += `\n ${prop}`;
264
+ });
265
+ result += '\n/>';
266
+ // Replace in line
267
+ return line.substring(0, startIndex) + result + line.substring(endIndex + 2);
268
+ }
269
+ }
270
+ }
271
+ }
272
+ return line;
273
+ });
274
+ content = processedLines.join('\n');
275
+ // Then handle opening tags with content
276
+ content = content.replace(/<([A-Z][A-Za-z0-9]*)\s+([^>]+)>([^<]*)<\/\1>/g, (match, componentName, propsString, innerContent) => {
277
+ // Parse and count props
278
+ const props = this.parseJsxProps(propsString);
279
+ if (props.length >= threshold) {
280
+ // Format as multi-line
281
+ let result = `<${componentName}`;
282
+ props.forEach((prop) => {
283
+ result += `\n ${prop}`;
284
+ });
285
+ result += '\n>\n ' + innerContent.trim() + `\n</${componentName}>`;
286
+ return result;
287
+ }
288
+ return match;
289
+ });
290
+ return content;
291
+ }
292
+ parseJsxProps(propsString) {
293
+ const props = [];
294
+ let current = '';
295
+ let inQuotes = false;
296
+ let quoteChar = '';
297
+ let braceDepth = 0;
298
+ let i = 0;
299
+ while (i < propsString.length) {
300
+ const char = propsString[i];
301
+ if (!inQuotes && braceDepth === 0 && (char === '"' || char === "'")) {
302
+ inQuotes = true;
303
+ quoteChar = char;
304
+ current += char;
305
+ }
306
+ else if (inQuotes && char === quoteChar && propsString[i - 1] !== '\\') {
307
+ inQuotes = false;
308
+ quoteChar = '';
309
+ current += char;
310
+ }
311
+ else if (!inQuotes && char === '{') {
312
+ braceDepth++;
313
+ current += char;
314
+ }
315
+ else if (!inQuotes && char === '}') {
316
+ braceDepth--;
317
+ current += char;
318
+ }
319
+ else if (!inQuotes && braceDepth === 0 && char === ' ') {
320
+ if (current.trim()) {
321
+ props.push(current.trim());
322
+ }
323
+ current = '';
324
+ }
325
+ else {
326
+ current += char;
327
+ }
328
+ i++;
329
+ }
330
+ if (current.trim()) {
331
+ props.push(current.trim());
332
+ }
333
+ return props;
334
+ }
335
+ /**
336
+ * Rule 7: Validate MDX and throw errors on invalid content
337
+ */
338
+ validateMdx(content) {
339
+ // First, remove all code blocks and inline code to avoid false positives
340
+ let cleanedContent = content;
341
+ // Remove fenced code blocks (```...```)
342
+ cleanedContent = cleanedContent.replace(/```[\s\S]*?```/g, '');
343
+ // Remove inline code (`...`)
344
+ cleanedContent = cleanedContent.replace(/`[^`]*`/g, '');
345
+ // Now validate only the cleaned content
346
+ // Simple check for unclosed quotes - but be more careful with complex props
347
+ const lines = cleanedContent.split('\n');
348
+ for (const line of lines) {
349
+ // Check for unclosed quotes in simpler cases
350
+ let inString = false;
351
+ let stringChar = '';
352
+ let braceDepth = 0;
353
+ for (let i = 0; i < line.length; i++) {
354
+ const char = line[i];
355
+ const prevChar = i > 0 ? line[i - 1] : '';
356
+ if (!inString && (char === '"' || char === "'")) {
357
+ inString = true;
358
+ stringChar = char;
359
+ }
360
+ else if (inString && char === stringChar && prevChar !== '\\') {
361
+ inString = false;
362
+ stringChar = '';
363
+ }
364
+ else if (!inString && char === '{') {
365
+ braceDepth++;
366
+ }
367
+ else if (!inString && char === '}') {
368
+ braceDepth--;
369
+ }
370
+ }
371
+ // Only throw error if we have unmatched quotes outside of braces
372
+ if (inString && braceDepth === 0 && line.includes('<') && line.includes('=')) {
373
+ throw new Error('Invalid MDX: Unclosed attribute quotes');
374
+ }
375
+ }
376
+ // Check for unclosed JSX tags, but handle self-closing tags properly
377
+ const openTags = [];
378
+ // Parse JSX tags more carefully to handle complex props
379
+ let i = 0;
380
+ while (i < cleanedContent.length) {
381
+ if (cleanedContent[i] === '<') {
382
+ // Check if it's a closing tag
383
+ if (cleanedContent[i + 1] === '/') {
384
+ // Find the closing tag name
385
+ let j = i + 2;
386
+ while (j < cleanedContent.length && cleanedContent[j].match(/[A-Za-z0-9]/)) {
387
+ j++;
388
+ }
389
+ if (j > i + 2 && cleanedContent[j] === '>') {
390
+ const tagName = cleanedContent.substring(i + 2, j);
391
+ if (tagName[0] === tagName[0].toUpperCase()) {
392
+ const lastOpen = openTags.pop();
393
+ if (lastOpen !== tagName) {
394
+ throw new Error(`Invalid MDX: Mismatched JSX tags. Expected </${lastOpen}> but found </${tagName}>`);
395
+ }
396
+ }
397
+ i = j + 1;
398
+ continue;
399
+ }
400
+ }
401
+ // Check if it's an opening tag (starts with capital letter)
402
+ else if (cleanedContent[i + 1] && cleanedContent[i + 1].match(/[A-Z]/)) {
403
+ // Find the tag name
404
+ let j = i + 1;
405
+ while (j < cleanedContent.length && cleanedContent[j].match(/[A-Za-z0-9]/)) {
406
+ j++;
407
+ }
408
+ const tagName = cleanedContent.substring(i + 1, j);
409
+ // Make sure this is actually a JSX tag (next char should be space, /, or >)
410
+ if (j < cleanedContent.length && !cleanedContent[j].match(/[\s\/>]/)) {
411
+ // Not a JSX tag, skip it
412
+ i++;
413
+ continue;
414
+ }
415
+ // Now skip to the end of the tag, handling props properly
416
+ let inStr = false;
417
+ let strChar = '';
418
+ let brDepth = 0;
419
+ let isSelfClosing = false;
420
+ while (j < cleanedContent.length) {
421
+ const char = cleanedContent[j];
422
+ const prevChar = j > 0 ? cleanedContent[j - 1] : '';
423
+ if (!inStr && (char === '"' || char === "'")) {
424
+ inStr = true;
425
+ strChar = char;
426
+ }
427
+ else if (inStr && char === strChar && prevChar !== '\\') {
428
+ inStr = false;
429
+ }
430
+ else if (!inStr && char === '{') {
431
+ brDepth++;
432
+ }
433
+ else if (!inStr && char === '}') {
434
+ brDepth--;
435
+ }
436
+ else if (!inStr && brDepth === 0 && char === '/' && cleanedContent[j + 1] === '>') {
437
+ isSelfClosing = true;
438
+ j += 2;
439
+ break;
440
+ }
441
+ else if (!inStr && brDepth === 0 && char === '>') {
442
+ j++;
443
+ break;
444
+ }
445
+ j++;
446
+ }
447
+ if (!isSelfClosing) {
448
+ openTags.push(tagName);
449
+ }
450
+ i = j;
451
+ continue;
452
+ }
453
+ }
454
+ i++;
455
+ }
456
+ if (openTags.length > 0) {
457
+ throw new Error(`Invalid MDX: Unclosed JSX tags: ${openTags.join(', ')}`);
458
+ }
459
+ // Check for invalid nesting (simplified check)
460
+ if (cleanedContent.includes('<p>') && cleanedContent.includes('<div>')) {
461
+ // Check if div is inside p (invalid in HTML/MDX)
462
+ const invalidNesting = /<p[^>]*>[\s\S]*?<div[^>]*>[\s\S]*?<\/div>[\s\S]*?<\/p>/;
463
+ if (invalidNesting.test(cleanedContent)) {
464
+ throw new Error('Invalid MDX: <div> cannot be nested inside <p>');
465
+ }
466
+ }
467
+ return true;
468
+ }
469
+ }