@takazudo/mdx-formatter 0.1.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 +21 -0
- package/README.md +72 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +128 -0
- package/dist/html-block-formatter.d.ts +46 -0
- package/dist/html-block-formatter.js +370 -0
- package/dist/hybrid-formatter.d.ts +59 -0
- package/dist/hybrid-formatter.js +977 -0
- package/dist/indent-detector.d.ts +62 -0
- package/dist/indent-detector.js +358 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +57 -0
- package/dist/load-config.d.ts +13 -0
- package/dist/load-config.js +71 -0
- package/dist/plugins/docusaurus-admonitions.d.ts +5 -0
- package/dist/plugins/docusaurus-admonitions.js +42 -0
- package/dist/plugins/fix-autolink-output.d.ts +4 -0
- package/dist/plugins/fix-autolink-output.js +24 -0
- package/dist/plugins/fix-formatting-issues.d.ts +4 -0
- package/dist/plugins/fix-formatting-issues.js +42 -0
- package/dist/plugins/fix-paragraph-spacing.d.ts +5 -0
- package/dist/plugins/fix-paragraph-spacing.js +96 -0
- package/dist/plugins/html-definition-list.d.ts +5 -0
- package/dist/plugins/html-definition-list.js +64 -0
- package/dist/plugins/japanese-text.d.ts +5 -0
- package/dist/plugins/japanese-text.js +79 -0
- package/dist/plugins/normalize-lists.d.ts +5 -0
- package/dist/plugins/normalize-lists.js +58 -0
- package/dist/plugins/preprocess-japanese.d.ts +7 -0
- package/dist/plugins/preprocess-japanese.js +15 -0
- package/dist/plugins/preserve-image-alt.d.ts +8 -0
- package/dist/plugins/preserve-image-alt.js +19 -0
- package/dist/plugins/preserve-jsx.d.ts +6 -0
- package/dist/plugins/preserve-jsx.js +48 -0
- package/dist/settings.d.ts +7 -0
- package/dist/settings.js +91 -0
- package/dist/specific-formatter.d.ts +30 -0
- package/dist/specific-formatter.js +469 -0
- package/dist/types.d.ts +226 -0
- package/dist/types.js +4 -0
- package/dist/utils.d.ts +12 -0
- package/dist/utils.js +47 -0
- package/format-stdin.js +36 -0
- package/package.json +107 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Takazudo
|
|
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,72 @@
|
|
|
1
|
+
# @takazudo/mdx-formatter
|
|
2
|
+
|
|
3
|
+
AST-based markdown and MDX formatter with Japanese text support. Built on top of the unified ecosystem with remark plugins.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
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
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @takazudo/mdx-formatter
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or use directly with npx:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx @takazudo/mdx-formatter --write "**/*.md"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### CLI
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Check files (exit with error if formatting needed)
|
|
35
|
+
mdx-formatter --check "**/*.{md,mdx}"
|
|
36
|
+
|
|
37
|
+
# Format files in place
|
|
38
|
+
mdx-formatter --write "**/*.{md,mdx}"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### API
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
import { format } from '@takazudo/mdx-formatter';
|
|
45
|
+
|
|
46
|
+
const formatted = await format('# Hello\nWorld');
|
|
47
|
+
console.log(formatted); // '# Hello\n\nWorld'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Documentation
|
|
51
|
+
|
|
52
|
+
For full documentation including configuration, options reference, formatting rules, and API reference, visit the [documentation site](https://takazudo.github.io/mdx-formatter/).
|
|
53
|
+
|
|
54
|
+
## Development
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Install dependencies
|
|
58
|
+
pnpm install
|
|
59
|
+
|
|
60
|
+
# Run tests
|
|
61
|
+
pnpm test
|
|
62
|
+
|
|
63
|
+
# Run tests in watch mode
|
|
64
|
+
pnpm test:watch
|
|
65
|
+
|
|
66
|
+
# Run tests with coverage
|
|
67
|
+
pnpm test:coverage
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import { glob } from 'glob';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { formatFile, checkFile } from './index.js';
|
|
7
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
8
|
+
program
|
|
9
|
+
.name('mdx-formatter')
|
|
10
|
+
.description('AST-based markdown and MDX formatter')
|
|
11
|
+
.version(pkg.version)
|
|
12
|
+
.argument('[patterns...]', 'Glob patterns for files to format', ['**/*.{md,mdx}'])
|
|
13
|
+
.option('-w, --write', 'Write formatted files in place')
|
|
14
|
+
.option('-c, --check', 'Check if files need formatting')
|
|
15
|
+
.option('--config <path>', 'Path to config file (.mdx-formatter.json)')
|
|
16
|
+
.option('--ignore <patterns>', 'Comma-separated patterns to ignore', 'node_modules/**,dist/**,build/**,.git/**,worktrees/**')
|
|
17
|
+
.action(async (patterns, options) => {
|
|
18
|
+
try {
|
|
19
|
+
await main(patterns, options);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
23
|
+
console.error(chalk.red('Error:'), message);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
program.parse();
|
|
28
|
+
/**
|
|
29
|
+
* Main CLI function
|
|
30
|
+
*/
|
|
31
|
+
async function main(patterns, options) {
|
|
32
|
+
const ignorePatterns = options.ignore.split(',').map((p) => p.trim());
|
|
33
|
+
// Build format options from CLI flags
|
|
34
|
+
const formatOptions = {};
|
|
35
|
+
if (options.config) {
|
|
36
|
+
formatOptions.config = options.config;
|
|
37
|
+
}
|
|
38
|
+
// Find all matching files
|
|
39
|
+
const files = [];
|
|
40
|
+
for (const pattern of patterns) {
|
|
41
|
+
const matches = await glob(pattern, {
|
|
42
|
+
ignore: ignorePatterns,
|
|
43
|
+
nodir: true,
|
|
44
|
+
});
|
|
45
|
+
files.push(...matches);
|
|
46
|
+
}
|
|
47
|
+
// Remove duplicates
|
|
48
|
+
const uniqueFiles = [...new Set(files)];
|
|
49
|
+
if (uniqueFiles.length === 0) {
|
|
50
|
+
console.log(chalk.yellow('No files found matching the patterns.'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
console.log(chalk.blue(`Processing ${uniqueFiles.length} file(s)...`));
|
|
54
|
+
let changedCount = 0;
|
|
55
|
+
let errorCount = 0;
|
|
56
|
+
for (const file of uniqueFiles) {
|
|
57
|
+
try {
|
|
58
|
+
if (options.write) {
|
|
59
|
+
const changed = await formatFile(file, formatOptions);
|
|
60
|
+
if (changed) {
|
|
61
|
+
changedCount++;
|
|
62
|
+
console.log(chalk.green('✓'), chalk.gray(file), chalk.green('formatted'));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.log(chalk.gray('○'), chalk.gray(file), chalk.gray('unchanged'));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (options.check) {
|
|
69
|
+
const needsFormatting = await checkFile(file, formatOptions);
|
|
70
|
+
if (needsFormatting) {
|
|
71
|
+
changedCount++;
|
|
72
|
+
console.log(chalk.yellow('⚠'), chalk.gray(file), chalk.yellow('needs formatting'));
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
console.log(chalk.green('✓'), chalk.gray(file), chalk.green('formatted correctly'));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// Default: just show what would be done
|
|
80
|
+
const needsFormatting = await checkFile(file, formatOptions);
|
|
81
|
+
if (needsFormatting) {
|
|
82
|
+
changedCount++;
|
|
83
|
+
console.log(chalk.blue('→'), chalk.gray(file), chalk.blue('would be formatted'));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.log(chalk.gray('○'), chalk.gray(file), chalk.gray('already formatted'));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
errorCount++;
|
|
92
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
93
|
+
console.error(chalk.red('✗'), chalk.gray(file), chalk.red(message));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Summary
|
|
97
|
+
console.log();
|
|
98
|
+
if (options.write) {
|
|
99
|
+
if (changedCount > 0) {
|
|
100
|
+
console.log(chalk.green(`✓ Formatted ${changedCount} file(s)`));
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
console.log(chalk.gray('All files are already formatted'));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (options.check) {
|
|
107
|
+
if (changedCount > 0) {
|
|
108
|
+
console.log(chalk.yellow(`⚠ ${changedCount} file(s) need formatting`));
|
|
109
|
+
process.exit(1); // Exit with error code for CI
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.log(chalk.green('✓ All files are formatted correctly'));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
if (changedCount > 0) {
|
|
117
|
+
console.log(chalk.blue(`→ ${changedCount} file(s) would be formatted`));
|
|
118
|
+
console.log(chalk.gray('Use --write to apply changes'));
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
console.log(chalk.gray('All files are already formatted'));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (errorCount > 0) {
|
|
125
|
+
console.log(chalk.red(`✗ ${errorCount} error(s) occurred`));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Block Formatter
|
|
3
|
+
* Formats HTML blocks within MDX content using Prettier or built-in formatting
|
|
4
|
+
*/
|
|
5
|
+
import type { FormatHtmlBlocksInMdxSetting } from './types.js';
|
|
6
|
+
export declare class HtmlBlockFormatter {
|
|
7
|
+
private settings;
|
|
8
|
+
private readonly htmlElements;
|
|
9
|
+
constructor(settings?: Partial<FormatHtmlBlocksInMdxSetting>);
|
|
10
|
+
/**
|
|
11
|
+
* Check if a tag name is an HTML element (not a JSX component)
|
|
12
|
+
*/
|
|
13
|
+
isHtmlElement(tagName: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Format HTML content using Prettier
|
|
16
|
+
*/
|
|
17
|
+
formatWithPrettier(html: string): Promise<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Extract HTML block from position in original content
|
|
20
|
+
*/
|
|
21
|
+
extractHtmlBlock(content: string, startPos: {
|
|
22
|
+
line: number;
|
|
23
|
+
column: number;
|
|
24
|
+
}, endPos: {
|
|
25
|
+
line: number;
|
|
26
|
+
column: number;
|
|
27
|
+
}): string;
|
|
28
|
+
/**
|
|
29
|
+
* Find matching closing tag for an opening tag
|
|
30
|
+
*/
|
|
31
|
+
findMatchingClosingTag(content: string, startIndex: number, tagName: string): number;
|
|
32
|
+
/**
|
|
33
|
+
* Format MDX content with HTML block formatting
|
|
34
|
+
*/
|
|
35
|
+
format(content: string): Promise<string>;
|
|
36
|
+
/**
|
|
37
|
+
* Replace HTML block in content with formatted version
|
|
38
|
+
*/
|
|
39
|
+
replaceHtmlBlock(content: string, startPos: {
|
|
40
|
+
line: number;
|
|
41
|
+
column: number;
|
|
42
|
+
}, endPos: {
|
|
43
|
+
line: number;
|
|
44
|
+
column: number;
|
|
45
|
+
}, replacement: string): string;
|
|
46
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Block Formatter
|
|
3
|
+
* Formats HTML blocks within MDX content using Prettier or built-in formatting
|
|
4
|
+
*/
|
|
5
|
+
import * as prettier from 'prettier';
|
|
6
|
+
export class HtmlBlockFormatter {
|
|
7
|
+
settings;
|
|
8
|
+
htmlElements;
|
|
9
|
+
constructor(settings = {}) {
|
|
10
|
+
this.settings = {
|
|
11
|
+
enabled: true,
|
|
12
|
+
description: '',
|
|
13
|
+
formatterConfig: {
|
|
14
|
+
parser: 'html',
|
|
15
|
+
tabWidth: 2,
|
|
16
|
+
useTabs: false,
|
|
17
|
+
},
|
|
18
|
+
...settings,
|
|
19
|
+
};
|
|
20
|
+
// List of HTML elements (not JSX components which start with uppercase)
|
|
21
|
+
this.htmlElements = new Set([
|
|
22
|
+
// Structure
|
|
23
|
+
'html',
|
|
24
|
+
'head',
|
|
25
|
+
'body',
|
|
26
|
+
'div',
|
|
27
|
+
'span',
|
|
28
|
+
'section',
|
|
29
|
+
'article',
|
|
30
|
+
'aside',
|
|
31
|
+
'header',
|
|
32
|
+
'footer',
|
|
33
|
+
'main',
|
|
34
|
+
'nav',
|
|
35
|
+
'figure',
|
|
36
|
+
'figcaption',
|
|
37
|
+
// Text
|
|
38
|
+
'p',
|
|
39
|
+
'h1',
|
|
40
|
+
'h2',
|
|
41
|
+
'h3',
|
|
42
|
+
'h4',
|
|
43
|
+
'h5',
|
|
44
|
+
'h6',
|
|
45
|
+
'blockquote',
|
|
46
|
+
'pre',
|
|
47
|
+
'code',
|
|
48
|
+
'em',
|
|
49
|
+
'strong',
|
|
50
|
+
'i',
|
|
51
|
+
'b',
|
|
52
|
+
'u',
|
|
53
|
+
's',
|
|
54
|
+
'mark',
|
|
55
|
+
'small',
|
|
56
|
+
'del',
|
|
57
|
+
'ins',
|
|
58
|
+
'sub',
|
|
59
|
+
'sup',
|
|
60
|
+
'cite',
|
|
61
|
+
'q',
|
|
62
|
+
'abbr',
|
|
63
|
+
'address',
|
|
64
|
+
'time',
|
|
65
|
+
// Lists
|
|
66
|
+
'ul',
|
|
67
|
+
'ol',
|
|
68
|
+
'li',
|
|
69
|
+
'dl',
|
|
70
|
+
'dt',
|
|
71
|
+
'dd',
|
|
72
|
+
// Tables
|
|
73
|
+
'table',
|
|
74
|
+
'thead',
|
|
75
|
+
'tbody',
|
|
76
|
+
'tfoot',
|
|
77
|
+
'tr',
|
|
78
|
+
'td',
|
|
79
|
+
'th',
|
|
80
|
+
'caption',
|
|
81
|
+
'colgroup',
|
|
82
|
+
'col',
|
|
83
|
+
// Forms
|
|
84
|
+
'form',
|
|
85
|
+
'input',
|
|
86
|
+
'textarea',
|
|
87
|
+
'button',
|
|
88
|
+
'select',
|
|
89
|
+
'option',
|
|
90
|
+
'optgroup',
|
|
91
|
+
'label',
|
|
92
|
+
'fieldset',
|
|
93
|
+
'legend',
|
|
94
|
+
'datalist',
|
|
95
|
+
'output',
|
|
96
|
+
'progress',
|
|
97
|
+
'meter',
|
|
98
|
+
// Media
|
|
99
|
+
'img',
|
|
100
|
+
'audio',
|
|
101
|
+
'video',
|
|
102
|
+
'source',
|
|
103
|
+
'track',
|
|
104
|
+
'picture',
|
|
105
|
+
'iframe',
|
|
106
|
+
'embed',
|
|
107
|
+
'object',
|
|
108
|
+
'param',
|
|
109
|
+
'canvas',
|
|
110
|
+
'svg',
|
|
111
|
+
// Other
|
|
112
|
+
'a',
|
|
113
|
+
'br',
|
|
114
|
+
'hr',
|
|
115
|
+
'details',
|
|
116
|
+
'summary',
|
|
117
|
+
'dialog',
|
|
118
|
+
'menu',
|
|
119
|
+
'menuitem',
|
|
120
|
+
'script',
|
|
121
|
+
'noscript',
|
|
122
|
+
'template',
|
|
123
|
+
'slot',
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Check if a tag name is an HTML element (not a JSX component)
|
|
128
|
+
*/
|
|
129
|
+
isHtmlElement(tagName) {
|
|
130
|
+
if (!tagName)
|
|
131
|
+
return false;
|
|
132
|
+
// HTML elements are lowercase or known HTML elements
|
|
133
|
+
return this.htmlElements.has(tagName.toLowerCase());
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Format HTML content using Prettier
|
|
137
|
+
*/
|
|
138
|
+
async formatWithPrettier(html) {
|
|
139
|
+
try {
|
|
140
|
+
// Preprocess: Remove newlines within dd and dt tags to keep them on single lines
|
|
141
|
+
// This is important for Japanese text readability in definition lists
|
|
142
|
+
const preprocessed = html
|
|
143
|
+
.replace(/<dd>([\s\S]*?)<\/dd>/g, (_match, content) => {
|
|
144
|
+
// Replace multiple whitespaces (including newlines) with single space
|
|
145
|
+
const cleaned = content.replace(/\s+/g, ' ').trim();
|
|
146
|
+
return `<dd>${cleaned}</dd>`;
|
|
147
|
+
})
|
|
148
|
+
.replace(/<dt>([\s\S]*?)<\/dt>/g, (_match, content) => {
|
|
149
|
+
// Same for dt tags
|
|
150
|
+
const cleaned = content.replace(/\s+/g, ' ').trim();
|
|
151
|
+
return `<dt>${cleaned}</dt>`;
|
|
152
|
+
});
|
|
153
|
+
const formatted = await prettier.format(preprocessed, {
|
|
154
|
+
parser: this.settings.formatterConfig.parser || 'html',
|
|
155
|
+
printWidth: 999999, // Never wrap lines
|
|
156
|
+
tabWidth: this.settings.formatterConfig.tabWidth || 2,
|
|
157
|
+
useTabs: this.settings.formatterConfig.useTabs || false,
|
|
158
|
+
htmlWhitespaceSensitivity: 'css', // Use CSS mode to handle whitespace better
|
|
159
|
+
bracketSameLine: true, // Keep closing bracket on same line to prevent broken tags
|
|
160
|
+
singleAttributePerLine: false,
|
|
161
|
+
});
|
|
162
|
+
// Remove trailing newline that prettier adds
|
|
163
|
+
let result = formatted.replace(/\n$/, '');
|
|
164
|
+
// Remove self-closing slashes from void elements if not present in original
|
|
165
|
+
// This maintains compatibility with existing MDX content
|
|
166
|
+
const voidElements = ['input', 'br', 'hr', 'img', 'meta', 'link'];
|
|
167
|
+
for (const elem of voidElements) {
|
|
168
|
+
const originalHasSelfClosing = new RegExp(`<${elem}[^>]*/>`, 'i').test(html);
|
|
169
|
+
if (!originalHasSelfClosing) {
|
|
170
|
+
// Remove the self-closing slash that Prettier adds
|
|
171
|
+
result = result.replace(new RegExp(`(<${elem}[^>]*?)\\s*/>`, 'gi'), '$1>');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Special handling for dt/dd tags - trim content inside them
|
|
175
|
+
// This preserves the original formatting requirement for definition lists
|
|
176
|
+
result = result.replace(/<(dt|dd)>\s*(.*?)\s*<\/(dt|dd)>/g, '<$1>$2</$1>');
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return html;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Extract HTML block from position in original content
|
|
185
|
+
*/
|
|
186
|
+
extractHtmlBlock(content, startPos, endPos) {
|
|
187
|
+
const lines = content.split('\n');
|
|
188
|
+
const startLine = startPos.line - 1;
|
|
189
|
+
const endLine = endPos.line - 1;
|
|
190
|
+
const startCol = startPos.column - 1;
|
|
191
|
+
const endCol = endPos.column - 1;
|
|
192
|
+
if (startLine === endLine) {
|
|
193
|
+
// Single line
|
|
194
|
+
return lines[startLine].substring(startCol, endCol);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// Multi-line
|
|
198
|
+
const extractedLines = [];
|
|
199
|
+
extractedLines.push(lines[startLine].substring(startCol));
|
|
200
|
+
for (let i = startLine + 1; i < endLine; i++) {
|
|
201
|
+
extractedLines.push(lines[i]);
|
|
202
|
+
}
|
|
203
|
+
extractedLines.push(lines[endLine].substring(0, endCol));
|
|
204
|
+
return extractedLines.join('\n');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Find matching closing tag for an opening tag
|
|
209
|
+
*/
|
|
210
|
+
findMatchingClosingTag(content, startIndex, tagName) {
|
|
211
|
+
let depth = 1;
|
|
212
|
+
let index = startIndex;
|
|
213
|
+
const openPattern = new RegExp(`<${tagName}(?:\\s[^>]*)?>`, 'gi');
|
|
214
|
+
const closePattern = new RegExp(`<\\/${tagName}>`, 'gi');
|
|
215
|
+
while (depth > 0 && index < content.length) {
|
|
216
|
+
// Reset lastIndex for each search
|
|
217
|
+
openPattern.lastIndex = index;
|
|
218
|
+
closePattern.lastIndex = index;
|
|
219
|
+
const openMatch = openPattern.exec(content);
|
|
220
|
+
const closeMatch = closePattern.exec(content);
|
|
221
|
+
if (!closeMatch) {
|
|
222
|
+
// No closing tag found
|
|
223
|
+
return -1;
|
|
224
|
+
}
|
|
225
|
+
if (!openMatch || closeMatch.index < openMatch.index) {
|
|
226
|
+
// Found closing tag before next opening tag
|
|
227
|
+
depth--;
|
|
228
|
+
index = closeMatch.index + closeMatch[0].length;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
// Found opening tag before closing tag
|
|
232
|
+
depth++;
|
|
233
|
+
index = openMatch.index + openMatch[0].length;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return depth === 0 ? index : -1;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Format MDX content with HTML block formatting
|
|
240
|
+
*/
|
|
241
|
+
async format(content) {
|
|
242
|
+
if (!this.settings.enabled) {
|
|
243
|
+
return content;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
// Find HTML blocks - handle nested tags properly
|
|
247
|
+
const blocks = [];
|
|
248
|
+
const processedRanges = [];
|
|
249
|
+
// First pass: find all opening tags
|
|
250
|
+
const openingTagPattern = /<(\w+)(?:\s[^>]*)?>(?!.*\/>)/g;
|
|
251
|
+
let match;
|
|
252
|
+
while ((match = openingTagPattern.exec(content)) !== null) {
|
|
253
|
+
const tagName = match[1];
|
|
254
|
+
if (this.isHtmlElement(tagName)) {
|
|
255
|
+
const startIndex = match.index;
|
|
256
|
+
const afterOpenTag = match.index + match[0].length;
|
|
257
|
+
// Find the matching closing tag
|
|
258
|
+
const endIndex = this.findMatchingClosingTag(content, afterOpenTag, tagName);
|
|
259
|
+
if (endIndex !== -1) {
|
|
260
|
+
// Check if this range overlaps with any processed range
|
|
261
|
+
let overlaps = false;
|
|
262
|
+
for (const range of processedRanges) {
|
|
263
|
+
if ((startIndex >= range[0] && startIndex < range[1]) ||
|
|
264
|
+
(endIndex > range[0] && endIndex <= range[1])) {
|
|
265
|
+
overlaps = true;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (!overlaps) {
|
|
270
|
+
blocks.push({
|
|
271
|
+
start: startIndex,
|
|
272
|
+
end: endIndex,
|
|
273
|
+
content: content.substring(startIndex, endIndex),
|
|
274
|
+
tagName: tagName,
|
|
275
|
+
});
|
|
276
|
+
processedRanges.push([startIndex, endIndex]);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Also handle self-closing tags
|
|
282
|
+
const selfClosingPattern = /<(\w+)(?:\s[^>]*)?\/>/g;
|
|
283
|
+
while ((match = selfClosingPattern.exec(content)) !== null) {
|
|
284
|
+
const tagName = match[1];
|
|
285
|
+
if (this.isHtmlElement(tagName)) {
|
|
286
|
+
const startIndex = match.index;
|
|
287
|
+
const endIndex = match.index + match[0].length;
|
|
288
|
+
// Check if this is within an already processed block
|
|
289
|
+
let inBlock = false;
|
|
290
|
+
for (const range of processedRanges) {
|
|
291
|
+
if (startIndex >= range[0] && endIndex <= range[1]) {
|
|
292
|
+
inBlock = true;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (!inBlock) {
|
|
297
|
+
blocks.push({
|
|
298
|
+
start: startIndex,
|
|
299
|
+
end: endIndex,
|
|
300
|
+
content: match[0],
|
|
301
|
+
tagName: tagName,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Sort blocks by position (reverse order to maintain positions)
|
|
307
|
+
blocks.sort((a, b) => b.start - a.start);
|
|
308
|
+
// Apply formatting to each HTML block
|
|
309
|
+
let result = content;
|
|
310
|
+
for (const block of blocks) {
|
|
311
|
+
// Always use Prettier for HTML formatting
|
|
312
|
+
const formatted = await this.formatWithPrettier(block.content);
|
|
313
|
+
// Replace the original HTML with formatted version
|
|
314
|
+
result = result.substring(0, block.start) + formatted + result.substring(block.end);
|
|
315
|
+
}
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return content;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Replace HTML block in content with formatted version
|
|
324
|
+
*/
|
|
325
|
+
replaceHtmlBlock(content, startPos, endPos, replacement) {
|
|
326
|
+
const lines = content.split('\n');
|
|
327
|
+
const startLine = startPos.line - 1;
|
|
328
|
+
const endLine = endPos.line - 1;
|
|
329
|
+
const startCol = startPos.column - 1;
|
|
330
|
+
const endCol = endPos.column - 1;
|
|
331
|
+
if (startLine === endLine) {
|
|
332
|
+
// Single line replacement
|
|
333
|
+
lines[startLine] =
|
|
334
|
+
lines[startLine].substring(0, startCol) + replacement + lines[startLine].substring(endCol);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
// Multi-line replacement
|
|
338
|
+
const replacementLines = replacement.split('\n');
|
|
339
|
+
// Create new line array
|
|
340
|
+
const newLines = [];
|
|
341
|
+
// Add lines before the block
|
|
342
|
+
for (let i = 0; i < startLine; i++) {
|
|
343
|
+
newLines.push(lines[i]);
|
|
344
|
+
}
|
|
345
|
+
// Add first line (partial) + first replacement line
|
|
346
|
+
if (replacementLines.length === 1) {
|
|
347
|
+
// Single line replacement for multi-line original
|
|
348
|
+
newLines.push(lines[startLine].substring(0, startCol) +
|
|
349
|
+
replacementLines[0] +
|
|
350
|
+
lines[endLine].substring(endCol));
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
// Multi-line replacement
|
|
354
|
+
newLines.push(lines[startLine].substring(0, startCol) + replacementLines[0]);
|
|
355
|
+
// Add middle replacement lines
|
|
356
|
+
for (let i = 1; i < replacementLines.length - 1; i++) {
|
|
357
|
+
newLines.push(replacementLines[i]);
|
|
358
|
+
}
|
|
359
|
+
// Add last replacement line + rest of last original line
|
|
360
|
+
newLines.push(replacementLines[replacementLines.length - 1] + lines[endLine].substring(endCol));
|
|
361
|
+
}
|
|
362
|
+
// Add lines after the block
|
|
363
|
+
for (let i = endLine + 1; i < lines.length; i++) {
|
|
364
|
+
newLines.push(lines[i]);
|
|
365
|
+
}
|
|
366
|
+
return newLines.join('\n');
|
|
367
|
+
}
|
|
368
|
+
return lines.join('\n');
|
|
369
|
+
}
|
|
370
|
+
}
|